Compare commits

..
30 Commits
Author SHA1 Message Date
or-else 4e720a0d5b provide UI feedback for remote hangup 2022-06-03 16:50:06 -07:00
or-else ae15a043b1 adding "end of call" sound 2022-06-02 18:46:33 -07:00
or-else 3af0b4409c bug fixes 2022-06-02 09:27:54 -07:00
or-else feb2e4b3e3 icon positioning fix 2022-06-01 11:33:51 -07:00
or-else 276d8c631a use server-provided ICE servers 2022-05-31 13:50:16 -07:00
or-else ed7e246f61 rebuilt with the sdk fix 2022-05-31 08:52:25 -07:00
or-else 0dea77b5f2 bump version to -beta1 2022-05-30 20:00:18 -07:00
or-else e287755f13 more idimatic VC formatting, better presentation of preview 2022-05-30 19:51:58 -07:00
or-else eef880c448 consistent naming or properties 2022-05-30 15:19:52 -07:00
aforge fc94d7454f Merge pull request #97 from tinode/calls-head
Move video call message state and duration to head.
2022-05-29 23:54:49 +03:00
aforge 9dc1f6f3a7 Remove separate mime for video call messages. 2022-05-29 23:44:31 +03:00
aforge 3c87341280 Move video call message state and duration to head.
'webrtc' header for state.
'webrtc-duration' header for duration.

