removed incomplete video conferences
This commit is contained in:
@@ -3244,80 +3244,6 @@ ul.phone-country-selector span.country {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Video conferencing. */
|
||||
.call-party-speaking {
|
||||
border: 2.5px solid #2196f3;
|
||||
}
|
||||
|
||||
.carousel-item .caller-name.inactive {
|
||||
position: absolute;
|
||||
bottom: 0.25rem;
|
||||
right: 0.25rem;
|
||||
max-width: 6.5rem;
|
||||
}
|
||||
|
||||
.carousel-item .muted {
|
||||
position: absolute;
|
||||
top: 0.25rem;
|
||||
right: 0.25rem;
|
||||
}
|
||||
|
||||
.carousel-item .avatar-box {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
/* Center the avatar in parent view. */
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
#carousel-wrapper {
|
||||
width: 100%;
|
||||
max-width: 54rem;
|
||||
position: relative;
|
||||
align-self: center;
|
||||
padding-top: 0.5rem;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
#carousel {
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
scroll-behavior: smooth;
|
||||
scrollbar-width: none; /* Hide scrollbar in Firefox */
|
||||
}
|
||||
#carousel::-webkit-scrollbar {
|
||||
display: none; /* Hide scrollbar in Safari and Chrome */
|
||||
}
|
||||
|
||||
#carousel-content {
|
||||
display: grid;
|
||||
grid-gap: 1rem;
|
||||
grid-auto-flow: column;
|
||||
}
|
||||
|
||||
#carousel-prev {
|
||||
top: 50%;
|
||||
left: 0;
|
||||
transform: translate(50%, -50%);
|
||||
}
|
||||
|
||||
#carousel-next {
|
||||
top: 50%;
|
||||
right: 0;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.carousel-item {
|
||||
position: relative;
|
||||
width: 8rem;
|
||||
height: 8rem;
|
||||
}
|
||||
|
||||
/*
|
||||
Very wide monitor.
|
||||
*/
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -36,8 +36,6 @@
|
||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-dom@18/umd/react-dom.development.js"></script>
|
||||
<!-- React-Intl for translations -->
|
||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@7/react-intl.iife.js"></script>
|
||||
<!-- LiveKit for group video calls -->
|
||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/livekit-client@1/dist/livekit-client.umd.js"></script>
|
||||
<!-- Displaying QR code with ID of the topic -->
|
||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.js"></script>
|
||||
</head>
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
<script crossorigin="anonymous" src="https://polyfill-fastly.io/v3/polyfill.min.js?features=Intl.Segmenter"></script>
|
||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react@18.3.1/umd/react.production.min.js"></script>
|
||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-dom@18.3.1/umd/react-dom.production.min.js"></script>
|
||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/livekit-client@1.10.0/dist/livekit-client.umd.js"></script>
|
||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@7.1.6/react-intl.iife.min.js"></script>
|
||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.min.js"></script>
|
||||
</head>
|
||||
|
||||
@@ -69,7 +69,6 @@
|
||||
"firebase": "^11.0.0",
|
||||
"libphonenumber-js": "^1.10.40",
|
||||
"qrcodejs": "^1.0.0",
|
||||
"livekit-client": "^1.10.0",
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0",
|
||||
"react-intl": "^7.0.0",
|
||||
|
||||
@@ -7,5 +7,3 @@ export const CALL_STATE_IN_PROGRESS = 3;
|
||||
// Video call message headers representing call state.
|
||||
export const CALL_HEAD_STARTED = 'started';
|
||||
|
||||
// Gap between video conference carousel items.
|
||||
export const VC_CAROUSEL_ITEM_GAP = 16;
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
-2
@@ -1653,7 +1653,7 @@
|
||||
"defaultMessage": "Conference call",
|
||||
"description": "Conference call label",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
"obsolete": true
|
||||
},
|
||||
"password_reset_success": {
|
||||
"translation": "Passwort erfolgreich zurückgesetzt",
|
||||
@@ -1667,7 +1667,7 @@
|
||||
"defaultMessage": "+ Join",
|
||||
"description": "Call to action to join a conference call",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
"obsolete": true
|
||||
},
|
||||
"scan_qr_code": {
|
||||
"translation": "QR-Code scannen",
|
||||
|
||||
+2
-2
@@ -1653,7 +1653,7 @@
|
||||
"defaultMessage": "Conference call",
|
||||
"description": "Conference call label",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
"obsolete": true
|
||||
},
|
||||
"password_reset_success": {
|
||||
"translation": "Password reset successfully",
|
||||
@@ -1667,7 +1667,7 @@
|
||||
"defaultMessage": "+ Join",
|
||||
"description": "Call to action to join a conference call",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
"obsolete": true
|
||||
},
|
||||
"scan_qr_code": {
|
||||
"translation": "Scan QR Code",
|
||||
|
||||
+2
-2
@@ -1653,7 +1653,7 @@
|
||||
"defaultMessage": "Conference call",
|
||||
"description": "Conference call label",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
"obsolete": true
|
||||
},
|
||||
"password_reset_success": {
|
||||
"translation": "Restablecimiento de contraseña con éxito",
|
||||
@@ -1667,7 +1667,7 @@
|
||||
"defaultMessage": "+ Join",
|
||||
"description": "Call to action to join a conference call",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
"obsolete": true
|
||||
},
|
||||
"scan_qr_code": {
|
||||
"translation": "Escanear código QR",
|
||||
|
||||
@@ -143,10 +143,6 @@
|
||||
"defaultMessage": "missed",
|
||||
"description": "Label for missed incoming call"
|
||||
},
|
||||
"calls_conference": {
|
||||
"defaultMessage": "Conference call",
|
||||
"description": "Conference call label"
|
||||
},
|
||||
"calls_incoming": {
|
||||
"defaultMessage": "Incoming call",
|
||||
"description": "Incoming call label"
|
||||
@@ -374,10 +370,6 @@
|
||||
"defaultMessage": "Invalid security token",
|
||||
"description": "Error message when resetting password."
|
||||
},
|
||||
"join_conference": {
|
||||
"defaultMessage": "+ Join",
|
||||
"description": "Call to action to join a conference call"
|
||||
},
|
||||
"label_client": {
|
||||
"defaultMessage": "Client:",
|
||||
"description": "Label for a client version"
|
||||
|
||||
+2
-2
@@ -1653,7 +1653,7 @@
|
||||
"defaultMessage": "Conference call",
|
||||
"description": "Conference call label",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
"obsolete": true
|
||||
},
|
||||
"password_reset_success": {
|
||||
"translation": "Mot de passe réinitialisé avec succès",
|
||||
@@ -1667,7 +1667,7 @@
|
||||
"defaultMessage": "+ Join",
|
||||
"description": "Call to action to join a conference call",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
"obsolete": true
|
||||
},
|
||||
"scan_qr_code": {
|
||||
"translation": "Scanner le code QR",
|
||||
|
||||
+2
-2
@@ -1653,7 +1653,7 @@
|
||||
"defaultMessage": "Conference call",
|
||||
"description": "Conference call label",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
"obsolete": true
|
||||
},
|
||||
"password_reset_success": {
|
||||
"translation": "비밀번호 재설정 성공",
|
||||
@@ -1667,7 +1667,7 @@
|
||||
"defaultMessage": "+ Join",
|
||||
"description": "Call to action to join a conference call",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
"obsolete": true
|
||||
},
|
||||
"scan_qr_code": {
|
||||
"translation": "QR 코드 스캔",
|
||||
|
||||
+2
-2
@@ -1653,7 +1653,7 @@
|
||||
"defaultMessage": "Conference call",
|
||||
"description": "Conference call label",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
"obsolete": true
|
||||
},
|
||||
"password_reset_success": {
|
||||
"translation": "Resetarea parolei cu succes",
|
||||
@@ -1667,7 +1667,7 @@
|
||||
"defaultMessage": "+ Join",
|
||||
"description": "Call to action to join a conference call",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
"obsolete": true
|
||||
},
|
||||
"scan_qr_code": {
|
||||
"translation": "Scanați codul QR",
|
||||
|
||||
+2
-2
@@ -1653,7 +1653,7 @@
|
||||
"defaultMessage": "Conference call",
|
||||
"description": "Conference call label",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
"obsolete": true
|
||||
},
|
||||
"password_reset_success": {
|
||||
"translation": "Пароль успешно изменен",
|
||||
@@ -1667,7 +1667,7 @@
|
||||
"defaultMessage": "+ Join",
|
||||
"description": "Call to action to join a conference call",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
"obsolete": true
|
||||
},
|
||||
"scan_qr_code": {
|
||||
"translation": "Отсканируйте QR код",
|
||||
|
||||
+2
-2
@@ -1653,14 +1653,14 @@
|
||||
"defaultMessage": "Conference call",
|
||||
"description": "Conference call label",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
"obsolete": true
|
||||
},
|
||||
"join_conference": {
|
||||
"translation": "",
|
||||
"defaultMessage": "+ Join",
|
||||
"description": "Call to action to join a conference call",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
"obsolete": true
|
||||
},
|
||||
"pin_message": {
|
||||
"translation": "",
|
||||
|
||||
+2
-2
@@ -1653,14 +1653,14 @@
|
||||
"defaultMessage": "Conference call",
|
||||
"description": "Conference call label",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
"obsolete": true
|
||||
},
|
||||
"join_conference": {
|
||||
"translation": "",
|
||||
"defaultMessage": "+ Join",
|
||||
"description": "Call to action to join a conference call",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
"obsolete": true
|
||||
},
|
||||
"pin_message": {
|
||||
"translation": "",
|
||||
|
||||
+2
-2
@@ -1653,7 +1653,7 @@
|
||||
"defaultMessage": "Conference call",
|
||||
"description": "Conference call label",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
"obsolete": true
|
||||
},
|
||||
"password_reset_success": {
|
||||
"translation": "密碼重置成功",
|
||||
@@ -1667,7 +1667,7 @@
|
||||
"defaultMessage": "+ Join",
|
||||
"description": "Call to action to join a conference call",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
"obsolete": true
|
||||
},
|
||||
"scan_qr_code": {
|
||||
"translation": "掃描二維碼",
|
||||
|
||||
+2
-2
@@ -1653,7 +1653,7 @@
|
||||
"defaultMessage": "Conference call",
|
||||
"description": "Conference call label",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
"obsolete": true
|
||||
},
|
||||
"password_reset_success": {
|
||||
"translation": "密码重置成功",
|
||||
@@ -1667,7 +1667,7 @@
|
||||
"defaultMessage": "+ Join",
|
||||
"description": "Call to action to join a conference call",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
"obsolete": true
|
||||
},
|
||||
"scan_qr_code": {
|
||||
"translation": "扫描二维码",
|
||||
|
||||
@@ -143,8 +143,6 @@ export function fullFormatter(style, data, values, key, stack) {
|
||||
attr.callState = data.state;
|
||||
attr.incoming = data.incoming;
|
||||
attr.duration = data.duration;
|
||||
attr.vc = data.vc;
|
||||
attr.onCallJoin = this.onCallJoin;
|
||||
}
|
||||
break;
|
||||
case 'VD':
|
||||
@@ -210,7 +208,7 @@ function handleImageData(el, data, attr) {
|
||||
attr.src = null;
|
||||
}
|
||||
} else {
|
||||
// Use custom element instead of <img>.
|
||||
// Use custom element instead of <img> or <video>.
|
||||
el = UploadingImage;
|
||||
}
|
||||
|
||||
|
||||
+16
-38
@@ -19,7 +19,6 @@ import LogoView from './logo-view.jsx';
|
||||
import MetaMessage from '../widgets/meta-message.jsx';
|
||||
import PinnedMessages from '../widgets/pinned-messages.jsx';
|
||||
import SendMessage from '../widgets/send-message.jsx';
|
||||
import VCPanel from '../widgets/vc-panel.jsx';
|
||||
import VideoPreview from '../widgets/video-preview.jsx';
|
||||
|
||||
import { DEFAULT_P2P_ACCESS_MODE, EDIT_PREVIEW_LENGTH, IMAGE_PREVIEW_DIM, IMMEDIATE_P2P_SUBSCRIPTION,
|
||||
@@ -1454,44 +1453,23 @@ class MessagesView extends React.Component {
|
||||
);
|
||||
} else if (this.state.rtcPanel) {
|
||||
// P2P call.
|
||||
if (Tinode.isP2PTopicName(this.state.rtcPanel)) {
|
||||
component2 = (
|
||||
<CallPanel
|
||||
topic={this.state.topic}
|
||||
seq={this.props.callSeq}
|
||||
callState={this.props.callState}
|
||||
callAudioOnly={this.props.callAudioOnly}
|
||||
tinode={this.props.tinode}
|
||||
title={this.state.title}
|
||||
avatar={this.state.avatar || true}
|
||||
component2 = (
|
||||
<CallPanel
|
||||
topic={this.state.topic}
|
||||
seq={this.props.callSeq}
|
||||
callState={this.props.callState}
|
||||
callAudioOnly={this.props.callAudioOnly}
|
||||
tinode={this.props.tinode}
|
||||
title={this.state.title}
|
||||
avatar={this.state.avatar || true}
|
||||
|
||||
onError={this.props.onError}
|
||||
onHangup={this.handleCallHangup}
|
||||
onInvite={this.props.onCallInvite}
|
||||
onSendOffer={this.props.onCallSendOffer}
|
||||
onIceCandidate={this.props.onCallIceCandidate}
|
||||
onSendAnswer={this.props.onCallSendAnswer} />
|
||||
);
|
||||
} else {
|
||||
// Video conference or stream.
|
||||
component2 = (
|
||||
<VCPanel
|
||||
topic={this.state.topic}
|
||||
seq={this.props.callSeq}
|
||||
callState={this.props.callState}
|
||||
callAudioOnly={this.props.callAudioOnly}
|
||||
tinode={this.props.tinode}
|
||||
title={this.state.title}
|
||||
avatar={this.state.avatar || true}
|
||||
myUid={this.props.myUserId}
|
||||
viewportWidth={this.props.viewportWidth}
|
||||
|
||||
onError={this.props.onError}
|
||||
onHangup={this.handleCallHangup}
|
||||
onCreateCall={this.props.onCallInvite}
|
||||
onJoin={this.props.onVCJoin} />
|
||||
);
|
||||
}
|
||||
onError={this.props.onError}
|
||||
onHangup={this.handleCallHangup}
|
||||
onInvite={this.props.onCallInvite}
|
||||
onSendOffer={this.props.onCallSendOffer}
|
||||
onIceCandidate={this.props.onCallIceCandidate}
|
||||
onSendAnswer={this.props.onCallSendAnswer} />
|
||||
);
|
||||
} else {
|
||||
const topic = this.props.tinode.getTopic(this.state.topic);
|
||||
const isChannel = topic.isChannelType();
|
||||
|
||||
@@ -181,7 +181,6 @@ class TinodeWeb extends React.Component {
|
||||
this.handleCallSendOffer = this.handleCallSendOffer.bind(this);
|
||||
this.handleCallIceCandidate = this.handleCallIceCandidate.bind(this);
|
||||
this.handleCallSendAnswer = this.handleCallSendAnswer.bind(this);
|
||||
this.handleVCCallJoin = this.handleVCCallJoin.bind(this);
|
||||
|
||||
this.handleCallAccept = this.handleCallAccept.bind(this);
|
||||
|
||||
@@ -1626,7 +1625,6 @@ class TinodeWeb extends React.Component {
|
||||
}
|
||||
|
||||
webrtc = writer && !!this.tinode.getServerParam('iceServers');
|
||||
const mediaServerAvailable = !!this.tinode.getServerParam('vcEndpoint');
|
||||
|
||||
return [
|
||||
subscribed ? {
|
||||
@@ -1637,9 +1635,8 @@ class TinodeWeb extends React.Component {
|
||||
title: this.props.intl.formatMessage(messages.menu_item_audio_call),
|
||||
handler: this.handleStartAudioCall
|
||||
} : null,
|
||||
subscribed && webrtc ? {
|
||||
subscribed && Tinode.isP2PTopicName(topicName) && webrtc ? {
|
||||
title: this.props.intl.formatMessage(messages.menu_item_video_call),
|
||||
disabled: Tinode.isGroupTopicName(topicName) && !mediaServerAvailable,
|
||||
handler: this.handleStartVideoCall
|
||||
} : null,
|
||||
subscribed ? 'messages_clear' : null,
|
||||
@@ -1797,12 +1794,7 @@ class TinodeWeb extends React.Component {
|
||||
switch (callState) {
|
||||
case CALL_STATE_OUTGOING_INITATED:
|
||||
const head = { webrtc: CALL_HEAD_STARTED, aonly: !!audioOnly };
|
||||
const msg = Drafty.videoCall(audioOnly);
|
||||
if (Tinode.isGroupTopicName(callTopic)) {
|
||||
// Video conference.
|
||||
head.vc = true;
|
||||
}
|
||||
return this.handleSendMessage(msg, undefined, undefined, head)
|
||||
return this.handleSendMessage(Drafty.videoCall(audioOnly), undefined, undefined, head)
|
||||
.then(ctrl => {
|
||||
if (ctrl.code < 200 || ctrl.code >= 300 || !ctrl.params || !ctrl.params.seq) {
|
||||
this.handleCallClose();
|
||||
@@ -1811,7 +1803,6 @@ class TinodeWeb extends React.Component {
|
||||
this.setState({callSeq: ctrl.params['seq']});
|
||||
return ctrl
|
||||
});
|
||||
break;
|
||||
case CALL_STATE_IN_PROGRESS:
|
||||
const topic = this.tinode.getTopic(callTopic);
|
||||
if (!topic) {
|
||||
@@ -1867,15 +1858,6 @@ class TinodeWeb extends React.Component {
|
||||
topic.videoCall('answer', callSeq, sdp);
|
||||
}
|
||||
|
||||
handleVCCallJoin(callTopic, callSeq) {
|
||||
const topic = this.tinode.getTopic(callTopic);
|
||||
if (!topic) {
|
||||
return;
|
||||
}
|
||||
|
||||
topic.videoCall('vc-join', callSeq);
|
||||
}
|
||||
|
||||
handleCallClose() {
|
||||
if (this.callTimeoutTimer) {
|
||||
clearTimeout(this.callTimeoutTimer);
|
||||
@@ -1896,7 +1878,7 @@ class TinodeWeb extends React.Component {
|
||||
this.handleTopicSelected(setCallTopic ? topicName : this.state.callTopic);
|
||||
const upd = {
|
||||
callState: CALL_STATE_IN_PROGRESS
|
||||
}
|
||||
};
|
||||
if (setCallTopic) {
|
||||
upd.callTopic = topicName;
|
||||
upd.callSeq = callSeq;
|
||||
@@ -2140,7 +2122,6 @@ class TinodeWeb extends React.Component {
|
||||
onCallSendOffer={this.handleCallSendOffer}
|
||||
onCallIceCandidate={this.handleCallIceCandidate}
|
||||
onCallSendAnswer={this.handleCallSendAnswer}
|
||||
onVCJoin={this.handleVCCallJoin}
|
||||
|
||||
errorText={this.state.errorText}
|
||||
errorLevel={this.state.errorLevel}
|
||||
|
||||
@@ -7,21 +7,12 @@ import { secondsToTime } from '../lib/strformat.js';
|
||||
|
||||
export default class CallMessage extends React.PureComponent {
|
||||
render() {
|
||||
const bigIcon = this.props.vc ?
|
||||
<div className="composed-material">
|
||||
<i className="material-icons gray second-small">group</i>
|
||||
<i className="material-icons gray big outline">call</i>
|
||||
</div>
|
||||
:
|
||||
<div><i className="material-icons big gray">call</i></div>;
|
||||
const isCallDropped = ['busy', 'declined', 'disconnected', 'missed'].includes(this.props.callState);
|
||||
const successClass = 'material-icons medium ' + (isCallDropped ? 'red' : 'green');
|
||||
const callIcon = this.props.incoming ?
|
||||
(isCallDropped ? 'call_missed' : 'call_received') :
|
||||
(isCallDropped ? 'call_missed_outgoing' : 'call_made');
|
||||
const title = this.props.vc ?
|
||||
<FormattedMessage id="calls_conference" defaultMessage="Conference call" description="Conference call label" /> :
|
||||
this.props.incoming ?
|
||||
const title = this.props.incoming ?
|
||||
<FormattedMessage id="calls_incoming" defaultMessage="Incoming call" description="Incoming call label" /> :
|
||||
<FormattedMessage id="calls_outgoing" defaultMessage="Outgoing call" description="Outgoing call label" />;
|
||||
let duration;
|
||||
@@ -45,17 +36,11 @@ export default class CallMessage extends React.PureComponent {
|
||||
} else {
|
||||
duration = <span>{secondsToTime(this.props.duration / 1000)}</span>;
|
||||
}
|
||||
const allowJoin = this.props.vc && this.props.callState == 'started';
|
||||
return <div className="call-message">
|
||||
<div><i className="material-icons big gray">{bigIcon}</i></div>
|
||||
<div><i className="material-icons big gray">call</i></div>
|
||||
<div className="flex-column narrow">
|
||||
<div>{title}</div>
|
||||
<div className="duration"><i className={successClass}>{callIcon}</i> {duration}</div>
|
||||
{allowJoin ?
|
||||
<a onClick={this.props.onCallJoin}>
|
||||
<FormattedMessage id="join_conference" defaultMessage="+ Join" description="Call to action to join a conference call" />
|
||||
</a>
|
||||
: null}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ class BaseChatMessage extends React.PureComponent {
|
||||
this.handleContextClick = this.handleContextClick.bind(this);
|
||||
this.handleCancelUpload = this.handleCancelUpload.bind(this);
|
||||
this.handleQuoteClick = this.handleQuoteClick.bind(this);
|
||||
this.handleCallJoin = this.handleCallJoin.bind(this);
|
||||
|
||||
this.formatterContext = {
|
||||
formatMessage: props.intl.formatMessage.bind(props.intl),
|
||||
@@ -39,8 +38,7 @@ class BaseChatMessage extends React.PureComponent {
|
||||
onImagePreview: this.handleExpandImage,
|
||||
onVideoPreview: this.handlePlayVideo,
|
||||
onFormButtonClick: this.handleFormButtonClick,
|
||||
onQuoteClick: this.handleQuoteClick,
|
||||
onCallJoin: this.handleCallJoin
|
||||
onQuoteClick: this.handleQuoteClick
|
||||
};
|
||||
}
|
||||
|
||||
@@ -151,10 +149,6 @@ class BaseChatMessage extends React.PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
handleCallJoin() {
|
||||
this.props.onAcceptCall(this.props.topic, true, this.props.seq);
|
||||
}
|
||||
|
||||
render() {
|
||||
const sideClass = this.props.sequence + ' ' + (this.props.response ? 'left' : 'right');
|
||||
const bubbleClass = (this.props.sequence == 'single' || this.props.sequence == 'last') ? 'bubble tip' : 'bubble';
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
// Video conference carousel item.
|
||||
import React from 'react';
|
||||
|
||||
import LetterTile from './letter-tile.jsx';
|
||||
|
||||
/**
|
||||
* Props:
|
||||
* - cameraPub
|
||||
* - micPub
|
||||
* - isSpeaking
|
||||
* - tinode
|
||||
* - photo
|
||||
* - userId
|
||||
* - name
|
||||
*/
|
||||
export default class VCCarouselItem extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleVideoRefChange = this.handleVideoRefChange.bind(this);
|
||||
this.handleAudioRefChange = this.handleAudioRefChange.bind(this);
|
||||
|
||||
this.videoRef = null;
|
||||
this.audioRef = null;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.videoRef && !this.videoRef.srcObject && this.props.cameraPub && this.props.cameraPub.videoTrack) {
|
||||
this.props.cameraPub.videoTrack.attach(this.videoRef);
|
||||
}
|
||||
if (this.audioRef && !this.audioRef.srcObject && this.props.micPub && this.props.micPub.audioTrack) {
|
||||
this.props.micPub.audioTrack.attach(this.audioRef);
|
||||
}
|
||||
}
|
||||
|
||||
handleVideoRefChange(node) {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.videoRef = node;
|
||||
|
||||
// Attach video track to video element.
|
||||
const cameraPub = this.props.cameraPub;
|
||||
const cameraEnabled = cameraPub && cameraPub.isSubscribed && !cameraPub.isMuted;
|
||||
if (cameraEnabled && cameraPub.videoTrack) {
|
||||
cameraPub.videoTrack.attach(node);
|
||||
}
|
||||
}
|
||||
|
||||
handleAudioRefChange(node) {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.audioRef = node;
|
||||
|
||||
// Attach audio track to audio element.
|
||||
const micPub = this.props.micPub;
|
||||
const micEnabled = micPub && micPub.isSubscribed && !micPub.isMuted;
|
||||
if (micEnabled && micPub.audioTrack) {
|
||||
micPub.audioTrack.attach(node);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let classes = 'call-party carousel-item';
|
||||
if (this.props.isSpeaking) {
|
||||
classes += ' call-party-speaking';
|
||||
}
|
||||
|
||||
const micPub = this.props.micPub;
|
||||
const cameraPub = this.props.cameraPub;
|
||||
return (
|
||||
<div className={classes}>
|
||||
<video ref={this.handleVideoRefChange}></video>
|
||||
<audio ref={this.handleAudioRefChange}></audio>
|
||||
{micPub && micPub.isMuted ?
|
||||
<i className="material-icons muted">mic_off</i>
|
||||
: null
|
||||
}
|
||||
{cameraPub && cameraPub.isMuted ?
|
||||
<div className="avatar-box">
|
||||
<LetterTile
|
||||
tinode={this.props.tinode}
|
||||
avatar={this.props.photo || true}
|
||||
topic={this.props.userId}
|
||||
title={this.props.name} />
|
||||
</div>
|
||||
: null}
|
||||
<div className="caller-name inactive">
|
||||
{this.props.name}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Track } from 'livekit-client';
|
||||
|
||||
import VCCarouselItem from './vc-carousel-item.jsx';
|
||||
|
||||
import { VC_CAROUSEL_ITEM_GAP } from '../constants.js';
|
||||
|
||||
export default class VCCarousel extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
prevVisible: false,
|
||||
nextVisible: false
|
||||
};
|
||||
|
||||
this.handleScrollReference = this.handleScrollReference.bind(this);
|
||||
this.handleScrollEvent = this.handleScrollEvent.bind(this);
|
||||
this.handleScrollClick = this.handleScrollClick.bind(this);
|
||||
|
||||
this.carouselRef = null;
|
||||
this.contentRef = React.createRef();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.carouselRef && this.props.viewportWidth != prevProps.viewportWidth) {
|
||||
this.handleScrollEvent({target: this.carouselRef});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.carouselRef) {
|
||||
this.carouselRef.removeEventListener('scroll', this.handleScrollEvent);
|
||||
}
|
||||
}
|
||||
|
||||
// Don't use React.createRef as the ref.current is not available in componentDidMount in this component.
|
||||
handleScrollReference(node) {
|
||||
if (node) {
|
||||
this.carouselRef = node;
|
||||
this.carouselRef.addEventListener('scroll', this.handleScrollEvent);
|
||||
this.handleScrollEvent({target: this.carouselRef});
|
||||
}
|
||||
}
|
||||
|
||||
// Get older messages and show/hide [go to latest message] button.
|
||||
handleScrollEvent(event) {
|
||||
this.setState({
|
||||
// Show [prev] / [next] as appropriate.
|
||||
prevVisible: event.target.scrollLeft > VC_CAROUSEL_ITEM_GAP,
|
||||
nextVisible: event.target.scrollWidth - event.target.scrollLeft - event.target.offsetWidth > VC_CAROUSEL_ITEM_GAP
|
||||
});
|
||||
}
|
||||
|
||||
handleScrollClick(next) {
|
||||
// this.carouselRef.offsetWidth - is the width of the visible window.
|
||||
const val = this.carouselRef.offsetWidth + VC_CAROUSEL_ITEM_GAP;
|
||||
this.carouselRef.scrollBy(next ? val : -val, 0);
|
||||
}
|
||||
|
||||
render() {
|
||||
let peers = [];
|
||||
if (this.props.participants) {
|
||||
Object.entries(this.props.participants).forEach(([identity, party], i) => {
|
||||
const participant = party.participant;
|
||||
const cameraPub = participant.getTrack(Track.Source.Camera);
|
||||
const micPub = participant.getTrack(Track.Source.Microphone);
|
||||
|
||||
peers.push(<VCCarouselItem
|
||||
tinode={this.props.tinode}
|
||||
isSpeaking={participant.isSpeaking}
|
||||
cameraPub={cameraPub ? cameraPub : null}
|
||||
micPub={micPub ? micPub : null}
|
||||
name={party.name}
|
||||
userId={identity}
|
||||
photo={party.photo}
|
||||
key={i}
|
||||
/>);
|
||||
});
|
||||
}
|
||||
return (
|
||||
<div id="carousel-wrapper">
|
||||
<div id="carousel" ref={this.handleScrollReference}>
|
||||
<div id="carousel-content" ref={this.contentRef}>
|
||||
{peers}
|
||||
</div>
|
||||
</div>
|
||||
{this.state.prevVisible ?
|
||||
<button id="carousel-prev" className="action-button" onClick={_ => this.handleScrollClick(false)}>
|
||||
<i className="material-icons">navigate_before</i>
|
||||
</button>
|
||||
:
|
||||
null}
|
||||
{this.state.nextVisible ?
|
||||
<button id="carousel-next" className="action-button" onClick={_ => this.handleScrollClick(true)}>
|
||||
<i className="material-icons">navigate_next</i>
|
||||
</button>
|
||||
:
|
||||
null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,412 +0,0 @@
|
||||
// Video conference or broadcast: local and/or peer viewports and controls.
|
||||
import React from 'react';
|
||||
import { FormattedMessage, injectIntl } from 'react-intl';
|
||||
|
||||
import {
|
||||
LocalVideoTrack,
|
||||
MediaDeviceFailure,
|
||||
ParticipantEvent,
|
||||
RemoteParticipant,
|
||||
Room,
|
||||
RoomEvent,
|
||||
Track,
|
||||
VideoPresets
|
||||
} from 'livekit-client';
|
||||
|
||||
import LetterTile from './letter-tile.jsx';
|
||||
import VCCarousel from './vc-carousel.jsx';
|
||||
import { makeImageUrl } from '../lib/blob-helpers.js';
|
||||
|
||||
import { MAX_PEER_TITLE_LENGTH } from '../config.js';
|
||||
import { CALL_STATE_OUTGOING_INITATED } from '../constants.js';
|
||||
|
||||
import { clipStr } from '../lib/utils.js'
|
||||
|
||||
class VCPanel extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
room: undefined,
|
||||
|
||||
participants: {},
|
||||
activeSpeaker: undefined,
|
||||
|
||||
previousOnInfo: undefined,
|
||||
};
|
||||
|
||||
this.isOutgoingCall = props.callState == CALL_STATE_OUTGOING_INITATED;
|
||||
const topic = this.props.tinode.getTopic(this.props.topic);
|
||||
this.isBroadcast = topic && topic.chan;
|
||||
|
||||
this.localRef = React.createRef();
|
||||
this.remoteRef = React.createRef();
|
||||
|
||||
this.onInfo = this.onInfo.bind(this);
|
||||
this.start = this.start.bind(this);
|
||||
this.stop = this.stop.bind(this);
|
||||
|
||||
this.reportError = this.reportError.bind(this);
|
||||
|
||||
this.handleCloseClick = this.handleCloseClick.bind(this);
|
||||
this.handleToggleCameraClick = this.handleToggleCameraClick.bind(this);
|
||||
this.handleToggleMicClick = this.handleToggleMicClick.bind(this);
|
||||
|
||||
this.handleRemoteHangup = this.handleRemoteHangup.bind(this);
|
||||
|
||||
this.handleTrackSubscribed = this.handleTrackSubscribed.bind(this);
|
||||
this.handleTrackUnsubscribed = this.handleTrackUnsubscribed.bind(this);
|
||||
this.handleActiveSpeakerChange = this.handleActiveSpeakerChange.bind(this);
|
||||
this.handleDisconnect = this.handleDisconnect.bind(this);
|
||||
this.handleLocalTrackUnpublished = this.handleLocalTrackUnpublished.bind(this);
|
||||
this.handleLocalTrackPublished = this.handleLocalTrackPublished.bind(this);
|
||||
this.handleParticipantConnected = this.handleParticipantConnected.bind(this);
|
||||
this.handleParticipantDisconnected = this.handleParticipantDisconnected.bind(this);
|
||||
this.handleData = this.handleData.bind(this);
|
||||
this.handleDevicesChanged = this.handleDevicesChanged.bind(this);
|
||||
this.startRoom = this.startRoom.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const topic = this.props.tinode.getTopic(this.props.topic);
|
||||
this.previousOnInfo = topic.onInfo;
|
||||
topic.onInfo = this.onInfo;
|
||||
this.start();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const topic = this.props.tinode.getTopic(this.props.topic);
|
||||
if (topic) {
|
||||
topic.onInfo = this.previousOnInfo;
|
||||
}
|
||||
this.stop();
|
||||
}
|
||||
|
||||
onInfo(info) {
|
||||
if (info.what != 'call') {
|
||||
return;
|
||||
}
|
||||
switch (info.event) {
|
||||
case 'vc-token':
|
||||
const ts = info.payload;
|
||||
this.setState({token: ts.token});
|
||||
this.startRoom(ts.token);
|
||||
break;
|
||||
case 'hang-up':
|
||||
this.handleRemoteHangup(info);
|
||||
break;
|
||||
default:
|
||||
console.warn("Unknown call event", info.event);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
handleParticipantConnected(participant) {
|
||||
if (!participant) return;
|
||||
if (participant instanceof RemoteParticipant) {
|
||||
participant
|
||||
.on(ParticipantEvent.TrackMuted, pub => {
|
||||
console.log('track was muted', pub.trackSid, participant.identity);
|
||||
this.forceUpdate(); // Redraw carousel.
|
||||
})
|
||||
.on(ParticipantEvent.TrackUnmuted, pub => {
|
||||
console.log('track was unmuted', pub.trackSid, participant.identity);
|
||||
this.forceUpdate(); // Redraw carousel.
|
||||
})
|
||||
.on(ParticipantEvent.IsSpeakingChanged, _ => {
|
||||
this.forceUpdate(); // Redraw carousel.
|
||||
})
|
||||
.on(ParticipantEvent.ConnectionQualityChanged, _ => {
|
||||
this.forceUpdate(); // Redraw carousel.
|
||||
});
|
||||
|
||||
const me = this.props.tinode.getMeTopic();
|
||||
const c = me.getContact(participant.identity);
|
||||
|
||||
const participants = Object.assign({}, this.state.participants);
|
||||
participants[participant.identity] = {
|
||||
name: c && c.public && c.public.fn ? c.public.fn : participant.identity,
|
||||
photo: makeImageUrl(c && c.public ? c.public.photo : null),
|
||||
participant: participant
|
||||
};
|
||||
this.setState({participants: participants});
|
||||
}
|
||||
}
|
||||
|
||||
handleParticipantDisconnected(participant) {
|
||||
const participants = Object.assign({}, this.state.participants);
|
||||
delete participants[participant.identity];
|
||||
this.setState({participants: participants});
|
||||
}
|
||||
|
||||
handleData(msg, participant) {
|
||||
console.log('Data message ', msg, participant);
|
||||
}
|
||||
|
||||
handleTrackSubscribed(track, publication, participant) {
|
||||
if (participant instanceof RemoteParticipant) {
|
||||
// Check if we're presenting a remote video stream.
|
||||
if (this.remoteRef.current && !(this.remoteRef.current.src != '' || this.remoteRef.srcObject)) {
|
||||
track.attach(this.remoteRef.current);
|
||||
}
|
||||
}
|
||||
// Redraw the carousel and remote view.
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
handleTrackUnsubscribed(track, publication, participant) {
|
||||
console.log('handleTrackUnsubscribed', track);
|
||||
// remove tracks from all attached elements
|
||||
track.detach();
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
handleLocalTrackPublished(pub) {
|
||||
console.log('Local track published', pub);
|
||||
const track = pub.track;
|
||||
if ((track instanceof LocalVideoTrack) && this.localRef.current) {
|
||||
const elem = this.localRef.current;
|
||||
track.attach(elem);
|
||||
// flip.
|
||||
elem.style.transform = 'scale(-1, 1)';
|
||||
}
|
||||
}
|
||||
|
||||
handleLocalTrackUnpublished(pub) {
|
||||
const track = pub.track;
|
||||
console.log('handleLocalTrackUnpublished', track);
|
||||
track.detach();
|
||||
// Redraw carousel.
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
handleActiveSpeakerChange(speakers) {
|
||||
console.log('handleActiveSpeakerChange', speakers);
|
||||
speakers.forEach(s => {
|
||||
if (s.isSpeaking) {
|
||||
console.log('Speaking participant ', s);
|
||||
|
||||
// Display speaker in remote peer view.
|
||||
const elem = this.remoteRef.current;
|
||||
const me = this.props.tinode.getMeTopic();
|
||||
const c = me.getContact(s.identity);
|
||||
const name = c && c.public && c.public.fn ? c.public.fn : undefined;
|
||||
this.setState({activeSpeaker: name});
|
||||
|
||||
const cameraPub = s.getTrack(Track.Source.Camera);
|
||||
const cameraEnabled = cameraPub && cameraPub.isSubscribed && !cameraPub.isMuted;
|
||||
if (cameraEnabled && cameraPub.videoTrack) {
|
||||
cameraPub.videoTrack.attach(elem);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleDevicesChanged() {
|
||||
console.log('devices changed');
|
||||
// TODO: implement.
|
||||
}
|
||||
|
||||
handleDisconnect() {
|
||||
console.log('disconnected from room');
|
||||
this.handleCloseClick();
|
||||
}
|
||||
|
||||
startRoom(token) {
|
||||
const roomOpts = {
|
||||
adaptiveStream: true,
|
||||
dynacast: true,
|
||||
publishDefaults: {
|
||||
simulcast: true,
|
||||
videoSimulcastLayers: [VideoPresets.h90, VideoPresets.h216],
|
||||
videoCodec: 'vp8'
|
||||
},
|
||||
videoCaptureDefaults: {
|
||||
resolution: VideoPresets.h720.resolution
|
||||
}
|
||||
};
|
||||
|
||||
const connectOpts = {
|
||||
autoSubscribe: true
|
||||
};
|
||||
|
||||
// creates a new room with options
|
||||
const room = new Room(roomOpts);
|
||||
|
||||
this.setState({room: room});
|
||||
|
||||
const startTime = Date.now();
|
||||
const topic = this.props.tinode.getTopic(this.props.topic);
|
||||
const shouldPublish = this.isOutgoingCall || !this.isBroadcast;
|
||||
|
||||
room
|
||||
.on(RoomEvent.ParticipantConnected, this.handleParticipantConnected)
|
||||
.on(RoomEvent.ParticipantDisconnected, this.handleParticipantDisconnected)
|
||||
.on(RoomEvent.DataReceived, this.handleData)
|
||||
.on(RoomEvent.TrackSubscribed, this.handleTrackSubscribed)
|
||||
.on(RoomEvent.TrackUnsubscribed, this.handleTrackUnsubscribed)
|
||||
.on(RoomEvent.ActiveSpeakersChanged, this.handleActiveSpeakerChange)
|
||||
.on(RoomEvent.Disconnected, this.handleDisconnect)
|
||||
.on(RoomEvent.LocalTrackUnpublished, this.handleLocalTrackUnpublished)
|
||||
.on(RoomEvent.LocalTrackPublished, this.handleLocalTrackPublished)
|
||||
|
||||
.on(RoomEvent.RoomMetadataChanged, metadata => {
|
||||
console.log('VC room new metadata ', metadata);
|
||||
})
|
||||
.on(RoomEvent.MediaDevicesChanged, this.handleDevicesChanged)
|
||||
.on(RoomEvent.AudioPlaybackStatusChanged, e => {
|
||||
// TODO: handle it.
|
||||
console.log('Audio playback status change: ', e);
|
||||
})
|
||||
.on(RoomEvent.MediaDevicesError, e => {
|
||||
const failure = MediaDeviceFailure.getFailure(e);
|
||||
this.reportError(failure);
|
||||
})
|
||||
.on(RoomEvent.ConnectionQualityChanged, (quality, participant) => {
|
||||
const id = participant ? participant.identity : 'nil';
|
||||
console.log('Connection quality changed', id, ' -> ', quality);
|
||||
})
|
||||
.on(RoomEvent.SignalConnected, _ => {
|
||||
const signalConnectionTime = Date.now() - startTime;
|
||||
console.log(`signal connection established in ${signalConnectionTime}ms`);
|
||||
// speed up publishing by starting to publish before it's fully connected
|
||||
// publishing is accepted as soon as signal connection has established
|
||||
if (shouldPublish) {
|
||||
room.localParticipant.enableCameraAndMicrophone();
|
||||
console.log(`tracks published in ${Date.now() - startTime}ms`);
|
||||
}
|
||||
});
|
||||
|
||||
const vcEndpoint = this.props.tinode.getServerParam('vcEndpoint', null);
|
||||
room.connect(vcEndpoint, token, connectOpts).then(_ => {
|
||||
console.log('Connected to VC room', room.name, '. Access token', token);
|
||||
|
||||
room.participants.forEach((participant) => {
|
||||
this.handleParticipantConnected(participant);
|
||||
});
|
||||
this.handleParticipantConnected(room.localParticipant);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
start() {
|
||||
if (this.state.localStream) {
|
||||
this.props.onError(this.props.intl.formatMessage(messages.already_in_call), 'info');
|
||||
return;
|
||||
}
|
||||
if (this.isOutgoingCall) {
|
||||
this.props.onCreateCall(this.props.topic, this.props.seq, this.props.callState, this.props.callAudioOnly)
|
||||
.then(ctrl => {
|
||||
const token = ctrl.params.token;
|
||||
const seq = ctrl.params.seq;
|
||||
this.setState({callSeq: seq, token: token});
|
||||
this.startRoom(token);
|
||||
});
|
||||
} else {
|
||||
this.props.onJoin(this.props.topic, this.props.seq);
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.state.room) {
|
||||
this.state.room.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
reportError(err) {
|
||||
this.props.onError(err.message, 'err');
|
||||
}
|
||||
|
||||
// Call disconnected by remote.
|
||||
handleRemoteHangup() {
|
||||
this.handleCloseClick();
|
||||
}
|
||||
|
||||
handleCloseClick() {
|
||||
this.stop();
|
||||
this.props.onHangup(this.props.topic, this.props.seq);
|
||||
}
|
||||
|
||||
handleToggleCameraClick() {
|
||||
if (!this.state.room) return;
|
||||
const enabled = this.state.room.localParticipant.isCameraEnabled;
|
||||
this.state.room.localParticipant.setCameraEnabled(!enabled)
|
||||
.then(_ => this.forceUpdate());
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
handleToggleMicClick() {
|
||||
if (!this.state.room) return;
|
||||
const enabled = this.state.room.localParticipant.isMicrophoneEnabled;
|
||||
this.state.room.localParticipant.setMicrophoneEnabled(!enabled)
|
||||
.then(_ => this.forceUpdate());
|
||||
|
||||
// Make sure we redraw the mute/unmute icons (e.g. mic -> mic_off).
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
render() {
|
||||
const isPublishing = this.isOutgoingCall || !this.isBroadcast;
|
||||
const disabled = !isPublishing || !this.state.room || !this.state.room.localParticipant;
|
||||
const audioIcon = !disabled && this.state.room.localParticipant.isMicrophoneEnabled ? 'mic' : 'mic_off';
|
||||
const videoIcon = !disabled && this.state.room.localParticipant.isCameraEnabled ? 'videocam' : 'videocam_off';
|
||||
|
||||
const peerTitle = this.state.activeSpeaker ? this.state.activeSpeaker : clipStr(this.props.title, MAX_PEER_TITLE_LENGTH);
|
||||
|
||||
const remoteActive = this.remoteRef.current && (this.remoteRef.current.src || this.remoteRef.current.srcObject)
|
||||
|
||||
return (
|
||||
<div id="video-container">
|
||||
{!this.isBroadcast && Object.keys(this.state.participants).length > 0 ?
|
||||
<VCCarousel
|
||||
viewportWidth={this.props.viewportWidth}
|
||||
tinode={this.props.tinode}
|
||||
participants={this.state.participants} /> :
|
||||
null
|
||||
}
|
||||
<div id="video-container-panel">
|
||||
{isPublishing ?
|
||||
<div className="call-party self" disabled={this.state.audioOnly}>
|
||||
<video ref={this.localRef} autoPlay muted playsInline />
|
||||
<div className="caller-name inactive">
|
||||
<FormattedMessage id="calls_you_label"
|
||||
defaultMessage="You" description="Shown over the local video screen" />
|
||||
</div>
|
||||
</div> :
|
||||
null }
|
||||
<div className="call-party peer" disabled={!remoteActive}>
|
||||
<video ref={this.remoteRef} autoPlay playsInline />
|
||||
{remoteActive ?
|
||||
<div className="caller-name inactive">{peerTitle}</div>
|
||||
:
|
||||
<div className="caller-card">
|
||||
<div className="avatar-box">
|
||||
<LetterTile
|
||||
tinode={this.props.tinode}
|
||||
avatar={this.props.avatar}
|
||||
topic={this.props.topic}
|
||||
title={this.props.title} />
|
||||
</div>
|
||||
<div className="caller-name">{peerTitle}</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className="controls">
|
||||
<button className="danger" onClick={this.handleCloseClick}>
|
||||
<i className="material-icons">call_end</i>
|
||||
</button>
|
||||
<button className="secondary" onClick={this.handleToggleCameraClick} disabled={disabled}>
|
||||
<i className="material-icons">{videoIcon}</i>
|
||||
</button>
|
||||
<button className="secondary" onClick={this.handleToggleMicClick} disabled={disabled}>
|
||||
<i className="material-icons">{audioIcon}</i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default injectIntl(VCPanel);
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+62
-695
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user