diff --git a/src/config.js b/src/config.js index 31ce984a..5ef4e417 100644 --- a/src/config.js +++ b/src/config.js @@ -107,11 +107,6 @@ export const LINK_PRIVACY_POLICY = 'https://tinode.co/privacy.html'; // Link to Terms of Service. export const LINK_TERMS_OF_SERVICE = 'https://tinode.co/terms.html'; -// Video call states. -export const CALL_STATE_NONE = 0; -export const CALL_STATE_OUTGOING_INITATED = 1; -export const CALL_STATE_INCOMING_RECEIVED = 2; -export const CALL_STATE_IN_PROGRESS = 3; // Video calls/WebRTC configuration. export const CALL_WEBRTC_CONFIG = { iceServers: [ diff --git a/src/constants.js b/src/constants.js new file mode 100644 index 00000000..bebdf415 --- /dev/null +++ b/src/constants.js @@ -0,0 +1,8 @@ +// Video call states. +export const CALL_STATE_NONE = 0; +export const CALL_STATE_OUTGOING_INITATED = 1; +export const CALL_STATE_INCOMING_RECEIVED = 2; +export const CALL_STATE_IN_PROGRESS = 3; + +// Mime type used for video call messages. +export const CALL_MESSAGE_MIME_TYPE = 'application/x-tinode-webrtc'; diff --git a/src/lib/utils.js b/src/lib/utils.js index 46944715..cc12e54c 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -2,6 +2,8 @@ import { Tinode } from 'tinode-sdk'; +import { CALL_MESSAGE_MIME_TYPE } from '../constants.js' + // Make shortcut icon appear with a green dot + show unread count in title. export function updateFavicon(count) { const oldIcon = document.getElementById('shortcut-icon'); @@ -208,7 +210,7 @@ export function cancelablePromise(promise) { // Returns true if specified mime type represents a Tinode video call. export function isVideoCall(mime) { - return mime == 'application/tinode-video-call'; + return mime == CALL_MESSAGE_MIME_TYPE; } // Clips string to the specified length. diff --git a/src/views/incoming-call-view.jsx b/src/views/incoming-call-view.jsx index 277052e3..e7b07054 100644 --- a/src/views/incoming-call-view.jsx +++ b/src/views/incoming-call-view.jsx @@ -1,21 +1,20 @@ // IncomingCallView: displays Accept & Reject buttons for incoming calls. import React from 'react'; -import { FormattedMessage, injectIntl } from 'react-intl'; - -import Tinode from 'tinode-sdk'; +import { FormattedMessage } from 'react-intl'; import AvatarUpload from '../widgets/avatar-upload.jsx'; import BadgeList from '../widgets/badge-list.jsx'; import MenuCancel from '../widgets/menu-cancel.jsx'; -import { CALL_STATE_INCOMING_RECEIVED, MAX_TITLE_LENGTH } from '../config.js'; +import { MAX_TITLE_LENGTH } from '../config.js'; +import { CALL_STATE_INCOMING_RECEIVED } from '../constants.js'; import { makeImageUrl } from '../lib/blob-helpers.js'; import { clipStr } from '../lib/utils.js' -const RING_SOUND = new Audio("audio/phone_ring.mp3"); +const RING_SOUND = new Audio('audio/phone_ring.mp3'); -class IncomingCallView extends React.Component { +export default class IncomingCallView extends React.Component { constructor(props) { super(props); @@ -119,12 +118,8 @@ class IncomingCallView extends React.Component { } render() { - const {formatMessage} = this.props.intl; - let panelTitle = 'Unknown'; - if (this.props.callState == CALL_STATE_INCOMING_RECEIVED) { - panelTitle = ; - } + let panelTitle = ; return (
@@ -170,5 +165,3 @@ class IncomingCallView extends React.Component { ); } }; - -export default injectIntl(IncomingCallView); diff --git a/src/views/messages-view.jsx b/src/views/messages-view.jsx index 22a12aa2..cf7dd4cc 100644 --- a/src/views/messages-view.jsx +++ b/src/views/messages-view.jsx @@ -21,10 +21,12 @@ import SendMessage from '../widgets/send-message.jsx'; import { DEFAULT_P2P_ACCESS_MODE, IMAGE_PREVIEW_DIM, KEYPRESS_DELAY, MESSAGES_PAGE, MAX_EXTERN_ATTACHMENT_SIZE, MAX_IMAGE_DIM, MAX_INBAND_ATTACHMENT_SIZE, - READ_DELAY, QUOTED_REPLY_LENGTH, CALL_STATE_OUTGOING_INITATED, CALL_STATE_IN_PROGRESS } from '../config.js'; + READ_DELAY, QUOTED_REPLY_LENGTH } from '../config.js'; +import { CALL_STATE_OUTGOING_INITATED, CALL_STATE_IN_PROGRESS } from '../constants.js'; import { blobToBase64, fileToBase64, imageScaled, makeImageUrl } from '../lib/blob-helpers.js'; import HashNavigation from '../lib/navigation.js'; import { bytesToHumanSize, shortDateFormat } from '../lib/strformat.js'; +import { isVideoCall } from '../lib/utils.js'; // Run timer with this frequency (ms) for checking notification queue. const NOTIFICATION_EXEC_INTERVAL = 300; @@ -177,7 +179,6 @@ class MessagesView extends React.Component { if (prevState.topic && !Tinode.isNewGroupTopicName(prevState.topic)) { this.leave(prevState.topic); if (prevState.rtcPanel) { - console.log('hangup: ', prevState.topic); this.handleCallHangup(prevState.topic, prevProps.callSeq); } } @@ -1146,13 +1147,21 @@ class MessagesView extends React.Component { ); prevDate = thisDate; } + let duration; + if (msg.head && isVideoCall(msg.head.mime)) { + topic.messageVersions(msg, (cur, before, unused1, unused2) => { + if (cur.content == 'finished' && before.content == 'accepted') { + duration = msg.ts - before.ts; + } + }); + } messageNodes.push( { + .then((ctrl) => { if (topic.isArchived()) { topic.archive(false); } @@ -1698,11 +1699,10 @@ class TinodeWeb extends React.Component { handleCallInvite(callTopic, callSeq, callState) { switch (callState) { case CALL_STATE_OUTGOING_INITATED: - let head = {mime: 'application/tinode-video-call'}; + let head = {mime: CALL_MESSAGE_MIME_TYPE }; this.handleSendMessage('started', undefined, undefined, head) .then((ctrl) => { - if (ctrl.code < 200 || ctrl.code >= 300 || - typeof ctrl.params == 'undefined' || typeof ctrl.params['seq'] == 'undefined') { + if (ctrl.code < 200 || ctrl.code >= 300 || !ctrl.params || !ctrl.params.seq) { this.handleCallClose(); return; } @@ -1715,7 +1715,7 @@ class TinodeWeb extends React.Component { return; } // We've accepted the call. Let the other side know. - topic.call('accept', callSeq).catch((err) => { + topic.videoCall('accept', callSeq).catch((err) => { this.handleCallClose(); this.handleError(err.message, 'err'); }); @@ -1729,7 +1729,7 @@ class TinodeWeb extends React.Component { return; } - topic.call('ringing', callSeq); + topic.videoCall('ringing', callSeq); } handleCallHangup(callTopic, callSeq) { @@ -1738,7 +1738,7 @@ class TinodeWeb extends React.Component { return; } - topic.call('hang-up', callSeq); + topic.videoCall('hang-up', callSeq); } handleCallSendOffer(callTopic, callSeq, sdp) { @@ -1747,7 +1747,7 @@ class TinodeWeb extends React.Component { return; } - topic.call('offer', callSeq, sdp); + topic.videoCall('offer', callSeq, sdp); } handleCallIceCandidate(callTopic, callSeq, candidate) { @@ -1756,7 +1756,7 @@ class TinodeWeb extends React.Component { return; } - topic.call('ice-candidate', callSeq, candidate); + topic.videoCall('ice-candidate', callSeq, candidate); } handleCallSendAnswer(callTopic, callSeq, sdp) { const topic = this.tinode.getTopic(callTopic); @@ -1764,7 +1764,7 @@ class TinodeWeb extends React.Component { return; } - topic.call('answer', callSeq, sdp); + topic.videoCall('answer', callSeq, sdp); } handleCallClose() { @@ -1802,7 +1802,6 @@ class TinodeWeb extends React.Component { } switch (info.event) { case 'invite': - console.log('invite: ', info); if (info.from != this.state.myUserId) { // Incoming call. this.setState({ diff --git a/src/widgets/call-panel.jsx b/src/widgets/call-panel.jsx index d6a79549..610309cc 100644 --- a/src/widgets/call-panel.jsx +++ b/src/widgets/call-panel.jsx @@ -4,8 +4,8 @@ import { FormattedMessage } from 'react-intl'; import LetterTile from '../widgets/letter-tile.jsx'; -import { CALL_STATE_OUTGOING_INITATED, CALL_STATE_IN_PROGRESS, CALL_WEBRTC_CONFIG, - MAX_TITLE_LENGTH } from '../config.js'; +import { CALL_WEBRTC_CONFIG, MAX_TITLE_LENGTH } from '../config.js'; +import { CALL_STATE_OUTGOING_INITATED, CALL_STATE_IN_PROGRESS } from '../constants.js'; import { clipStr } from '../lib/utils.js' @@ -64,19 +64,16 @@ export default class CallPanel extends React.PureComponent { const pc = this.createPeerConnection(); const stream = this.state.localStream; stream.getTracks().forEach(track => { - console.log('local stream adding track ', track.id, track.kind, track.readyState); pc.addTrack(track, stream); }); } onInfo(info) { - console.log('info --> ', info); if (info.what != 'call') { return; } switch (info.event) { case 'accept': - // Call accepted. this.handleVideoCallAccepted(info); break; case 'offer': @@ -106,7 +103,6 @@ export default class CallPanel extends React.PureComponent { componentWillUnmount() { this.props.topic.onInfo = this.previousOnInfo; - console.log('will unmount. stop'); this.stop(); } @@ -143,16 +139,12 @@ export default class CallPanel extends React.PureComponent { pc.ontrack = this.handleTrackEvent; this.setState({pc: pc}); - console.log('Created RTCPeerConnnection'); return pc; } handleVideoAnswerMsg(info) { - console.log("*** Call recipient has accepted our call"); - // Configure the remote description, which is the SDP payload - // in our "video-answer" message. - + // in 'info' message. var desc = new RTCSessionDescription(info.payload); this.state.pc.setRemoteDescription(desc).catch(this.reportError); } @@ -186,18 +178,16 @@ export default class CallPanel extends React.PureComponent { handleICEConnectionStateChangeEvent(event) { switch (this.state.pc.iceConnectionState) { - case "closed": - case "failed": + case 'closed': + case 'failed': this.handleCloseClick(); break; } } handleSignalingStateChangeEvent(event) { - switch (this.state.pc.signalingState) { - case "closed": - this.handleCloseClick(); - break; + if (this.state.pc.signalingState == 'closed') { + this.handleCloseClick(); } } @@ -212,18 +202,17 @@ export default class CallPanel extends React.PureComponent { } handleGetUserMediaError(e) { - console.log('-->', e); switch(e.name) { - case "NotFoundError": + case 'NotFoundError': console.log("Unable to open your call because no camera and/or microphone" + "were found."); break; - case "SecurityError": - case "PermissionDeniedError": + case 'SecurityError': + case 'PermissionDeniedError': // Do nothing; this is the same as the user canceling the call. break; default: - console.log("Error opening your camera and/or microphone: " + e.message); + console.log('Error opening your camera and/or microphone: ' + e.message); break; } @@ -240,7 +229,6 @@ export default class CallPanel extends React.PureComponent { var desc = new RTCSessionDescription(info.payload); pc.setRemoteDescription(desc).then(() => { - console.log('!!! handleVideoOffer/setRemoteDescription -> getUserMesia'); return navigator.mediaDevices.getUserMedia(this.localStreamConstraints); }) .then(stream => { @@ -249,7 +237,6 @@ export default class CallPanel extends React.PureComponent { this.setState({localStream: stream}); localStream.getTracks().forEach(track => { - console.log('local stream A adding track ', track.id, track.kind, track.readyState); pc.addTrack(track, localStream); }); }) @@ -279,7 +266,6 @@ export default class CallPanel extends React.PureComponent { tracks.forEach((track) => { track.stop(); track.enabled = false; - console.log('stopped track ', track.id, track.kind, track.readyState); }); } el.srcObject = null; @@ -316,7 +302,6 @@ export default class CallPanel extends React.PureComponent { if (track.kind != kind) { return; } - console.log('local stream: toggling track ', track.id, track.kind, track.readyState); track.enabled = !track.enabled; }); // Make sure we redraw the UI properly. diff --git a/src/widgets/chat-message.jsx b/src/widgets/chat-message.jsx index 276a07ea..0b66eff3 100644 --- a/src/widgets/chat-message.jsx +++ b/src/widgets/chat-message.jsx @@ -149,7 +149,7 @@ class BaseChatMessage extends React.PureComponent { } else if (isVideoCall(this.props.mimeType)) { // Video calls. - const direction = this.props.response ? "call_received" : "call_made"; + const direction = this.props.response ? 'call_received' : 'call_made'; const text = this.props.response ? : @@ -157,7 +157,7 @@ class BaseChatMessage extends React.PureComponent { defaultMessage="Outgoing Call" description="Outgoing call label" />; const isCallDropped = this.props.content == "disconnected"; content = <>{direction}{text} - { !isCallDropped ? + { !isCallDropped && this.props.duration ? [' (', , ')'] : null}