Content is now Drafty.videoCall() / VC.
2022-05-29 15:29:13 +03:00
or-else df460c6b61 a couple more UI tweaks 2022-05-28 18:50:11 -07:00
or-else fec6a5bfa6 call status in message preview was isolated in a class and made prettier 2022-05-28 18:36:54 -07:00
or-else 43e3395350 i18n of video calls 2022-05-28 16:54:43 -07:00
or-else 535d276665 call in progress animation, crash if button is clicked too soon 2022-05-28 14:42:06 -07:00
or-else b37ce5e8cf consistent capitalisation of menu items 2022-05-27 17:07:58 -07:00
or-else 147dfe0ea3 adding missing file 2022-05-27 17:05:52 -07:00
or-else 91eacd23ef nicer message bubbles for calls 2022-05-27 16:59:23 -07:00
or-else 97f3699e97 a small glitch 2022-05-27 15:17:00 -07:00
or-else 50c8c61078 made incoming calls panel prettier 2022-05-27 15:09:53 -07:00
or-else ff605369be correct method name 2022-05-27 11:16:37 -07:00
or-else 3d5b2bf7b3 add rining to outgoing call, convert souds to much smaller m4a 2022-05-27 11:13:07 -07:00
or-else 92aa11be39 outgoing call panel looks good 2022-05-27 10:40:38 -07:00
or-else 4e7c3419c0 make self-video mirrored 2022-05-26 21:47:17 -07:00
or-else feae1c9f46 package all sounds 2022-05-26 20:50:34 -07:00
or-else 293d71dea6 much smaller ring file, UI tweaks 2022-05-26 20:28:40 -07:00
or-else 7d2c7a634c a bit more work on sizing video 2022-05-26 19:44:58 -07:00
or-else d78dc0e22a making call UI prettier 2022-05-26 18:36:11 -07:00
or-else 203612b421 rebuilt with the sdk fix 2022-05-26 16:42:20 -07:00
42 changed files with 1733 additions and 1488 deletions
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
+219 -71
View File
@@ -154,6 +154,22 @@ button.outline:focus, button.outline:hover, button.outline:active {
background-color: #e8eaf6; /* indigo 50 */
}
button.danger {
background-color: #ec6560;
color: white;
}
button.danger:focus, button.danger:hover, button.danger:active {
background-color: #f77;
}
button.positive {
background-color: #71c46f;
color: white;
}
button.positive:focus, button.positive:hover, button.positive:active {
background-color: rgb(127, 237, 127);
}
textarea, input {
font-family: inherit;
font-size: 1.1rem;
@@ -1562,6 +1578,195 @@ img.channel {
font-size: 6rem;
}
/* Video-call related styles */
#video-container {
position: absolute;
display: flex;
flex-direction: column;
justify-content: space-between;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255,255,255,0.90);
}
.controls {
display: flex;
align-items: center;
justify-content: center;
height: 3.2rem;
z-index: 1;
background-color: #eceff1;
box-shadow: 0px -2px 3px -1px rgba(0,0,0,0.1); /* Faint shadow above panel */
}
#video-container-panel {
position: relative;
height: 100%;
width: 100%;
}
.call-party {
background-color: #eee;
border-radius: 0.5rem;
border: 2px solid #ccc;
overflow: hidden;
}
.call-party:hover {
border: 2px solid #2196f3;
}
.call-party.peer {
position: absolute;
top: 1rem;
left: 1rem;
right: 1rem;
bottom: 1rem;
z-index: 1;
}
.call-party.self {
position: absolute;
right: 0.5rem;
bottom: 0.5rem;
width: 10rem;
height: 10rem;
z-index: 2;
}
video {
width: 100%;
height: 100%;
object-fit: contain;
}
.call-party.self video {
transform: scaleX(-1);
}
.caller-card {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.caller-card .avatar-box {
width: 10rem;
height: 10rem;
font-size: 6rem;
}
.caller-card.pulse img.avatar,
.caller-card.pulse .lettertile {
animation: active-call 1.5s linear infinite;;
}
.caller-card .avatar-box .material-icons {
font-size: 6rem;
line-height: 1.6;
}
/* Badge with "You" for current user or peer's name. */
.call-party .caller-name {
padding: 0.15rem 0.5rem;
border-radius: 0.35rem;
z-index: 1;
text-shadow: 0 0 1px white;
background-color: rgba(255, 255, 255, 0.7);
}
.call-party.self .caller-name.inactive {
position: absolute;
bottom: 0.25rem;
right: 0.25rem;
}
.caller-card .caller-name {
font-size: larger;
margin-top: 0.5rem;
}
.caller-card .trusted-badge {
display: inline-block;
}
.call-party.peer .caller-name.inactive {
position: absolute;
font-size: larger;
left: 50%;
bottom: 0.5rem;
transform: translateX(-50%);
margin: 0 auto;
}
.incoming-call {
position: relative;
background-color: white;
box-shadow: 0.15rem 0.15rem 1.5rem rgb(0 0 0 / 45%);
border-radius: 0.3rem;
margin: auto 2rem auto 2rem;
width: fit-content;
min-width: 16rem;
max-width: 24rem;
}
.incoming-call .caller-card {
position: relative;
top: initial;
left: initial;
width: 100%;
height: 100%;
padding: 5rem 3.5rem 2rem 3.5rem;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.incoming-call .caller-name {
padding: 0.15rem 0.5rem;
border-radius: 0.35rem;
z-index: 1;
text-shadow: 0 0 1px white;
background-color: rgba(255, 255, 255, 0.7);
}
.call-message {
display: flex;
flex-direction: row;
}
.call-message .duration {
font-weight: lighter;
}
@keyframes active-call {
0% {
box-shadow:
0 0 0 0 rgba(33, 150, 243, 0.3),
0 0 0 0.1rem rgba(33, 150, 243, 0.3),
0 0 0 0.3rem rgba(33, 150, 243, 0.3),
0 0 0 0.5rem rgba(33, 150, 243, 0.3);
}
100% {
box-shadow:
0 0 0 0 rgba(33, 150, 243, 0.3),
0 0 0 0.4rem rgba(33, 150, 243, 0.3),
0 0 0 2rem rgba(33, 150, 243, 0),
0 0 0 3rem rgba(33, 150, 243, 0);
}
}
/* Talk bubbles */
.chat-box {
list-style: none;
@@ -2254,12 +2459,26 @@ ul.tabbar li.active > a {
.material-icons.small {
font-size: 1rem;
}
.material-icons.medium {
font-size: 1.2rem;
}
.material-icons.big {
font-size: 2.4rem;
}
.material-icons.huge {
font-size: 4rem;
}
.composed-material {
display: inline-block;
position: relative;
}
.composed-material i:nth-child(2) {
position: absolute;
font-size: 85%;
top: 7%;
left: 39%;
font-weight: bold;
}
#self-avatar .material-icons {
font-size: 2.4rem;
@@ -2737,74 +2956,3 @@ ul.tabbar li.active > a {
opacity: 0.3;
}
}
#video-container {
display: flex;
flex-direction: column;
justify-content: center;
height: 100%!important;
width: 85%;
margin: auto;
height: 80%;
}
video {
width:100%;
}
.video-container-panel {
-ms-flex-align: center!important;
align-items: center!important;
margin-top: 10%;
position:relative;
}
.video-elem {
width:50%;
float:left;
position:relative;
z-index:0;
}
.video-title {
position:absolute;
bottom: 0;
right: 0;
z-index: 1;
background-color: white;
}
#video-container-controls {
display: flex;
justify-content: center;
padding: 20px;
}
.video-call-accept {
border-radius: 10px;
background-color: green;
}
.video-call-hangup {
border-radius: 10px;
background-color: red;
}
.video-call-toggle-media {
border-radius: 10px;
background-color: gray;
}
.incoming-call-title {
text-align: center;
padding: 10px;
}
.incoming-call-badges {
text-align: center;
}
.incoming-call-actions {
text-align: center;
padding: 20px;
}
+1 -1
View File
File diff suppressed because one or more lines are too long
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "tinode-webapp",
"description": "Tinode messenger for the web",
"version": "0.20.0-alpha1",
"version": "0.20.0-beta1",
"repository": {
"type": "git",
"url": "https://github.com/tinode/example-react-js.git"
@@ -26,7 +26,7 @@
"umd/index.dev.js.map",
"umd/tinode.prod.js",
"umd/tinode.dev.js",
"audio/msg.mp3",
"audio/*.m4a",
"css/base.css",
"css/base.min.css",
"img/*.png",
@@ -71,7 +71,7 @@
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-intl": "^5.20.2",
"tinode-sdk": "^0.20.0-alpha1",
"tinode-sdk": "^0.20.0-beta1",
"webrtc-adapter": "^8.1.1"
},
"devDependencies": {
+2 -17
View File
@@ -89,6 +89,8 @@ export const MAX_ONLINE_IN_TOPIC = 4;
export const MAX_TITLE_LENGTH = 60;
// Maximum length of topic description.
export const MAX_TOPIC_DESCRIPTION_LENGTH = 360;
// Maximum length of user name when displaying a video call.
export const MAX_PEER_TITLE_LENGTH = 20;
// Length of message previews in chat list, in characters.
export const MESSAGE_PREVIEW_LENGTH = 80;
@@ -112,20 +114,3 @@ 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 calls/WebRTC configuration.
export const CALL_WEBRTC_CONFIG = {
iceServers: [
{ urls: [ "stun:bn-turn1.xirsys.com" ] },
{
username: "0kYXFmQL9xojOrUy4VFemlTnNPVFZpp7jfPjpB3AjxahuRe4QWrCs6Ll1vDc7TTjAAAAAGAG2whXZWJUdXRzUGx1cw==",
credential: "285ff060-5a58-11eb-b269-0242ac140004",
urls: [
"turn:bn-turn1.xirsys.com:80?transport=udp",
"turn:bn-turn1.xirsys.com:3478?transport=udp",
"turn:bn-turn1.xirsys.com:80?transport=tcp",
"turn:bn-turn1.xirsys.com:3478?transport=tcp",
"turns:bn-turn1.xirsys.com:443?transport=tcp",
"turns:bn-turn1.xirsys.com:5349?transport=tcp"
]
}
]};
+2 -2
View File
@@ -4,5 +4,5 @@ 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';
// Video call drafty headers representing call state.
export const CALL_HEAD_STARTED = 'started';
+32 -25
View File
@@ -1368,46 +1368,53 @@
"missing": false,
"obsolete": false
},
"preview_unavailable": {
"translation": "nicht verfügbar",
"defaultMessage": "unavailable",
"description": "Message shown when media preview is not available",
"missing": false,
"obsolete": true
},
"calls_incoming": {
"translation": "",
"defaultMessage": "Incoming Call",
"translation": "Eingehender Anruf",
"defaultMessage": "Incoming call",
"description": "Incoming call label",
"missing": true,
"obsolete": false
},
"calls_incoming_title": {
"translation": "",
"defaultMessage": "Incoming Call",
"description": "Incoming call title (Incoming call view)",
"missing": true,
"missing": false,
"obsolete": false
},
"calls_outgoing": {
"translation": "",
"defaultMessage": "Outgoing Call",
"translation": "Ausgehender Anruf",
"defaultMessage": "Outgoing call",
"description": "Outgoing call label",
"missing": true,
"missing": false,
"obsolete": false
},
"calls_you_label": {
"translation": "",
"translation": "Du",
"defaultMessage": "You",
"description": "Shown over the local video screen",
"missing": true,
"missing": false,
"obsolete": false
},
"menu_item_video_call": {
"translation": "",
"defaultMessage": "Video Call",
"translation": "Videoanruf",
"defaultMessage": "Video call",
"description": "Start video call",
"missing": true,
"missing": false,
"obsolete": false
},
"already_in_call": {
"translation": "Du redest schon!",
"defaultMessage": "You already in an ongoing call!",
"description": "Error message when the user tried to accept a new call without finishing pervious one",
"missing": false,
"obsolete": false
},
"call_cancelled": {
"translation": "abgebrochen",
"defaultMessage": "cancelled",
"description": "Label for cancelled outgoing call",
"missing": false,
"obsolete": false
},
"call_missed": {
"translation": "verpasster",
"defaultMessage": "missed",
"description": "Label for cancelled incoming call",
"missing": false,
"obsolete": false
}
}
+27 -20
View File
@@ -1368,30 +1368,16 @@
"missing": false,
"obsolete": false
},
"preview_unavailable": {
"translation": "unavailable",
"defaultMessage": "unavailable",
"description": "Message shown when media preview is not available",
"missing": false,
"obsolete": true
},
"calls_incoming": {
"translation": "Incoming Call",
"defaultMessage": "Incoming Call",
"translation": "Incoming call",
"defaultMessage": "Incoming call",
"description": "Incoming call label",
"missing": false,
"obsolete": false
},
"calls_incoming_title": {
"translation": "Incoming Call",
"defaultMessage": "Incoming Call",
"description": "Incoming call title (Incoming call view)",
"missing": false,
"obsolete": false
},
"calls_outgoing": {
"translation": "Outgoing Call",
"defaultMessage": "Outgoing Call",
"translation": "Outgoing call",
"defaultMessage": "Outgoing call",
"description": "Outgoing call label",
"missing": false,
"obsolete": false
@@ -1404,10 +1390,31 @@
"obsolete": false
},
"menu_item_video_call": {
"translation": "Video Call",
"defaultMessage": "Video Call",
"translation": "Video call",
"defaultMessage": "Video call",
"description": "Start video call",
"missing": false,
"obsolete": false
},
"already_in_call": {
"translation": "You already in an ongoing call!",
"defaultMessage": "You already in an ongoing call!",
"description": "Error message when the user tried to accept a new call without finishing pervious one",
"missing": false,
"obsolete": false
},
"call_cancelled": {
"translation": "cancelled",
"defaultMessage": "cancelled",
"description": "Label for cancelled outgoing call",
"missing": false,
"obsolete": false
},
"call_missed": {
"translation": "missed",
"defaultMessage": "missed",
"description": "Label for cancelled incoming call",
"missing": false,
"obsolete": false
}
}
+32 -25
View File
@@ -1368,46 +1368,53 @@
"missing": false,
"obsolete": false
},
"preview_unavailable": {
"translation": "indisponible",
"defaultMessage": "unavailable",
"description": "Message shown when media preview is not available",
"missing": false,
"obsolete": true
},
"calls_incoming": {
"translation": "",
"defaultMessage": "Incoming Call",
"translation": "Llamada entrante",
"defaultMessage": "Incoming call",
"description": "Incoming call label",
"missing": true,
"obsolete": false
},
"calls_incoming_title": {
"translation": "",
"defaultMessage": "Incoming Call",
"description": "Incoming call title (Incoming call view)",
"missing": true,
"missing": false,
"obsolete": false
},
"calls_outgoing": {
"translation": "",
"defaultMessage": "Outgoing Call",
"translation": "Llamada saliente",
"defaultMessage": "Outgoing call",
"description": "Outgoing call label",
"missing": true,
"missing": false,
"obsolete": false
},
"calls_you_label": {
"translation": "",
"translation": "Tú",
"defaultMessage": "You",
"description": "Shown over the local video screen",
"missing": true,
"missing": false,
"obsolete": false
},
"menu_item_video_call": {
"translation": "",
"defaultMessage": "Video Call",
"translation": "Videollamada",
"defaultMessage": "Video call",
"description": "Start video call",
"missing": true,
"missing": false,
"obsolete": false
},
"already_in_call": {
"translation": "¡Ya estás hablando!",
"defaultMessage": "You already in an ongoing call!",
"description": "Error message when the user tried to accept a new call without finishing pervious one",
"missing": false,
"obsolete": false
},
"call_cancelled": {
"translation": "cancelada",
"defaultMessage": "cancelled",
"description": "Label for cancelled outgoing call",
"missing": false,
"obsolete": false
},
"call_missed": {
"translation": "perdida",
"defaultMessage": "missed",
"description": "Label for cancelled incoming call",
"missing": false,
"obsolete": false
}
}
+15 -7
View File
@@ -23,6 +23,10 @@
"defaultMessage": "Report Conversation",
"description": "Flat button [Report Group]"
},
"already_in_call": {
"defaultMessage": "You already in an ongoing call!",
"description": "Error message when the user tried to accept a new call without finishing pervious one"
},
"archived_contacts": {
"defaultMessage": "Archived contacts ({count})",
"description": "Label for archived chats"
@@ -119,16 +123,20 @@
"defaultMessage": "Update",
"description": "Button [Update]"
},
"call_cancelled": {
"defaultMessage": "cancelled",
"description": "Label for cancelled outgoing call"
},
"call_missed": {
"defaultMessage": "missed",
"description": "Label for cancelled incoming call"
},
"calls_incoming": {
"defaultMessage": "Incoming Call",
"defaultMessage": "Incoming call",
"description": "Incoming call label"
},
"calls_incoming_title": {
"defaultMessage": "Incoming Call",
"description": "Incoming call title (Incoming call view)"
},
"calls_outgoing": {
"defaultMessage": "Outgoing Call",
"defaultMessage": "Outgoing call",
"description": "Outgoing call label"
},
"calls_you_label": {
@@ -497,7 +505,7 @@
"description": "Turn notifications on"
},
"menu_item_video_call": {
"defaultMessage": "Video Call",
"defaultMessage": "Video call",
"description": "Start video call"
},
"message_sending": {
+32 -25
View File
@@ -1368,46 +1368,53 @@
"missing": false,
"obsolete": false
},
"preview_unavailable": {
"translation": "없는",
"defaultMessage": "unavailable",
"description": "Message shown when media preview is not available",
"missing": false,
"obsolete": true
},
"calls_incoming": {
"translation": "",
"defaultMessage": "Incoming Call",
"translation": "수신 전화",
"defaultMessage": "Incoming call",
"description": "Incoming call label",
"missing": true,
"obsolete": false
},
"calls_incoming_title": {
"translation": "",
"defaultMessage": "Incoming Call",
"description": "Incoming call title (Incoming call view)",
"missing": true,
"missing": false,
"obsolete": false
},
"calls_outgoing": {
"translation": "",
"defaultMessage": "Outgoing Call",
"translation": "발신 전화",
"defaultMessage": "Outgoing call",
"description": "Outgoing call label",
"missing": true,
"missing": false,
"obsolete": false
},
"calls_you_label": {
"translation": "",
"translation": "너",
"defaultMessage": "You",
"description": "Shown over the local video screen",
"missing": true,
"missing": false,
"obsolete": false
},
"menu_item_video_call": {
"translation": "",
"defaultMessage": "Video Call",
"translation": "영상 통화",
"defaultMessage": "Video call",
"description": "Start video call",
"missing": true,
"missing": false,
"obsolete": false
},
"already_in_call": {
"translation": "당신은 이미 말하고 있습니다!",
"defaultMessage": "You already in an ongoing call!",
"description": "Error message when the user tried to accept a new call without finishing pervious one",
"missing": false,
"obsolete": false
},
"call_cancelled": {
"translation": "취소 된",
"defaultMessage": "cancelled",
"description": "Label for cancelled outgoing call",
"missing": false,
"obsolete": false
},
"call_missed": {
"translation": "부재중",
"defaultMessage": "missed",
"description": "Label for cancelled incoming call",
"missing": false,
"obsolete": false
}
}
+32 -25
View File
@@ -1368,46 +1368,53 @@
"missing": false,
"obsolete": false
},
"preview_unavailable": {
"translation": "indisponibil",
"defaultMessage": "unavailable",
"description": "Message shown when media preview is not available",
"missing": false,
"obsolete": true
},
"calls_incoming": {
"translation": "",
"defaultMessage": "Incoming Call",
"translation": "Apel primit",
"defaultMessage": "Incoming call",
"description": "Incoming call label",
"missing": true,
"obsolete": false
},
"calls_incoming_title": {
"translation": "",
"defaultMessage": "Incoming Call",
"description": "Incoming call title (Incoming call view)",
"missing": true,
"missing": false,
"obsolete": false
},
"calls_outgoing": {
"translation": "",
"defaultMessage": "Outgoing Call",
"translation": "Apel efectuat",
"defaultMessage": "Outgoing call",
"description": "Outgoing call label",
"missing": true,
"missing": false,
"obsolete": false
},
"calls_you_label": {
"translation": "",
"translation": "Tu",
"defaultMessage": "You",
"description": "Shown over the local video screen",
"missing": true,
"missing": false,
"obsolete": false
},
"menu_item_video_call": {
"translation": "",
"defaultMessage": "Video Call",
"translation": "Apel video",
"defaultMessage": "Video call",
"description": "Start video call",
"missing": true,
"missing": false,
"obsolete": false
},
"already_in_call": {
"translation": "Deja vorbesti!",
"defaultMessage": "You already in an ongoing call!",
"description": "Error message when the user tried to accept a new call without finishing pervious one",
"missing": false,
"obsolete": false
},
"call_cancelled": {
"translation": "anulat",
"defaultMessage": "cancelled",
"description": "Label for cancelled outgoing call",
"missing": false,
"obsolete": false
},
"call_missed": {
"translation": "pierdut",
"defaultMessage": "missed",
"description": "Label for cancelled incoming call",
"missing": false,
"obsolete": false
}
}
+32 -25
View File
@@ -1368,46 +1368,53 @@
"missing": false,
"obsolete": false
},
"preview_unavailable": {
"translation": "недоступно",
"defaultMessage": "unavailable",
"description": "Message shown when media preview is not available",
"missing": false,
"obsolete": true
},
"calls_incoming": {
"translation": "",
"defaultMessage": "Incoming Call",
"translation": "Входящий звонок",
"defaultMessage": "Incoming call",
"description": "Incoming call label",
"missing": true,
"obsolete": false
},
"calls_incoming_title": {
"translation": "",
"defaultMessage": "Incoming Call",
"description": "Incoming call title (Incoming call view)",
"missing": true,
"missing": false,
"obsolete": false
},
"calls_outgoing": {
"translation": "",
"defaultMessage": "Outgoing Call",
"translation": "Исходящий звонок",
"defaultMessage": "Outgoing call",
"description": "Outgoing call label",
"missing": true,
"missing": false,
"obsolete": false
},
"calls_you_label": {
"translation": "",
"translation": "Вы",
"defaultMessage": "You",
"description": "Shown over the local video screen",
"missing": true,
"missing": false,
"obsolete": false
},
"menu_item_video_call": {
"translation": "",
"defaultMessage": "Video Call",
"translation": "Видеозвонок",
"defaultMessage": "Video call",
"description": "Start video call",
"missing": true,
"missing": false,
"obsolete": false
},
"already_in_call": {
"translation": "Вы уже звоните кому-то!",
"defaultMessage": "You already in an ongoing call!",
"description": "Error message when the user tried to accept a new call without finishing pervious one",
"missing": false,
"obsolete": false
},
"call_cancelled": {
"translation": "отменен",
"defaultMessage": "cancelled",
"description": "Label for cancelled outgoing call",
"missing": false,
"obsolete": false
},
"call_missed": {
"translation": "пропущен",
"defaultMessage": "missed",
"description": "Label for cancelled incoming call",
"missing": false,
"obsolete": false
}
}
+32 -25
View File
@@ -1368,46 +1368,53 @@
"missing": false,
"obsolete": false
},
"preview_unavailable": {
"translation": "不可用",
"defaultMessage": "unavailable",
"description": "Message shown when media preview is not available",
"missing": false,
"obsolete": true
},
"calls_incoming": {
"translation": "",
"defaultMessage": "Incoming Call",
"translation": "來電",
"defaultMessage": "Incoming call",
"description": "Incoming call label",
"missing": true,
"obsolete": false
},
"calls_incoming_title": {
"translation": "",
"defaultMessage": "Incoming Call",
"description": "Incoming call title (Incoming call view)",
"missing": true,
"missing": false,
"obsolete": false
},
"calls_outgoing": {
"translation": "",
"defaultMessage": "Outgoing Call",
"translation": "撥出電話",
"defaultMessage": "Outgoing call",
"description": "Outgoing call label",
"missing": true,
"missing": false,
"obsolete": false
},
"calls_you_label": {
"translation": "",
"translation": "你",
"defaultMessage": "You",
"description": "Shown over the local video screen",
"missing": true,
"missing": false,
"obsolete": false
},
"menu_item_video_call": {
"translation": "",
"defaultMessage": "Video Call",
"translation": "視頻電話",
"defaultMessage": "Video call",
"description": "Start video call",
"missing": true,
"missing": false,
"obsolete": false
},
"already_in_call": {
"translation": "你已經在說話了!",
"defaultMessage": "You already in an ongoing call!",
"description": "Error message when the user tried to accept a new call without finishing pervious one",
"missing": false,
"obsolete": false
},
"call_cancelled": {
"translation": "取消",
"defaultMessage": "cancelled",
"description": "Label for cancelled outgoing call",
"missing": false,
"obsolete": false
},
"call_missed": {
"translation": "未接",
"defaultMessage": "missed",
"description": "Label for cancelled incoming call",
"missing": false,
"obsolete": false
}
}
+32 -25
View File
@@ -1368,46 +1368,53 @@
"missing": false,
"obsolete": false
},
"preview_unavailable": {
"translation": "不可用",
"defaultMessage": "unavailable",
"description": "Message shown when media preview is not available",
"missing": false,
"obsolete": true
},
"calls_incoming": {
"translation": "",
"defaultMessage": "Incoming Call",
"translation": "来电",
"defaultMessage": "Incoming call",
"description": "Incoming call label",
"missing": true,
"obsolete": false
},
"calls_incoming_title": {
"translation": "",
"defaultMessage": "Incoming Call",
"description": "Incoming call title (Incoming call view)",
"missing": true,
"missing": false,
"obsolete": false
},
"calls_outgoing": {
"translation": "",
"defaultMessage": "Outgoing Call",
"translation": "拨出电话",
"defaultMessage": "Outgoing call",
"description": "Outgoing call label",
"missing": true,
"missing": false,
"obsolete": false
},
"calls_you_label": {
"translation": "",
"translation": "你",
"defaultMessage": "You",
"description": "Shown over the local video screen",
"missing": true,
"missing": false,
"obsolete": false
},
"menu_item_video_call": {
"translation": "",
"defaultMessage": "Video Call",
"translation": "视频电话",
"defaultMessage": "Video call",
"description": "Start video call",
"missing": true,
"missing": false,
"obsolete": false
},
"already_in_call": {
"translation": "你已经在说话了!",
"defaultMessage": "You already in an ongoing call!",
"description": "Error message when the user tried to accept a new call without finishing pervious one",
"missing": false,
"obsolete": false
},
"call_cancelled": {
"translation": "取消",
"defaultMessage": "cancelled",
"description": "Label for cancelled outgoing call",
"missing": false,
"obsolete": false
},
"call_missed": {
"translation": "未接",
"defaultMessage": "missed",
"description": "Label for cancelled incoming call",
"missing": false,
"obsolete": false
}
}
+21
View File
@@ -4,6 +4,8 @@ import { defineMessages } from 'react-intl';
import { Drafty } from 'tinode-sdk';
import AudioPlayer from '../widgets/audio-player.jsx'
import CallMessage from '../widgets/call-message.jsx'
import CallStatus from '../widgets/call-status.jsx';
import LazyImage from '../widgets/lazy-image.jsx'
import UploadingImage from '../widgets/uploading-image.jsx'
@@ -163,6 +165,16 @@ export function fullFormatter(style, data, values, key, stack) {
attr.className = 'reply-quote'
attr.onClick = this.onQuoteClick;
break;
case 'VC':
el = CallMessage;
// Video call messages do not have content.
values = null;
if (data) {
attr.callState = data.state;
attr.incoming = data.incoming;
attr.duration = data.duration;
}
break;
default:
if (!el) {
// Unknown element.
@@ -245,6 +257,15 @@ export function previewFormatter(style, data, values, key) {
el = React.Fragment;
values = [<i key="ex" className="material-icons">attachment</i>, ' ', this.formatMessage(messages.drafty_attachment)];
break;
case 'VC':
el = CallStatus;
if (data) {
attr.callState = data.state;
attr.incoming = data.incoming;
attr.duration = data.duration;
}
values = null;
break;
case 'QQ':
case 'HD':
el = null;
-7
View File
@@ -2,8 +2,6 @@
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,11 +206,6 @@ export function cancelablePromise(promise) {
};
};
// Returns true if specified mime type represents a Tinode video call.
export function isVideoCall(mime) {
return mime == CALL_MESSAGE_MIME_TYPE;
}
// Clips string to the specified length.
export function clipStr(str, length) {
return str && str.substring(0, length);
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,3 +1,3 @@
// This is a generated file. Don't edit.
export const PACKAGE_VERSION = "0.20.0-alpha1";
export const PACKAGE_VERSION = "0.20.0-beta1";
+8 -21
View File
@@ -26,7 +26,6 @@ import { CALL_STATE_OUTGOING_INITATED, CALL_STATE_IN_PROGRESS } from '../constan
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;
@@ -766,7 +765,7 @@ class MessagesView extends React.Component {
// - if file is small enough, just send it in-band.
sendFileAttachment(file) {
// Server-provided limit reduced for base64 encoding and overhead.
const maxInbandAttachmentSize = (this.props.tinode.getServerLimit('maxMessageSize',
const maxInbandAttachmentSize = (this.props.tinode.getServerParam('maxMessageSize',
MAX_INBAND_ATTACHMENT_SIZE) * 0.75 - 1024) | 0;
if (file.size > maxInbandAttachmentSize) {
@@ -795,7 +794,7 @@ class MessagesView extends React.Component {
// handleAttachFile method is called when [Attach file] button is clicked: launch attachment preview.
handleAttachFile(file) {
const maxExternAttachmentSize = this.props.tinode.getServerLimit('maxFileUploadSize', MAX_EXTERN_ATTACHMENT_SIZE);
const maxExternAttachmentSize = this.props.tinode.getServerParam('maxFileUploadSize', MAX_EXTERN_ATTACHMENT_SIZE);
if (file.size > maxExternAttachmentSize) {
// Too large.
@@ -829,7 +828,7 @@ class MessagesView extends React.Component {
const fname = this.state.imagePreview.name;
// Server-provided limit reduced for base64 encoding and overhead.
const maxInbandAttachmentSize = (this.props.tinode.getServerLimit('maxMessageSize',
const maxInbandAttachmentSize = (this.props.tinode.getServerParam('maxMessageSize',
MAX_INBAND_ATTACHMENT_SIZE) * 0.75 - 1024) | 0;
if (blob.size > maxInbandAttachmentSize) {
@@ -888,7 +887,7 @@ class MessagesView extends React.Component {
// handleAttachImage method is called when [Attach image] button is clicked: launch image preview.
handleAttachImage(file) {
const maxExternAttachmentSize = this.props.tinode.getServerLimit('maxFileUploadSize', MAX_EXTERN_ATTACHMENT_SIZE);
const maxExternAttachmentSize = this.props.tinode.getServerParam('maxFileUploadSize', MAX_EXTERN_ATTACHMENT_SIZE);
// Get image dimensions and size, optionally scale it down.
imageScaled(file, MAX_IMAGE_DIM, MAX_IMAGE_DIM, maxExternAttachmentSize, false)
@@ -913,7 +912,7 @@ class MessagesView extends React.Component {
.then(result => result.blob())
.then(blob => {
// Server-provided limit reduced for base64 encoding and overhead.
const maxInbandAttachmentSize = this.props.tinode.getServerLimit('maxMessageSize', MAX_INBAND_ATTACHMENT_SIZE) * 0.75 - 1024;
const maxInbandAttachmentSize = this.props.tinode.getServerParam('maxMessageSize', MAX_INBAND_ATTACHMENT_SIZE) * 0.75 - 1024;
if (blob.size > maxInbandAttachmentSize) {
// Too large to send inband - uploading out of band and sending as a link.
const uploader = this.props.tinode.getLargeFileHelper();
@@ -1046,13 +1045,11 @@ class MessagesView extends React.Component {
onSendMessage={this.sendFileAttachment} />
);
} else if (this.state.rtcPanel) {
const topic = this.props.tinode.getTopic(this.state.topic);
component2 = (
<CallPanel
topic={topic}
topic={this.state.topic}
seq={this.props.callSeq}
callState={this.props.callState}
displayMobile={this.props.displayMobile}
tinode={this.props.tinode}
title={this.state.title}
avatar={this.state.avatar || true}
@@ -1062,8 +1059,7 @@ class MessagesView extends React.Component {
onInvite={this.props.onCallInvite}
onSendOffer={this.props.onCallSendOffer}
onIceCandidate={this.props.onCallIceCandidate}
onSendAnswer={this.props.onCallSendAnswer}
/>
onSendAnswer={this.props.onCallSendAnswer} />
);
} else {
const topic = this.props.tinode.getTopic(this.state.topic);
@@ -1140,21 +1136,12 @@ class MessagesView extends React.Component {
);
prevDate = thisDate;
}
let duration;
if (msg.head && isVideoCall(msg.head.mime)) {
topic.messageVersions(msg, (cur, before) => {
if (cur.content == 'finished' && before.content == 'accepted') {
duration = msg.ts - before.ts;
}
});
}
messageNodes.push(
<ChatMessage
tinode={this.props.tinode}
content={msg.content}
mimeType={msg.head ? msg.head.mime : null}
mimeType={msg.head && msg.head.mime}
timestamp={msg.ts}
duration={duration}
response={isReply}
seq={msg.seq}
isGroup={groupTopic}
+20 -21
View File
@@ -11,9 +11,9 @@ import { Drafty, Tinode } from 'tinode-sdk';
import Alert from '../widgets/alert.jsx';
import ContextMenu from '../widgets/context-menu.jsx';
import ForwardDialog from '../widgets/forward-dialog.jsx';
import CallIncoming from '../widgets/call-incoming.jsx';
import InfoView from './info-view.jsx';
import IncomingCallView from './incoming-call-view.jsx';
import MessagesView from './messages-view.jsx';
import SidepanelView from './sidepanel-view.jsx';
@@ -21,7 +21,7 @@ import { API_KEY, APP_NAME, DEFAULT_P2P_ACCESS_MODE, FORWARDED_PREVIEW_LENGTH, L
MEDIA_BREAKPOINT, RECEIVED_DELAY } from '../config.js';
import { CALL_STATE_NONE, CALL_STATE_OUTGOING_INITATED,
CALL_STATE_INCOMING_RECEIVED, CALL_STATE_IN_PROGRESS,
CALL_MESSAGE_MIME_TYPE } from '../constants.js';
CALL_HEAD_STARTED } from '../constants.js';
import { PACKAGE_VERSION } from '../version.js';
import { base64ReEncode, makeImageUrl } from '../lib/blob-helpers.js';
import { detectServerAddress, isLocalHost, isSecureConnection } from '../lib/host-name.js';
@@ -31,7 +31,7 @@ import { secondsToTime } from '../lib/strformat.js'
import { updateFavicon } from '../lib/utils.js';
// Sound to play on message received.
const POP_SOUND = new Audio('audio/msg.mp3');
const POP_SOUND = new Audio('audio/msg.m4a');
const messages = defineMessages({
reconnect_countdown: {
@@ -71,7 +71,7 @@ const messages = defineMessages({
},
menu_item_video_call: {
id: 'menu_item_video_call',
defaultMessage: 'Video Call',
defaultMessage: 'Video call',
description: 'Start video call'
}
});
@@ -1687,8 +1687,8 @@ class TinodeWeb extends React.Component {
handleCallInvite(callTopic, callSeq, callState) {
switch (callState) {
case CALL_STATE_OUTGOING_INITATED:
let head = {mime: CALL_MESSAGE_MIME_TYPE };
this.handleSendMessage('started', undefined, undefined, head)
let head = { webrtc: CALL_HEAD_STARTED };
this.handleSendMessage(Drafty.videoCall(), undefined, undefined, head)
.then((ctrl) => {
if (ctrl.code < 200 || ctrl.code >= 300 || !ctrl.params || !ctrl.params.seq) {
this.handleCallClose();
@@ -1857,6 +1857,20 @@ class TinodeWeb extends React.Component {
:
null
}
{this.state.callTopic && this.state.callState == CALL_STATE_INCOMING_RECEIVED ?
<CallIncoming
tinode={this.tinode}
onClose={this.handleCallClose}
topic={this.state.callTopic}
seq={this.state.callSeq}
callState={this.state.callState}
onRinging={this.handleCallRinging}
onAcceptCall={this.handleCallAcceptCall}
onReject={this.handleCallHangup}
/>
:
null
}
<Alert
visible={this.state.alertVisible}
title={this.state.alertParams.title}
@@ -2019,21 +2033,6 @@ class TinodeWeb extends React.Component {
:
null
}
{this.state.callTopic && this.state.callState == CALL_STATE_INCOMING_RECEIVED ?
<IncomingCallView
tinode={this.tinode}
onClose={this.handleCallClose}
topic={this.state.callTopic}
seq={this.state.callSeq}
callState={this.state.callState}
onRinging={this.handleCallRinging}
onAcceptCall={this.handleCallAcceptCall}
onReject={this.handleCallHangup}
/>
:
null
}
</div>
);
}
+1 -1
View File
@@ -30,7 +30,7 @@ class BadgeList extends React.PureComponent {
if (this.props.trustedBadges && this.props.trustedBadges.length > 0) {
badges = [];
this.props.trustedBadges.map((b) => {
const comment = formatMessage(messages['badge_' + b]);
const comment = this.props.short ? null : formatMessage(messages['badge_' + b]);
const style = 'material-icons ' + b + '-color';
badges.push(<div className="trusted-badge" key={b}><i className={style}>{icon_mapping[b] || b}</i> {comment}</div>);
});
@@ -1,20 +1,18 @@
// IncomingCallView: displays Accept & Reject buttons for incoming calls.
// IncomingCall: displays Accept & Reject buttons for incoming calls.
import React from 'react';
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 BadgeList from './badge-list.jsx';
import LetterTile from './letter-tile.jsx';
import { MAX_TITLE_LENGTH } from '../config.js';
import { MAX_TITLE_LENGTH, MAX_PEER_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/call-in.m4a');
export default class IncomingCallView extends React.Component {
export default class CallIncoming extends React.Component {
constructor(props) {
super(props);
@@ -38,6 +36,7 @@ export default class IncomingCallView extends React.Component {
if (!topic) {
return;
}
this.resetDesc(topic, this.props);
if (this.props.callState == CALL_STATE_INCOMING_RECEIVED) {
RING_SOUND.play();
@@ -118,47 +117,33 @@ export default class IncomingCallView extends React.Component {
}
render() {
let panelTitle = <FormattedMessage id="calls_incoming_title"
defaultMessage="Incoming Call" description="Incoming call title (Incoming call view)" />;
return (
<div id="info-view">
<div className="caption-panel" id="info-caption-panel">
<div className="panel-title" id="info-title">{panelTitle}</div>
<div>
<MenuCancel onCancel={this.handleRejectCall} />
</div>
</div>
<div id="info-view-content" className="scrollable-panel">
<div className="panel-form-column">
<center>
<AvatarUpload
<div className="alert-container">
<div className="incoming-call">
<div className="caller-card incoming pulse">
<div className="avatar-box">
<LetterTile
tinode={this.props.tinode}
avatar={this.state.avatar}
readOnly={true}
uid={this.props.topic}
avatar={this.state.avatar || true}
topic={this.props.topic}
title={this.state.fullName} />
</center>
<div className="group incoming-call-title">
<div className="large ellipsized">{clipStr(this.state.fullName)}</div>
</div>
<div className="group incoming-call-badges">
<BadgeList trustedBadges={this.state.trustedBadges} />
</div>
<div id="actions" className="group incoming-call-actions">
{this.props.callState == CALL_STATE_INCOMING_RECEIVED ?
<>
<button className="video-call-hangup" onClick={this.handleRejectCall}>
<i className="material-icons">call_end</i>
</button>
<button className="video-call-accept" onClick={this.handleAcceptCall}>
<i className="material-icons">call</i>
</button>
</>
:
null
}
</div>
<div className="caller-name">{clipStr(this.state.fullName, MAX_PEER_TITLE_LENGTH)}
<BadgeList short={true} trustedBadges={this.state.trustedBadges} /></div>
</div>
<div className="controls">
{this.props.callState == CALL_STATE_INCOMING_RECEIVED ?
<>
<button className="danger" onClick={this.handleRejectCall}>
<i className="material-icons">call_end</i>
</button>
<button className="positive" onClick={this.handleAcceptCall}>
<i className="material-icons">call</i>
</button>
</>
:
null
}
</div>
</div>
</div>
+32
View File
@@ -0,0 +1,32 @@
// Chat message describing a video or voice call.
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { secondsToTime } from '../lib/strformat.js';
export default class CallMessage extends React.PureComponent {
render() {
const isCallDropped = this.props.callState == 'disconnected';
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 duration = isCallDropped ? (
this.props.incoming ?
<FormattedMessage id="call_missed" defaultMessage="missed" description="Label for cancelled incoming call" /> :
<FormattedMessage id="call_cancelled" defaultMessage="cancelled" description="Label for cancelled outgoing call" />
) :
<span>{secondsToTime(this.props.duration / 1000)}</span>;
return <div className="call-message">
<div><i className="material-icons big gray">call</i></div>
<div className="flex-column narrow">
<div>{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" />
}</div>
<div className="duration"><i className={successClass}>{callIcon}</i> {duration}</div>
</div>
</div>;
}
}
+171 -135
View File
@@ -1,15 +1,28 @@
// CallPanel: displays local and remote viewports and controls.
// CallPanel displays call in progress: local and remote viewports and controls.
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import LetterTile from '../widgets/letter-tile.jsx';
import LetterTile from './letter-tile.jsx';
import { CALL_WEBRTC_CONFIG, MAX_TITLE_LENGTH } from '../config.js';
import { MAX_PEER_TITLE_LENGTH } from '../config.js';
import { CALL_STATE_OUTGOING_INITATED, CALL_STATE_IN_PROGRESS } from '../constants.js';
import { clipStr } from '../lib/utils.js'
export default class CallPanel extends React.PureComponent {
const RING_SOUND = new Audio('audio/call-out.m4a');
RING_SOUND.loop = true;
const CALL_ENDED_SOUND = new Audio('audio/call-end.m4a');
CALL_ENDED_SOUND.loop = true;
const messages = defineMessages({
already_in_call: {
id: 'already_in_call',
defaultMessage: 'You already in an ongoing call!',
description: 'Error message when the user tried to accept a new call without finishing pervious one',
}
});
class CallPanel extends React.PureComponent {
constructor(props) {
super(props);
@@ -17,7 +30,8 @@ export default class CallPanel extends React.PureComponent {
localStream: undefined,
pc: undefined,
previousOnInfo: undefined
previousOnInfo: undefined,
waitingForPeer: false
};
this.localStreamConstraints = {
@@ -30,6 +44,7 @@ export default class CallPanel extends React.PureComponent {
this.onInfo = this.onInfo.bind(this);
this.start = this.start.bind(this);
this.stop = this.stop.bind(this);
this.createPeerConnection = this.createPeerConnection.bind(this);
@@ -48,7 +63,6 @@ export default class CallPanel extends React.PureComponent {
this.reportError = this.reportError.bind(this);
this.handleGetUserMediaError = this.handleGetUserMediaError.bind(this);
this.stop = this.stop.bind(this);
this.stopTracks = this.stopTracks.bind(this);
this.handleCloseClick = this.handleCloseClick.bind(this);
@@ -60,7 +74,23 @@ export default class CallPanel extends React.PureComponent {
this.handleVideoCallAccepted = this.handleVideoCallAccepted.bind(this);
}
handleVideoCallAccepted(info) {
componentDidMount() {
const topic = this.props.tinode.getTopic(this.props.topic);
this.previousOnInfo = topic.onInfo;
topic.onInfo = this.onInfo;
if ((this.props.callState == CALL_STATE_OUTGOING_INITATED ||
this.props.callState == CALL_STATE_IN_PROGRESS) && this.localRef.current) {
this.start();
}
}
componentWillUnmount() {
const topic = this.props.tinode.getTopic(this.props.topic);
topic.onInfo = this.previousOnInfo;
this.stop();
}
handleVideoCallAccepted(info) {
const pc = this.createPeerConnection();
const stream = this.state.localStream;
stream.getTracks().forEach(track => {
@@ -91,45 +121,79 @@ export default class CallPanel extends React.PureComponent {
}
}
componentDidMount() {
const topic = this.props.topic;
this.previousOnInfo = topic.onInfo;
topic.onInfo = this.onInfo;
if ((this.props.callState == CALL_STATE_OUTGOING_INITATED ||
this.props.callState == CALL_STATE_IN_PROGRESS) && this.localRef.current) {
this.start();
}
}
componentWillUnmount() {
this.props.topic.onInfo = this.previousOnInfo;
this.stop();
}
start() {
if (this.state.localStream) {
console.log("You can't start a call because you already have one open!");
} else {
if (this.props.callState == CALL_STATE_IN_PROGRESS) {
// We apparently just accepted the call.
this.props.onInvite(this.props.topic.name, this.props.seq, this.props.callState);
return;
}
// This is an outgoing call waiting for the other side to pick up.
// Start local video.
navigator.mediaDevices.getUserMedia(this.localStreamConstraints)
.then(stream => {
this.setState({localStream: stream});
this.localRef.current.srcObject = stream;
// Send call invitation.
this.props.onInvite(this.props.topic.name, this.props.seq, this.props.callState);
})
.catch(this.handleGetUserMediaError);
this.props.onError(this.props.intl.formatMessage(messages.already_in_call));
return;
}
if (this.props.callState == CALL_STATE_IN_PROGRESS) {
// We apparently just accepted the call.
this.props.onInvite(this.props.topic, this.props.seq, this.props.callState);
return;
}
// This is an outgoing call waiting for the other side to pick up.
// Start local video.
navigator.mediaDevices.getUserMedia(this.localStreamConstraints)
.then(stream => {
this.setState({localStream: stream, waitingForPeer: true});
this.localRef.current.srcObject = stream;
RING_SOUND.play();
// Send call invitation.
this.props.onInvite(this.props.topic, this.props.seq, this.props.callState);
})
.catch(this.handleGetUserMediaError);
}
stop() {
CALL_ENDED_SOUND.pause();
CALL_ENDED_SOUND.currentTime = 0;
RING_SOUND.pause();
RING_SOUND.currentTime = 0;
this.stopTracks(this.localRef.current);
this.stopTracks(this.remoteRef.current);
if (this.state.pc) {
this.state.pc.ontrack = null;
this.state.pc.onremovetrack = null;
this.state.pc.onremovestream = null;
this.state.pc.onicecandidate = null;
this.state.pc.oniceconnectionstatechange = null;
this.state.pc.onsignalingstatechange = null;
this.state.pc.onicegatheringstatechange = null;
this.state.pc.onnegotiationneeded = null;
this.state.pc.close();
}
this.setState({pc: null, waitingForPeer: false});
}
stopTracks(el) {
if (!el) {
return;
}
let stream = el.srcObject;
if (!stream) {
return;
}
let tracks = stream.getTracks();
if (tracks) {
tracks.forEach(track => {
track.stop();
track.enabled = false;
});
}
el.srcObject = null;
el.src = '';
}
createPeerConnection() {
var pc = new RTCPeerConnection(CALL_WEBRTC_CONFIG);
const iceServers = this.props.tinode.getServerParam('iceServers', null);
const pc = iceServers ? new RTCPeerConnection({iceServers: iceServers}) : new RTCPeerConnection();
pc.onicecandidate = this.handleICECandidateEvent;
pc.oniceconnectionstatechange = this.handleICEConnectionStateChangeEvent;
@@ -138,14 +202,14 @@ export default class CallPanel extends React.PureComponent {
pc.onnegotiationneeded = this.handleNegotiationNeededEvent;
pc.ontrack = this.handleTrackEvent;
this.setState({pc: pc});
this.setState({pc: pc, waitingForPeer: false});
return pc;
}
handleVideoAnswerMsg(info) {
// Configure the remote description, which is the SDP payload
// in 'info' message.
var desc = new RTCSessionDescription(info.payload);
const desc = new RTCSessionDescription(info.payload);
this.state.pc.setRemoteDescription(desc).catch(this.reportError);
}
@@ -158,20 +222,19 @@ export default class CallPanel extends React.PureComponent {
return this.state.pc.setLocalDescription(offer);
})
.then(_ => {
this.props.onSendOffer(this.props.topic.name, this.props.seq, this.state.pc.localDescription.toJSON());
this.props.onSendOffer(this.props.topic, this.props.seq, this.state.pc.localDescription.toJSON());
})
.catch(this.reportError);
}
handleICECandidateEvent(event) {
if (event.candidate) {
this.props.onIceCandidate(this.props.topic.name, this.props.seq, event.candidate.toJSON());
this.props.onIceCandidate(this.props.topic, this.props.seq, event.candidate.toJSON());
}
}
handleNewICECandidateMsg(info) {
var candidate = new RTCIceCandidate(info.payload);
const candidate = new RTCIceCandidate(info.payload);
this.state.pc.addIceCandidate(candidate)
.catch(this.reportError);
}
@@ -205,15 +268,16 @@ export default class CallPanel extends React.PureComponent {
handleGetUserMediaError(e) {
switch(e.name) {
case 'NotFoundError':
// Cannot start the call b/c no camera and/or microphone found.
this.reportError(e);
// Cannot start the call b/c no camera and/or microphone found.
this.reportError(e.message);
break;
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);
this.reportError(e.message);
console.error('Error opening your camera and/or microphone: ' + e.message);
break;
}
@@ -223,13 +287,11 @@ export default class CallPanel extends React.PureComponent {
}
handleVideoOfferMsg(info) {
var localStream = null;
let localStream = null;
const pc = this.createPeerConnection();
const desc = new RTCSessionDescription(info.payload);
var desc = new RTCSessionDescription(info.payload);
pc.setRemoteDescription(desc).then(() => {
pc.setRemoteDescription(desc).then(_ => {
return navigator.mediaDevices.getUserMedia(this.localStreamConstraints);
})
.then(stream => {
@@ -241,59 +303,41 @@ export default class CallPanel extends React.PureComponent {
pc.addTrack(track, localStream);
});
})
.then(() => {
.then(_ => {
return pc.createAnswer();
})
.then(answer => {
return pc.setLocalDescription(answer);
})
.then(() => {
this.props.onSendAnswer(this.props.topic.name, this.props.seq, pc.localDescription.toJSON());
.then(_ => {
this.props.onSendAnswer(this.props.topic, this.props.seq, pc.localDescription.toJSON());
})
.catch(this.handleGetUserMediaError);
}
// Call disconnected by remote.
handleRemoteHangup() {
this.handleCloseClick();
}
stopTracks(el) {
if (el == null) { return; }
let stream = el.srcObject;
if (stream == null) { return; }
let tracks = stream.getTracks();
if (tracks != null) {
tracks.forEach((track) => {
track.stop();
track.enabled = false;
});
if (!this.state.waitingForPeer) {
// This is live call, just hang up.
this.handleCloseClick();
} else {
// This is a call which is not yet connected.
// Stop pulse animation.
this.setState({waitingForPeer: false});
// Change sound and wait a bit before ending it.
RING_SOUND.pause();
RING_SOUND.currentTime = 0;
CALL_ENDED_SOUND.loop = true;
CALL_ENDED_SOUND.play();
setTimeout(_ => {
this.handleCloseClick();
}, 2000);
}
el.srcObject = null;
el.src = '';
}
stop() {
this.stopTracks(this.localRef.current);
this.stopTracks(this.remoteRef.current);
if (this.state.pc != null) {
this.state.pc.ontrack = null;
this.state.pc.onremovetrack = null;
this.state.pc.onremovestream = null;
this.state.pc.onicecandidate = null;
this.state.pc.oniceconnectionstatechange = null;
this.state.pc.onsignalingstatechange = null;
this.state.pc.onicegatheringstatechange = null;
this.state.pc.onnegotiationneeded = null;
this.state.pc.close();
}
this.setState({pc: null});
}
handleCloseClick() {
this.stop();
this.props.onHangup(this.props.topic.name, this.props.seq);
this.props.onHangup(this.props.topic, this.props.seq);
}
// Mute/unmute audio/video (specified by kind).
@@ -318,64 +362,56 @@ export default class CallPanel extends React.PureComponent {
}
render() {
if (!this.props.topic) {
return null;
}
const remoteActive = this.remoteRef.current != null && this.remoteRef.current.srcObject != null;
const audioIcon = this.state.localStream != null && this.state.localStream.getAudioTracks()[0].enabled ? 'mic' : 'mic_off';
const videoIcon = this.state.localStream != null && this.state.localStream.getVideoTracks()[0].enabled ? 'videocam' : 'videocam_off';
const remoteActive = this.remoteRef.current && this.remoteRef.current.srcObject;
const disabled = !(this.state.localStream && this.state.localStream.getTracks());
const audioIcon = this.state.localStream && this.state.localStream.getAudioTracks()[0].enabled ? 'mic' : 'mic_off';
const videoIcon = this.state.localStream && this.state.localStream.getVideoTracks()[0].enabled ? 'videocam' : 'videocam_off';
const peerTitle = clipStr(this.props.title, MAX_PEER_TITLE_LENGTH);
const pulseAnimation = this.state.waitingForPeer ? ' pulse' : '';
return (
<>
<div id="topic-caption-panel" className="caption-panel">
{this.props.displayMobile ?
<a href="#" id="hide-message-view" onClick={(e) => {e.preventDefault(); this.props.onHideMessagesView();}}>
<i className="material-icons">arrow_back</i>
</a>
:
null}
<div className="avatar-box">
<LetterTile
tinode={this.props.tinode}
avatar={this.props.avatar}
topic={this.props.topic}
title={this.props.title} />
</div>
<div id="topic-title-group">
<div id="topic-title" className="panel-title">{
this.props.title ||
<i><FormattedMessage id="unnamed_topic" defaultMessage="Unnamed"
description="Title shown when the topic has no name" /></i>
}</div>
</div>
</div>
<div id="video-container">
<div className="video-container-panel">
<div className="video-elem">
<video id="localVideo" ref={this.localRef} autoPlay muted playsInline></video>
<div className="video-title">
<div id="video-container-panel">
<div className="call-party self">
<video ref={this.localRef} autoPlay muted playsInline></video>
<div className="caller-name inactive">
<FormattedMessage id="calls_you_label"
defaultMessage="You" description="Shown over the local video screen" />
</div>
</div>
<div className="video-elem">
<video id="remoteVideo" ref={this.remoteRef} autoPlay playsInline></video>
<div className="call-party peer">
<video ref={this.remoteRef} autoPlay playsInline></video>
{remoteActive ?
<div className="video-title">{clipStr(this.props.title, MAX_TITLE_LENGTH)}</div>
:
null}
<div className="caller-name inactive">{peerTitle}</div> :
<div className={`caller-card${pulseAnimation}`}>
<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 id="video-container-controls">
<button className="video-call-hangup" onClick={this.handleCloseClick}>
<div className="controls">
<button className="danger" onClick={this.handleCloseClick}>
<i className="material-icons">call_end</i>
</button>
<button className="video-call-toggle-media" onClick={this.handleToggleCameraClick}><i className="material-icons">{videoIcon}</i></button>
<button className="video-call-toggle-media" onClick={this.handleToggleMicClick}><i className="material-icons">{audioIcon}</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(CallPanel);
+30
View File
@@ -0,0 +1,30 @@
// The counter of unread messages in the topic.
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { secondsToTime } from '../lib/strformat.js';
// Props:
// incoming: true | false.
// success: true | false.
export default class CallStatus extends React.PureComponent {
render() {
const isCallDropped = this.props.callState == 'disconnected';
const icon2 = this.props.incoming ?
(isCallDropped ? 'call_missed' : 'call_received') :
(isCallDropped ? 'call_missed_outgoing' : 'call_made');
const duration = isCallDropped ? (
this.props.incoming ?
<FormattedMessage id="call_missed" defaultMessage="missed" description="Label for cancelled incoming call" /> :
<FormattedMessage id="call_cancelled" defaultMessage="cancelled" description="Label for cancelled outgoing call" />
) :
<span>{secondsToTime(this.props.duration / 1000)}</span>;
return (
<>
<div className="composed-material">
<i className="material-icons">call</i><i className="material-icons second">{icon2}</i>
</div> {duration}
</>
);
}
};
+2 -14
View File
@@ -9,8 +9,7 @@ import LetterTile from './letter-tile.jsx';
import ReceivedMarker from './received-marker.jsx'
import { fullFormatter } from '../lib/formatters.js';
import { isVideoCall, sanitizeUrl } from '../lib/utils.js';
import { secondsToTime } from '../lib/strformat.js';
import { sanitizeUrl } from '../lib/utils.js';
class BaseChatMessage extends React.PureComponent {
constructor(props) {
@@ -34,6 +33,7 @@ class BaseChatMessage extends React.PureComponent {
formatMessage: props.intl.formatMessage.bind(props.intl),
viewportWidth: props.viewportWidth,
authorizeURL: props.tinode.authorizeURL.bind(props.tinode),
onImagePreview: this.handleImagePreview,
onFormButtonClick: this.handleFormButtonClick,
onQuoteClick: this.handleQuoteClick
@@ -147,18 +147,6 @@ class BaseChatMessage extends React.PureComponent {
<FormattedMessage id="deleted_content"
defaultMessage="content deleted" description="Shown when messages are deleted" />
</i></>
} else if (isVideoCall(this.props.mimeType)) {
// Video calls.
const direction = this.props.response ? 'call_received' : 'call_made';
const text = this.props.response ?
<FormattedMessage id="calls_incoming"
defaultMessage="Incoming Call" description="Incoming call label" /> :
<FormattedMessage id="calls_outgoing"
defaultMessage="Outgoing Call" description="Outgoing call label" />;
const isCallDropped = this.props.content == 'disconnected';
content = <><i className="material-icons" style={{color: isCallDropped ? 'red' : 'green'}}>{direction}</i>{text}
{ !isCallDropped && this.props.duration ? [' (', secondsToTime(this.props.duration / 1000), ')'] : null}
</>
} else if (typeof content != 'string') {
content = <><i className="material-icons gray">warning_amber</i> <i className="gray">
<FormattedMessage id="invalid_content"
-4
View File
@@ -9,7 +9,6 @@ import Contact from './contact.jsx';
import ContactAction from './contact-action.jsx';
import { makeImageUrl } from '../lib/blob-helpers.js';
import { isVideoCall } from '../lib/utils.js'
import { MESSAGE_PREVIEW_LENGTH } from '../config.js';
@@ -78,7 +77,6 @@ class ContactList extends React.Component {
c.private.join(',') : (c.private ? c.private.comment : null);
let preview;
let forwarded;
let previewIsVideoCall;
let previewIsResponse;
let deliveryStatus;
if (!this.props.showMode && c.latestMessage) {
@@ -86,7 +84,6 @@ class ContactList extends React.Component {
if (msg) {
forwarded = msg.head ? msg.head.forwarded : null;
deliveryStatus = msg._status || c.msgStatus(msg, true);
previewIsVideoCall = msg.head ? isVideoCall(msg.head.mime) : null;
previewIsResponse = msg.from != this.props.myUserId;
if (msg.content) {
preview = typeof msg.content == 'string' ?
@@ -103,7 +100,6 @@ class ContactList extends React.Component {
avatar={makeImageUrl(c.public ? c.public.photo : null)}
comment={comment}
preview={preview}
previewIsVideoCall={previewIsVideoCall}
previewIsResponse={previewIsResponse}
forwarded={forwarded}
received={deliveryStatus}
+2 -5
View File
@@ -70,14 +70,11 @@ class Contact extends React.Component {
// The this.props.preview contains alreay shortened Drafty or string.
let preview;
if (this.props.previewIsVideoCall) {
preview = <><i className="material-icons">{this.props.previewIsResponse ? 'call_received' : 'call_made'}</i>
<i className="material-icons">video_call</i></>;
} else if (typeof this.props.preview == 'string') {
if (typeof this.props.preview == 'string') {
preview = this.props.preview;
} else if (Drafty.isValid(this.props.preview)) {
preview = React.createElement(React.Fragment, null, Drafty.format(this.props.preview, previewFormatter,
{formatMessage: this.props.intl.formatMessage}));
{formatMessage: this.props.intl.formatMessage, previewIsResponse: this.props.previewIsResponse}));
} else if (this.props.preview) {
preview =
<><i className="material-icons gray">warning_amber</i> <i className="gray">
+3 -3
View File
@@ -49,7 +49,7 @@ export default class TagManager extends React.Component {
}
handleAddTag(tag) {
const maxTagCount = this.props.tinode.getServerLimit('maxTagCount', MAX_TAG_COUNT);
const maxTagCount = this.props.tinode.getServerParam('maxTagCount', MAX_TAG_COUNT);
if (tag.length > 0 && this.state.tags.length < maxTagCount) {
const tags = this.state.tags.slice(0);
@@ -87,8 +87,8 @@ export default class TagManager extends React.Component {
}
render() {
const minTagLength = this.props.tinode.getServerLimit('minTagLength', MIN_TAG_LENGTH);
const maxTagLength = this.props.tinode.getServerLimit('maxTagLength', MAX_TAG_LENGTH);
const minTagLength = this.props.tinode.getServerParam('minTagLength', MIN_TAG_LENGTH);
const maxTagLength = this.props.tinode.getServerParam('maxTagLength', MAX_TAG_LENGTH);
let tags = [];
if (this.state.activated) {
+835 -920
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+78 -9
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,3 +1,3 @@
// This is a generated file. Don't edit.
const PACKAGE_VERSION = "0.20.0-alpha1";
const PACKAGE_VERSION = "0.20.0-beta1";