18 changed files with 2116 additions and 1085 deletions
@ -0,0 +1,146 @@
@@ -0,0 +1,146 @@
|
||||
import React from 'react'; |
||||
import { |
||||
List, |
||||
Badge, |
||||
Avatar, |
||||
} from 'antd'; |
||||
import { |
||||
FileOutlined, |
||||
} from '@ant-design/icons'; |
||||
|
||||
import moment from 'moment'; |
||||
import { connect } from 'react-redux' |
||||
import { actions } from '../../../redux/module/panel' |
||||
import * as Params from '../../../common/param/Params' |
||||
import { axiosGet } from '../../../util/Request'; |
||||
|
||||
|
||||
class UserList extends React.Component { |
||||
constructor(props) { |
||||
super(props) |
||||
this.state = { |
||||
chooseUser: {} |
||||
} |
||||
} |
||||
|
||||
componentDidMount() { |
||||
} |
||||
|
||||
/** |
||||
* 选择用户,获取对应的消息 |
||||
* @param {选择的用户} value |
||||
*/ |
||||
chooseUser = (value) => { |
||||
let chooseUser = { |
||||
toUser: value.uuid, |
||||
toUsername: value.username, |
||||
messageType: value.messageType, |
||||
avatar: value.avatar |
||||
} |
||||
this.fetchMessages(chooseUser); |
||||
} |
||||
|
||||
/** |
||||
* 获取消息 |
||||
*/ |
||||
fetchMessages = (chooseUser) => { |
||||
const { messageType, toUser, toUsername } = chooseUser |
||||
let uuid = localStorage.uuid |
||||
if (messageType === 2) { |
||||
uuid = toUser |
||||
} |
||||
let data = { |
||||
Uuid: uuid, |
||||
FriendUsername: toUsername, |
||||
MessageType: 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: <p>{content}</p>, |
||||
datetime: moment(data[i].createAt).fromNow(), |
||||
} |
||||
comments.push(comment) |
||||
} |
||||
|
||||
this.props.setMessageList(comments); |
||||
// 设置选择的用户信息时,需要先设置消息列表,防止已经完成了滑动到底部动作后,消息才获取完成,导致消息不能完全滑动到底部 |
||||
this.props.setChooseUser(chooseUser); |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* 根据文件类型渲染对应的标签,比如视频,图片等。 |
||||
* @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; |
||||
} |
||||
|
||||
|
||||
render() { |
||||
|
||||
return ( |
||||
<> |
||||
<List |
||||
itemLayout="horizontal" |
||||
dataSource={this.props.userList} |
||||
renderItem={item => ( |
||||
<List.Item> |
||||
<List.Item.Meta |
||||
style={{ paddingLeft: 30 }} |
||||
onClick={() => this.chooseUser(item)} |
||||
avatar={<Badge dot={true}><Avatar src={item.avatar} /></Badge>} |
||||
title={item.username} |
||||
description="" |
||||
/> |
||||
</List.Item> |
||||
)} |
||||
/> |
||||
</> |
||||
); |
||||
} |
||||
} |
||||
|
||||
|
||||
function mapStateToProps(state) { |
||||
return { |
||||
chooseUser: state.panelReducer.chooseUser, |
||||
userList: state.panelReducer.userList, |
||||
} |
||||
} |
||||
|
||||
function mapDispatchToProps(dispatch) { |
||||
return { |
||||
setChooseUser: (data) => dispatch(actions.setChooseUser(data)), |
||||
setUserList: (data) => dispatch(actions.setUserList(data)), |
||||
setMessageList: (data) => dispatch(actions.setMessageList(data)), |
||||
} |
||||
} |
||||
|
||||
UserList = connect(mapStateToProps, mapDispatchToProps)(UserList) |
||||
|
||||
export default UserList |
||||
@ -0,0 +1,227 @@
@@ -0,0 +1,227 @@
|
||||
import React from 'react'; |
||||
import { |
||||
Row, |
||||
Button, |
||||
Col, |
||||
Menu, |
||||
Modal, |
||||
Dropdown, |
||||
Input, |
||||
Form, |
||||
message |
||||
} from 'antd'; |
||||
import { PlusCircleOutlined } from '@ant-design/icons'; |
||||
|
||||
import { connect } from 'react-redux' |
||||
import { actions } from '../../../redux/module/panel' |
||||
import * as Params from '../../../common/param/Params' |
||||
import { axiosGet, axiosPostBody } from '../../../util/Request'; |
||||
|
||||
|
||||
class UserSearch extends React.Component { |
||||
groupForm = React.createRef(); |
||||
constructor(props) { |
||||
super(props) |
||||
this.state = { |
||||
showCreateGroup: false, |
||||
hasUser: false, |
||||
queryUser: { |
||||
username: '', |
||||
nickname: '', |
||||
}, |
||||
} |
||||
} |
||||
|
||||
componentDidMount() { |
||||
|
||||
} |
||||
|
||||
/** |
||||
* 搜索用户 |
||||
* @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 |
||||
}); |
||||
}; |
||||
|
||||
addUser = () => { |
||||
let data = { |
||||
uuid: localStorage.uuid, |
||||
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 + localStorage.uuid + "/" + this.state.queryUser.groupUuid) |
||||
.then(_response => { |
||||
message.success("添加成功") |
||||
// this.fetchUserList() |
||||
this.setState({ |
||||
hasUser: false |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
handleCancel = () => { |
||||
this.setState({ |
||||
hasUser: false |
||||
}); |
||||
}; |
||||
|
||||
showCreateGroup = () => { |
||||
this.setState({ |
||||
showCreateGroup: true |
||||
}); |
||||
} |
||||
|
||||
handleCancelGroup = () => { |
||||
this.setState({ |
||||
showCreateGroup: false |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* 创建群 |
||||
*/ |
||||
createGroup = () => { |
||||
console.log(this.groupForm.current.getFieldValue()) |
||||
let values = this.groupForm.current.getFieldValue(); |
||||
let data = { |
||||
name: values.groupName |
||||
} |
||||
|
||||
axiosPostBody(Params.GROUP_LIST_URL + "/" + localStorage.uuid, data) |
||||
.then(_response => { |
||||
message.success("添加成功") |
||||
this.setState({ |
||||
showCreateGroup: false |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
render() { |
||||
const menu = ( |
||||
<Menu> |
||||
<Menu.Item key={1}> |
||||
<Button type='link' onClick={this.showModal}>添加用户</Button> |
||||
</Menu.Item> |
||||
<Menu.Item key={2}> |
||||
<Button type='link' onClick={this.showModal}>添加群</Button> |
||||
</Menu.Item> |
||||
<Menu.Item key={3}> |
||||
<Button type='link' onClick={this.showCreateGroup}>创建群</Button> |
||||
</Menu.Item> |
||||
</Menu> |
||||
); |
||||
|
||||
return ( |
||||
<> |
||||
<Row> |
||||
<Col span={20} > |
||||
<Input.Group compact> |
||||
<Input.Search allowClear style={{ width: '100%' }} onSearch={this.searchUser} /> |
||||
</Input.Group> |
||||
</Col> |
||||
<Col> |
||||
<Dropdown overlay={menu} placement="bottomCenter" arrow> |
||||
<PlusCircleOutlined style={{ fontSize: 22, color: 'gray', marginLeft: 3, marginTop: 5 }} /> |
||||
</Dropdown> |
||||
</Col> |
||||
</Row> |
||||
|
||||
|
||||
<Modal title="用户信息" visible={this.state.hasUser} onCancel={this.handleCancel} okText="添加用户" footer={null}> |
||||
<Input.Group compact> |
||||
<Input.Search allowClear style={{ width: '100%' }} onSearch={this.searchUser} /> |
||||
</Input.Group> |
||||
<br /><hr /><br /> |
||||
|
||||
<p>用户名:{this.state.queryUser.username}</p> |
||||
<p>昵称:{this.state.queryUser.nickname}</p> |
||||
<Button type='primary' onClick={this.addUser} 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> |
||||
|
||||
<Modal title="创建群" visible={this.state.showCreateGroup} onCancel={this.handleCancelGroup} onOk={this.createGroup} okText="创建群"> |
||||
<Form |
||||
name="groupForm" |
||||
ref={this.groupForm} |
||||
layout="vertical" |
||||
autoComplete="off" |
||||
> |
||||
<Form.Item |
||||
name="groupName" |
||||
label="群名称" |
||||
rules={[{ required: true }]} |
||||
> |
||||
<Input placeholder="群名称" /> |
||||
</Form.Item> |
||||
</Form> |
||||
|
||||
</Modal> |
||||
</> |
||||
); |
||||
} |
||||
} |
||||
|
||||
|
||||
function mapStateToProps(state) { |
||||
return { |
||||
user: state.userInfoReducer.user, |
||||
} |
||||
} |
||||
|
||||
function mapDispatchToProps(dispatch) { |
||||
return { |
||||
setUser: (data) => dispatch(actions.setUser(data)), |
||||
} |
||||
} |
||||
|
||||
UserSearch = connect(mapStateToProps, mapDispatchToProps)(UserSearch) |
||||
|
||||
export default UserSearch |
||||
@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
|
||||
import React from 'react'; |
||||
import UserSearch from './component/UserSearch' |
||||
import UserList from './component/UserList' |
||||
|
||||
export default class CenterIndex extends React.Component { |
||||
|
||||
render() { |
||||
|
||||
return ( |
||||
<> |
||||
<UserSearch /> |
||||
<UserList /> |
||||
</> |
||||
); |
||||
} |
||||
} |
||||
@ -0,0 +1,125 @@
@@ -0,0 +1,125 @@
|
||||
import React from 'react'; |
||||
import { |
||||
Button, |
||||
} from 'antd'; |
||||
import { UserOutlined, TeamOutlined } from '@ant-design/icons'; |
||||
|
||||
import { connect } from 'react-redux' |
||||
import { actions } from '../../../redux/module/panel' |
||||
import * as Params from '../../../common/param/Params' |
||||
import { axiosGet } from '../../../util/Request'; |
||||
|
||||
|
||||
class SwitchChat extends React.Component { |
||||
constructor(props) { |
||||
super(props) |
||||
this.state = { |
||||
menuType: 1, |
||||
} |
||||
} |
||||
|
||||
componentDidMount() { |
||||
this.fetchUserList(); |
||||
} |
||||
|
||||
/** |
||||
* 获取好友列表 |
||||
*/ |
||||
fetchUserList = () => { |
||||
this.setState({ |
||||
menuType: 1, |
||||
}) |
||||
let data = { |
||||
uuid: localStorage.uuid |
||||
} |
||||
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.props.setUserList(data); |
||||
}) |
||||
} |
||||
|
||||
/** |
||||
* 获取群组列表 |
||||
*/ |
||||
fetchGroupList = () => { |
||||
this.setState({ |
||||
menuType: 2, |
||||
}) |
||||
let data = { |
||||
uuid: localStorage.uuid |
||||
} |
||||
axiosGet(Params.GROUP_LIST_URL + "/" + localStorage.uuid, 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.props.setUserList(data); |
||||
}) |
||||
} |
||||
|
||||
render() { |
||||
const { menuType } = this.state |
||||
return ( |
||||
<> |
||||
<p > |
||||
<Button |
||||
icon={<UserOutlined />} |
||||
size="large" |
||||
type='link' |
||||
disabled={menuType === 1} |
||||
onClick={this.fetchUserList} |
||||
style={{color: menuType === 1 ? '#1890ff' : 'gray'}} |
||||
> |
||||
</Button> |
||||
</p> |
||||
<p onClick={this.fetchGroupList}> |
||||
<Button |
||||
icon={<TeamOutlined />} |
||||
size="large" |
||||
type='link' |
||||
disabled={menuType === 2} |
||||
style={{color: menuType === 2 ? '#1890ff' : 'gray'}} |
||||
> |
||||
</Button> |
||||
</p> |
||||
</> |
||||
); |
||||
} |
||||
} |
||||
|
||||
|
||||
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 |
||||
@ -0,0 +1,16 @@
@@ -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 ( |
||||
<> |
||||
<UserInfo history={this.props.history} /> |
||||
<SwitchChat /> |
||||
</> |
||||
); |
||||
} |
||||
} |
||||
@ -0,0 +1,127 @@
@@ -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(<audio src={window.URL.createObjectURL(blob)} controls autoPlay={false} preload="auto" />); |
||||
} |
||||
|
||||
render() { |
||||
const { chooseUser } = this.props; |
||||
return ( |
||||
<> |
||||
<Tooltip title="发送语音"> |
||||
<Button |
||||
shape="circle" |
||||
onMouseDown={this.startAudio} |
||||
onMouseUp={this.stopAudio} |
||||
onTouchStart={this.startAudio} |
||||
onTouchEnd={this.stopAudio} |
||||
style={{ marginRight: 10 }} |
||||
icon={<AudioOutlined />} |
||||
disabled={chooseUser.toUser === ''} |
||||
/> |
||||
</Tooltip> |
||||
</> |
||||
); |
||||
} |
||||
} |
||||
|
||||
|
||||
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 |
||||
@ -0,0 +1,180 @@
@@ -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 ( |
||||
<> |
||||
<Tooltip title="语音聊天"> |
||||
<Button |
||||
shape="circle" |
||||
onClick={this.startAudioOnline} |
||||
style={{ marginRight: 10 }} |
||||
icon={<PhoneOutlined />} |
||||
disabled={chooseUser.toUser === ''} |
||||
/> |
||||
</Tooltip> |
||||
|
||||
<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 /> |
||||
|
||||
<audio id="audioPhone" autoPlay controls /> |
||||
</Drawer> |
||||
</> |
||||
); |
||||
} |
||||
} |
||||
|
||||
|
||||
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 |
||||
@ -0,0 +1,156 @@
@@ -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 }) => ( |
||||
<InfiniteScroll |
||||
dataLength={comments.length} |
||||
scrollableTarget="scrollableDiv" |
||||
> |
||||
<List |
||||
dataSource={comments} |
||||
itemLayout="horizontal" |
||||
renderItem={props => <Comment {...props} />} |
||||
/> |
||||
</InfiniteScroll> |
||||
); |
||||
|
||||
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 ( |
||||
<> |
||||
|
||||
<Badge.Ribbon text={<MoreOutlined onClick={this.chatDetails} />}> |
||||
|
||||
<Card title={this.props.chooseUser.toUsername} size="larg"> |
||||
<div |
||||
id="scrollableDiv" |
||||
style={{ |
||||
height: 450, |
||||
overflow: 'auto', |
||||
padding: '0 16px', |
||||
border: '0px solid rgba(140, 140, 140, 0.35)', |
||||
}} |
||||
> |
||||
{this.props.messageList.length > 0 && <CommentList comments={this.props.messageList} />} |
||||
|
||||
</div> |
||||
</Card> |
||||
|
||||
</Badge.Ribbon> |
||||
<Drawer title="成员列表" placement="right" onClose={this.drawerOnClose} visible={this.state.drawerVisible}> |
||||
<List |
||||
itemLayout="horizontal" |
||||
dataSource={this.state.groupUsers} |
||||
renderItem={item => ( |
||||
<List.Item> |
||||
<List.Item.Meta |
||||
style={{ paddingLeft: 30 }} |
||||
avatar={<Avatar src={Params.HOST + "/file/" + item.avatar} />} |
||||
title={item.username} |
||||
description="" |
||||
/> |
||||
</List.Item> |
||||
)} |
||||
/> |
||||
</Drawer> |
||||
</> |
||||
); |
||||
} |
||||
} |
||||
|
||||
|
||||
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 |
||||
@ -0,0 +1,153 @@
@@ -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 }) => ( |
||||
<> |
||||
<Form.Item> |
||||
<TextArea rows={4} onChange={onChange} value={value} id="messageArea" /> |
||||
</Form.Item> |
||||
<Form.Item> |
||||
<Button htmlType="submit" loading={submitting} onClick={onSubmit} type="primary" disabled={toUser === ''}> |
||||
Send |
||||
</Button> |
||||
</Form.Item> |
||||
</> |
||||
); |
||||
|
||||
|
||||
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 ( |
||||
<> |
||||
|
||||
<Comment |
||||
content={ |
||||
<Editor |
||||
onChange={this.handleChange} |
||||
onSubmit={this.handleSubmit} |
||||
submitting={submitting} |
||||
value={value} |
||||
toUser={toUser} |
||||
/> |
||||
} |
||||
/> |
||||
</> |
||||
); |
||||
} |
||||
} |
||||
|
||||
|
||||
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 |
||||
@ -0,0 +1,121 @@
@@ -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(<FileOutlined style={{ fontSize: 38 }} />) |
||||
} |
||||
|
||||
}) |
||||
reader.readAsArrayBuffer(files[0]) |
||||
} |
||||
|
||||
render() { |
||||
const { chooseUser } = this.props; |
||||
return ( |
||||
<> |
||||
<Tooltip title="上传图片或者文件"> |
||||
<input type='file' id='file' onChange={this.uploadFile} hidden disabled={chooseUser.toUser === ''} /> |
||||
<Button |
||||
onClick={this.clickFile} |
||||
shape="circle" |
||||
style={{ marginRight: 10 }} |
||||
icon={<FileAddOutlined />} |
||||
disabled={chooseUser.toUser === ''} |
||||
/> |
||||
</Tooltip> |
||||
</> |
||||
); |
||||
} |
||||
} |
||||
|
||||
|
||||
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 |
||||
@ -0,0 +1,162 @@
@@ -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 ( |
||||
<> |
||||
<Tooltip title="屏幕共享"> |
||||
<Button |
||||
shape="circle" |
||||
onClick={this.startShareOnline} |
||||
style={{ marginRight: 10 }} |
||||
icon={<DesktopOutlined />} disabled={chooseUser.toUser === ''} |
||||
/> |
||||
</Tooltip> |
||||
|
||||
<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="preview" width="700px" height="auto" autoPlay muted controls /> |
||||
<canvas id="canvas" width={this.state.currentScreen.width} height={this.state.currentScreen.height} /> |
||||
</Drawer> |
||||
</> |
||||
); |
||||
} |
||||
} |
||||
|
||||
|
||||
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 |
||||
@ -0,0 +1,148 @@
@@ -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(<video src={URL.createObjectURL(recordedBlob)} controls autoPlay={false} preload="auto" width='200px' />); |
||||
|
||||
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 ( |
||||
<> |
||||
<Tooltip placement="bottom" title="录制视频"> |
||||
<Popover content={<video id="preview" height="250px" width="auto" autoPlay muted />} title="视频"> |
||||
<Button |
||||
shape="circle" |
||||
onMouseDown={this.startVideoRecord} |
||||
onMouseUp={this.stopVideoRecord} |
||||
onTouchStart={this.startVideoRecord} |
||||
onTouchEnd={this.stopVideoRecord} |
||||
style={{ marginRight: 10 }} |
||||
icon={<VideoCameraAddOutlined />} |
||||
disabled={chooseUser.toUser === ''} |
||||
/> |
||||
</Popover> |
||||
</Tooltip> |
||||
</> |
||||
); |
||||
} |
||||
} |
||||
|
||||
|
||||
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 |
||||
@ -0,0 +1,180 @@
@@ -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 ( |
||||
<> |
||||
<Tooltip title="视频聊天"> |
||||
<Button |
||||
shape="circle" |
||||
onClick={this.startVideoOnline} |
||||
style={{ marginRight: 10 }} |
||||
icon={<VideoCameraOutlined />} disabled={chooseUser.toUser === ''} |
||||
/> |
||||
</Tooltip> |
||||
|
||||
<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="preview" width="700px" height="auto" autoPlay muted controls /> |
||||
<video id="remoteVideo" width="700px" height="auto" autoPlay muted controls /> |
||||
</Drawer> |
||||
</> |
||||
); |
||||
} |
||||
} |
||||
|
||||
|
||||
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 |
||||
@ -0,0 +1,199 @@
@@ -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: <p>{content}</p>, |
||||
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(<img src={base64String} alt="" width="150px" />); |
||||
} |
||||
|
||||
/** |
||||
* 发送消息 |
||||
* @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 ( |
||||
<> |
||||
<ChatDetails history={this.props.history} appendMessage={this.appendMessage} /> |
||||
<br /> |
||||
<ChatFile |
||||
history={this.props.history} |
||||
appendMessage={this.appendMessage} |
||||
appendImgToPanel={this.appendImgToPanel} |
||||
sendMessage={this.sendMessage} |
||||
/> |
||||
<ChatAudio |
||||
history={this.props.history} |
||||
appendMessage={this.appendMessage} |
||||
sendMessage={this.sendMessage} |
||||
/> |
||||
|
||||
<ChatVideo |
||||
history={this.props.history} |
||||
appendMessage={this.appendMessage} |
||||
sendMessage={this.sendMessage} |
||||
checkMediaPermisssion={this.checkMediaPermisssion} |
||||
/> |
||||
|
||||
<ChatShareScreen |
||||
history={this.props.history} |
||||
sendMessage={this.sendMessage} |
||||
checkMediaPermisssion={this.checkMediaPermisssion} |
||||
/> |
||||
|
||||
<ChatAudioOline |
||||
history={this.props.history} |
||||
sendMessage={this.sendMessage} |
||||
checkMediaPermisssion={this.checkMediaPermisssion} |
||||
/> |
||||
|
||||
<ChatVideoOline |
||||
history={this.props.history} |
||||
sendMessage={this.sendMessage} |
||||
checkMediaPermisssion={this.checkMediaPermisssion} |
||||
/> |
||||
|
||||
<Tooltip title="显示视频面板"> |
||||
<Button |
||||
shape="circle" |
||||
onClick={this.showMediaPanel} |
||||
style={{ marginRight: 10 }} |
||||
icon={<UngroupOutlined />} |
||||
/> |
||||
</Tooltip> |
||||
|
||||
<Tag icon={<SyncOutlined spin />} color="processing" hidden={!this.props.media.isRecord}> |
||||
录制中 |
||||
</Tag> |
||||
|
||||
<ChatEdit |
||||
history={this.props.history} |
||||
appendMessage={this.appendMessage} |
||||
appendImgToPanel={this.appendImgToPanel} |
||||
sendMessage={this.sendMessage} |
||||
/> |
||||
|
||||
</> |
||||
); |
||||
} |
||||
} |
||||
|
||||
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 |
||||
@ -1,9 +1,11 @@
@@ -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)); |
||||
@ -0,0 +1,76 @@
@@ -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 |
||||
Loading…
Reference in new issue