Compare commits
30
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e720a0d5b | ||
|
|
ae15a043b1 | ||
|
|
3af0b4409c | ||
|
|
feb2e4b3e3 | ||
|
|
276d8c631a | ||
|
|
ed7e246f61 | ||
|
|
0dea77b5f2 | ||
|
|
e287755f13 | ||
|
|
eef880c448 | ||
|
|
fc94d7454f | ||
|
|
9dc1f6f3a7 | ||
|
|
3c87341280 | ||
|
|
df460c6b61 | ||
|
|
fec6a5bfa6 | ||
|
|
43e3395350 | ||
|
|
535d276665 | ||
|
|
b37ce5e8cf | ||
|
|
147dfe0ea3 | ||
|
|
91eacd23ef | ||
|
|
97f3699e97 | ||
|
|
50c8c61078 | ||
|
|
ff605369be | ||
|
|
3d5b2bf7b3 | ||
|
|
92aa11be39 | ||
|
|
4e7c3419c0 | ||
|
|
feae1c9f46 | ||
|
|
293d71dea6 | ||
|
|
7d2c7a634c | ||
|
|
d78dc0e22a | ||
|
|
203612b421 |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+219
-71
@@ -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;
|
||||
}
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+3
-3
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
File diff suppressed because one or more lines are too long
+1
-1
@@ -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";
|
||||
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
File diff suppressed because one or more lines are too long
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
+78
-9
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
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user