Browse Source

refactor: refactor project layout and spread to multiple components

feat_video_fine
konenet 4 years ago
parent
commit
9b061c2d6f
  1. 1124
      src/chat/Panel.jsx
  2. 146
      src/chat/panel/center/component/UserList.jsx
  3. 227
      src/chat/panel/center/component/UserSearch.jsx
  4. 16
      src/chat/panel/center/index.jsx
  5. 125
      src/chat/panel/left/component/SwitchChat.jsx
  6. 19
      src/chat/panel/left/component/UserInfo.jsx
  7. 16
      src/chat/panel/left/index.jsx
  8. 127
      src/chat/panel/right/component/ChatAudio.jsx
  9. 180
      src/chat/panel/right/component/ChatAudioOline.jsx
  10. 156
      src/chat/panel/right/component/ChatDetails.jsx
  11. 153
      src/chat/panel/right/component/ChatEdit.jsx
  12. 121
      src/chat/panel/right/component/ChatFile.jsx
  13. 162
      src/chat/panel/right/component/ChatShareScreen.jsx
  14. 148
      src/chat/panel/right/component/ChatVideo.jsx
  15. 180
      src/chat/panel/right/component/ChatVideoOline.jsx
  16. 199
      src/chat/panel/right/index.jsx
  17. 4
      src/chat/redux/module/index.jsx
  18. 76
      src/chat/redux/module/panel.jsx

1124
src/chat/Panel.jsx

File diff suppressed because it is too large Load Diff

146
src/chat/panel/center/component/UserList.jsx

@ -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

227
src/chat/panel/center/component/UserSearch.jsx

@ -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

16
src/chat/panel/center/index.jsx

@ -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 />
</>
);
}
}

125
src/chat/panel/left/component/SwitchChat.jsx

@ -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

19
src/chat/component/UserInfo.jsx → src/chat/panel/left/component/UserInfo.jsx

@ -11,8 +11,9 @@ import { @@ -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 { @@ -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 = () => {

16
src/chat/panel/left/index.jsx

@ -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 />
</>
);
}
}

127
src/chat/panel/right/component/ChatAudio.jsx

@ -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
// ArrayBufferUint8Array
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

180
src/chat/panel/right/component/ChatAudioOline.jsx

@ -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) {
// rtcTypeansweroffer
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

156
src/chat/panel/right/component/ChatDetails.jsx

@ -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

153
src/chat/panel/right/component/ChatEdit.jsx

@ -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
// ArrayBufferUint8Array
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

121
src/chat/panel/right/component/ChatFile.jsx

@ -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
// Uint8ArrayBuffer1 == 8ArrayBufferUint
// 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

162
src/chat/panel/right/component/ChatShareScreen.jsx

@ -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

148
src/chat/panel/right/component/ChatVideo.jsx

@ -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
// ArrayBufferUint8Array
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

180
src/chat/panel/right/component/ChatVideoOline.jsx

@ -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) {
// rtcTypeansweroffer
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

199
src/chat/panel/right/index.jsx

@ -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) {
// ArrayBufferbase64
var binary = '';
var bytes = new Uint8Array(imgData);
var len = bytes.byteLength;
for (var i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
let base64String = `data:image/jpeg;base64,${window.btoa(binary)}`;
this.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

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

@ -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));

76
src/chat/redux/module/panel.jsx

@ -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…
Cancel
Save