Compare commits
57
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13c1fcf811 | ||
|
|
a7d98136c5 | ||
|
|
2a7c85c61c | ||
|
|
4ff315329c | ||
|
|
2b51f19fbd | ||
|
|
a9677cee8b | ||
|
|
46b6a816eb | ||
|
|
6ffc03e8bd | ||
|
|
ec01ac2a50 | ||
|
|
a9e4f644a2 | ||
|
|
5087a70d5e | ||
|
|
57466f61b3 | ||
|
|
b4a5ad121b | ||
|
|
f9a563248b | ||
|
|
a4d765da14 | ||
|
|
de2f38bbd5 | ||
|
|
b45451893d | ||
|
|
2c4f005d76 | ||
|
|
30d57e11f2 | ||
|
|
d477fd4e82 | ||
|
|
06945a01ea | ||
|
|
cc75d81538 | ||
|
|
f50e79ea1e | ||
|
|
706a7e0aaa | ||
|
|
ed62fe17c5 | ||
|
|
e7290e6c62 | ||
|
|
e938c909a1 | ||
|
|
510a3d7113 | ||
|
|
c36fb2657a | ||
|
|
cc66356ad5 | ||
|
|
c4d4aa77db | ||
|
|
0daef53c87 | ||
|
|
ad98bec73e | ||
|
|
517f457182 | ||
|
|
0e92ace6f4 | ||
|
|
3675812353 | ||
|
|
4b9078e64a | ||
|
|
ca8235e1cc | ||
|
|
56b9bb7814 | ||
|
|
78116ef36f | ||
|
|
8fd49cc55e | ||
|
|
570ef47455 | ||
|
|
b310a94180 | ||
|
|
45ad8bb2ad | ||
|
|
2bc89f67f8 | ||
|
|
34ba874091 | ||
|
|
601c048ea5 | ||
|
|
74b756d817 | ||
|
|
f3def8cf8a | ||
|
|
2e0ed44763 | ||
|
|
ff7f8faf37 | ||
|
|
824f15aa29 | ||
|
|
713cb4ea03 | ||
|
|
f68057fe87 | ||
|
|
acf4f0fa2a | ||
|
|
6cea3e97db | ||
|
|
5370b39fb6 |
@@ -48,7 +48,7 @@ More translations are welcome. See [instructions](https://github.com/tinode/chat
|
||||
* End-to-End encryption.
|
||||
* Emoji support is weak.
|
||||
* Mentions, hashtags.
|
||||
* Previews not generated for videos, audio, links or docs.
|
||||
* Previews not generated for videos, links or docs.
|
||||
|
||||
## Other
|
||||
|
||||
|
||||
Binary file not shown.
+76
-6
@@ -1121,7 +1121,6 @@ img.channel {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: white;
|
||||
padding-left: 0.5px;
|
||||
}
|
||||
|
||||
/* END of avatar box */
|
||||
@@ -2324,7 +2323,7 @@ ul.tabbar li.active > a {
|
||||
white-space: nowrap;
|
||||
font-size: 90%;
|
||||
max-width: 12rem;
|
||||
overflow-x: hidden;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@@ -2671,11 +2670,11 @@ ul.tabbar li.active > a {
|
||||
|
||||
.audio-player .material-icons.large {
|
||||
font-size: 2.4rem;
|
||||
color: #667575;
|
||||
color: #667575CC;
|
||||
}
|
||||
|
||||
.audio-player .material-icons.disabled {
|
||||
color: #999;
|
||||
color: #999C;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
@@ -2692,13 +2691,13 @@ ul.tabbar li.active > a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #757575;
|
||||
color: #757575CC;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
.audio-player .timer {
|
||||
font-size: smaller;
|
||||
color: #666;
|
||||
color: #666C;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -2738,3 +2737,74 @@ 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
Generated
+1029
-997
File diff suppressed because it is too large
Load Diff
+3
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "tinode-webapp",
|
||||
"description": "Tinode messenger for the web",
|
||||
"version": "0.19.0-alpha1",
|
||||
"version": "0.20.0-alpha1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/tinode/example-react-js.git"
|
||||
@@ -71,7 +71,8 @@
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-intl": "^5.20.2",
|
||||
"tinode-sdk": "^0.19.0-alpha1"
|
||||
"tinode-sdk": "^0.20.0-alpha1",
|
||||
"webrtc-adapter": "^8.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.14.5",
|
||||
|
||||
+26
-3
@@ -16,7 +16,7 @@ export const DEFAULT_HOST = KNOWN_HOSTS.hosted;
|
||||
export const LOGGING_ENABLED = true;
|
||||
|
||||
// Minimum time between two keypress notifications, milliseconds.
|
||||
export const KEYPRESS_DELAY = 3*1000;
|
||||
export const KEYPRESS_DELAY = 3_000;
|
||||
// Delay before sending a {note} for reciving a message, milliseconds.
|
||||
export const RECEIVED_DELAY = 500;
|
||||
// Delay before sending a read notification, milliseconds.
|
||||
@@ -64,7 +64,7 @@ export const MESSAGES_PAGE = 24;
|
||||
// for instance 1024 bytes.
|
||||
// Increase this limit to a greater value in production, if desired. Also increase
|
||||
// max_message_size in the server config.
|
||||
export const MAX_INBAND_ATTACHMENT_SIZE = 262144;
|
||||
export const MAX_INBAND_ATTACHMENT_SIZE = 262_144;
|
||||
|
||||
// Default absolute maximum attachment size to be used with the server = 8MB.
|
||||
// Used when the server-provided value is unavailable. Increase to something like 100MB in production.
|
||||
@@ -98,11 +98,34 @@ export const QUOTED_REPLY_LENGTH = 30;
|
||||
// Length of a preview of a forwarded message.
|
||||
export const FORWARDED_PREVIEW_LENGTH = 84;
|
||||
|
||||
// Minimum duration of an audio recording in milliseconds (2 sec).
|
||||
export const MIN_DURATION = 2_000;
|
||||
// Maximum duration of an audio recording in milliseconds (10 min).
|
||||
export const MAX_DURATION = 600_000;
|
||||
|
||||
// Link for "Contact Us".
|
||||
export const LINK_CONTACT_US = 'email:support@tinode.co';
|
||||
export const LINK_CONTACT_US = 'mailto:support@tinode.co';
|
||||
|
||||
// Link to Privacy Policy.
|
||||
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"
|
||||
]
|
||||
}
|
||||
]};
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// Video call states.
|
||||
export const CALL_STATE_NONE = 0;
|
||||
export const CALL_STATE_OUTGOING_INITATED = 1;
|
||||
export const CALL_STATE_INCOMING_RECEIVED = 2;
|
||||
export const CALL_STATE_IN_PROGRESS = 3;
|
||||
|
||||
// Mime type used for video call messages.
|
||||
export const CALL_MESSAGE_MIME_TYPE = 'application/x-tinode-webrtc';
|
||||
@@ -1373,6 +1373,41 @@
|
||||
"defaultMessage": "unavailable",
|
||||
"description": "Message shown when media preview is not available",
|
||||
"missing": false,
|
||||
"obsolete": true
|
||||
},
|
||||
"calls_incoming": {
|
||||
"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,
|
||||
"obsolete": false
|
||||
},
|
||||
"calls_outgoing": {
|
||||
"translation": "",
|
||||
"defaultMessage": "Outgoing Call",
|
||||
"description": "Outgoing call label",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
},
|
||||
"calls_you_label": {
|
||||
"translation": "",
|
||||
"defaultMessage": "You",
|
||||
"description": "Shown over the local video screen",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
},
|
||||
"menu_item_video_call": {
|
||||
"translation": "",
|
||||
"defaultMessage": "Video Call",
|
||||
"description": "Start video call",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1373,6 +1373,41 @@
|
||||
"defaultMessage": "unavailable",
|
||||
"description": "Message shown when media preview is not available",
|
||||
"missing": false,
|
||||
"obsolete": true
|
||||
},
|
||||
"calls_incoming": {
|
||||
"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",
|
||||
"description": "Outgoing call label",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"calls_you_label": {
|
||||
"translation": "You",
|
||||
"defaultMessage": "You",
|
||||
"description": "Shown over the local video screen",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"menu_item_video_call": {
|
||||
"translation": "Video Call",
|
||||
"defaultMessage": "Video Call",
|
||||
"description": "Start video call",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1373,6 +1373,41 @@
|
||||
"defaultMessage": "unavailable",
|
||||
"description": "Message shown when media preview is not available",
|
||||
"missing": false,
|
||||
"obsolete": true
|
||||
},
|
||||
"calls_incoming": {
|
||||
"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,
|
||||
"obsolete": false
|
||||
},
|
||||
"calls_outgoing": {
|
||||
"translation": "",
|
||||
"defaultMessage": "Outgoing Call",
|
||||
"description": "Outgoing call label",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
},
|
||||
"calls_you_label": {
|
||||
"translation": "",
|
||||
"defaultMessage": "You",
|
||||
"description": "Shown over the local video screen",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
},
|
||||
"menu_item_video_call": {
|
||||
"translation": "",
|
||||
"defaultMessage": "Video Call",
|
||||
"description": "Start video call",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +119,22 @@
|
||||
"defaultMessage": "Update",
|
||||
"description": "Button [Update]"
|
||||
},
|
||||
"calls_incoming": {
|
||||
"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",
|
||||
"description": "Outgoing call label"
|
||||
},
|
||||
"calls_you_label": {
|
||||
"defaultMessage": "You",
|
||||
"description": "Shown over the local video screen"
|
||||
},
|
||||
"cannot_initiate_file_upload": {
|
||||
"defaultMessage": "Cannot initiate file upload.",
|
||||
"description": "Generic error messagewhen attachment fails"
|
||||
@@ -480,6 +496,10 @@
|
||||
"defaultMessage": "Unmute",
|
||||
"description": "Turn notifications on"
|
||||
},
|
||||
"menu_item_video_call": {
|
||||
"defaultMessage": "Video Call",
|
||||
"description": "Start video call"
|
||||
},
|
||||
"message_sending": {
|
||||
"defaultMessage": "sending...",
|
||||
"description": "Message being sent, in place of time stamp"
|
||||
@@ -608,10 +628,6 @@
|
||||
"defaultMessage": "phone",
|
||||
"description": "Dative case of 'phone', i.e. 'phone' in 'by phone'"
|
||||
},
|
||||
"preview_unavailable": {
|
||||
"defaultMessage": "unavailable",
|
||||
"description": "Message shown when media preview is not available"
|
||||
},
|
||||
"private_editing_placeholder": {
|
||||
"defaultMessage": "Visible to you only",
|
||||
"description": "Placeholder for editing 'private'"
|
||||
|
||||
@@ -1373,6 +1373,41 @@
|
||||
"defaultMessage": "unavailable",
|
||||
"description": "Message shown when media preview is not available",
|
||||
"missing": false,
|
||||
"obsolete": true
|
||||
},
|
||||
"calls_incoming": {
|
||||
"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,
|
||||
"obsolete": false
|
||||
},
|
||||
"calls_outgoing": {
|
||||
"translation": "",
|
||||
"defaultMessage": "Outgoing Call",
|
||||
"description": "Outgoing call label",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
},
|
||||
"calls_you_label": {
|
||||
"translation": "",
|
||||
"defaultMessage": "You",
|
||||
"description": "Shown over the local video screen",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
},
|
||||
"menu_item_video_call": {
|
||||
"translation": "",
|
||||
"defaultMessage": "Video Call",
|
||||
"description": "Start video call",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1373,6 +1373,41 @@
|
||||
"defaultMessage": "unavailable",
|
||||
"description": "Message shown when media preview is not available",
|
||||
"missing": false,
|
||||
"obsolete": true
|
||||
},
|
||||
"calls_incoming": {
|
||||
"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,
|
||||
"obsolete": false
|
||||
},
|
||||
"calls_outgoing": {
|
||||
"translation": "",
|
||||
"defaultMessage": "Outgoing Call",
|
||||
"description": "Outgoing call label",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
},
|
||||
"calls_you_label": {
|
||||
"translation": "",
|
||||
"defaultMessage": "You",
|
||||
"description": "Shown over the local video screen",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
},
|
||||
"menu_item_video_call": {
|
||||
"translation": "",
|
||||
"defaultMessage": "Video Call",
|
||||
"description": "Start video call",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1373,6 +1373,41 @@
|
||||
"defaultMessage": "unavailable",
|
||||
"description": "Message shown when media preview is not available",
|
||||
"missing": false,
|
||||
"obsolete": true
|
||||
},
|
||||
"calls_incoming": {
|
||||
"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,
|
||||
"obsolete": false
|
||||
},
|
||||
"calls_outgoing": {
|
||||
"translation": "",
|
||||
"defaultMessage": "Outgoing Call",
|
||||
"description": "Outgoing call label",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
},
|
||||
"calls_you_label": {
|
||||
"translation": "",
|
||||
"defaultMessage": "You",
|
||||
"description": "Shown over the local video screen",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
},
|
||||
"menu_item_video_call": {
|
||||
"translation": "",
|
||||
"defaultMessage": "Video Call",
|
||||
"description": "Start video call",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1373,6 +1373,41 @@
|
||||
"defaultMessage": "unavailable",
|
||||
"description": "Message shown when media preview is not available",
|
||||
"missing": false,
|
||||
"obsolete": true
|
||||
},
|
||||
"calls_incoming": {
|
||||
"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,
|
||||
"obsolete": false
|
||||
},
|
||||
"calls_outgoing": {
|
||||
"translation": "",
|
||||
"defaultMessage": "Outgoing Call",
|
||||
"description": "Outgoing call label",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
},
|
||||
"calls_you_label": {
|
||||
"translation": "",
|
||||
"defaultMessage": "You",
|
||||
"description": "Shown over the local video screen",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
},
|
||||
"menu_item_video_call": {
|
||||
"translation": "",
|
||||
"defaultMessage": "Video Call",
|
||||
"description": "Start video call",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1373,6 +1373,41 @@
|
||||
"defaultMessage": "unavailable",
|
||||
"description": "Message shown when media preview is not available",
|
||||
"missing": false,
|
||||
"obsolete": true
|
||||
},
|
||||
"calls_incoming": {
|
||||
"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,
|
||||
"obsolete": false
|
||||
},
|
||||
"calls_outgoing": {
|
||||
"translation": "",
|
||||
"defaultMessage": "Outgoing Call",
|
||||
"description": "Outgoing call label",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
},
|
||||
"calls_you_label": {
|
||||
"translation": "",
|
||||
"defaultMessage": "You",
|
||||
"description": "Shown over the local video screen",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
},
|
||||
"menu_item_video_call": {
|
||||
"translation": "",
|
||||
"defaultMessage": "Video Call",
|
||||
"description": "Start video call",
|
||||
"missing": true,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,7 +199,7 @@ export function blobToBase64(blob) {
|
||||
// File pasted from the clipboard. It's either an inline image or a file attachment.
|
||||
export function filePasted(event, onImageSuccess, onAttachmentSuccess, onError) {
|
||||
const items = (event.clipboardData || event.originalEvent.clipboardData || {}).items;
|
||||
if (!Array.isArray(items)) {
|
||||
if (!items || !items.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,16 +16,21 @@ export function shortDateFormat(then, locale) {
|
||||
return then.toLocaleDateString(locale, {year: 'numeric', month: 'short', day: 'numeric'});
|
||||
}
|
||||
|
||||
// Convert seconds to minutes:seconds, i.e. 156 sec -> 2:36.
|
||||
// Convert seconds to [hours:]minutes:seconds, i.e. 156 sec -> 2:36, 3756 sec -> 1:02:36.
|
||||
// If <code>fixedMins</code> is true, then minutes are represented by at least two digits.
|
||||
export function secondsToTime(seconds, fixedMin) {
|
||||
let min = Math.floor(seconds / 60) | 0;
|
||||
if (fixedMin) {
|
||||
let hours = Math.floor(min / 60) | 0;
|
||||
|
||||
if (fixedMin || hours > 0) {
|
||||
min = min < 10 ? `0${min}` : min;
|
||||
}
|
||||
let sec = (seconds % 60) | 0;
|
||||
sec = sec < 10 ? `0${sec}` : sec;
|
||||
return `${min}:${sec}`;
|
||||
if (hours == 0) {
|
||||
return `${min}:${sec}`;
|
||||
}
|
||||
return `${hours}:${min}:${sec}`;
|
||||
}
|
||||
|
||||
// Convert a number of bytes to human-readable format.
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
import { Tinode } from 'tinode-sdk';
|
||||
|
||||
import { CALL_MESSAGE_MIME_TYPE } from '../constants.js'
|
||||
|
||||
// Make shortcut icon appear with a green dot + show unread count in title.
|
||||
export function updateFavicon(count) {
|
||||
const oldIcon = document.getElementById('shortcut-icon');
|
||||
@@ -205,3 +207,13 @@ 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.19.0-alpha1";
|
||||
export const PACKAGE_VERSION = "0.20.0-alpha1";
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
// IncomingCallView: 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 { MAX_TITLE_LENGTH } from '../config.js';
|
||||
import { CALL_STATE_INCOMING_RECEIVED } from '../constants.js';
|
||||
|
||||
import { makeImageUrl } from '../lib/blob-helpers.js';
|
||||
import { clipStr } from '../lib/utils.js'
|
||||
|
||||
const RING_SOUND = new Audio('audio/phone_ring.mp3');
|
||||
|
||||
export default class IncomingCallView extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
topic: null,
|
||||
fullName: undefined,
|
||||
avatar: null,
|
||||
trustedBadges: [],
|
||||
previousMetaDesc: undefined,
|
||||
};
|
||||
|
||||
this.resetDesc = this.resetDesc.bind(this);
|
||||
this.onMetaDesc = this.onMetaDesc.bind(this);
|
||||
this.handleRejectCall = this.handleRejectCall.bind(this);
|
||||
this.handleAcceptCall = this.handleAcceptCall.bind(this);
|
||||
this.ringTimer = null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const topic = this.props.tinode.getTopic(this.props.topic);
|
||||
if (!topic) {
|
||||
return;
|
||||
}
|
||||
this.resetDesc(topic, this.props);
|
||||
if (this.props.callState == CALL_STATE_INCOMING_RECEIVED) {
|
||||
RING_SOUND.play();
|
||||
this.ringTimer = setInterval(() => RING_SOUND.play(), 2000);
|
||||
this.props.onRinging(this.props.topic, this.props.seq);
|
||||
}
|
||||
}
|
||||
|
||||
// No need to separately handle component mount.
|
||||
componentDidUpdate(props) {
|
||||
const topic = this.props.tinode.getTopic(props.topic);
|
||||
if (!topic) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.onMetaDesc != topic.onMetaDesc) {
|
||||
this.previousMetaDesc = topic.onMetaDesc;
|
||||
topic.onMetaDesc = this.onMetaDesc;
|
||||
}
|
||||
|
||||
if (this.state.topic != props.topic) {
|
||||
this.setState({topic: props.topic});
|
||||
this.resetDesc(topic, props);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.ringTimer != null) {
|
||||
clearInterval(this.ringTimer);
|
||||
RING_SOUND.pause();
|
||||
}
|
||||
const topic = this.props.tinode.getTopic(this.props.topic);
|
||||
if (!topic) {
|
||||
return;
|
||||
}
|
||||
this.setState({topic: null});
|
||||
topic.onMetaDesc = this.previousMetaDesc;
|
||||
}
|
||||
|
||||
resetDesc(topic, props) {
|
||||
const defacs = topic.getDefaultAccess() || {};
|
||||
const acs = topic.getAccessMode();
|
||||
const badges = [];
|
||||
if (topic.trusted) {
|
||||
for (const [key, val] of Object.entries(topic.trusted)) {
|
||||
if (val) {
|
||||
badges.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
fullName: clipStr(topic.public ? topic.public.fn : undefined, MAX_TITLE_LENGTH),
|
||||
avatar: makeImageUrl(topic.public ? topic.public.photo : null),
|
||||
trustedBadges: badges,
|
||||
});
|
||||
}
|
||||
|
||||
onMetaDesc(desc) {
|
||||
const topic = this.props.tinode.getTopic(this.props.topic);
|
||||
if (!topic) {
|
||||
return;
|
||||
}
|
||||
this.resetDesc(topic, this.props);
|
||||
|
||||
if (this.previousMetaDesc && this.previousMetaDesc != this.onMetaDesc) {
|
||||
this.previousMetaDesc(desc);
|
||||
}
|
||||
}
|
||||
|
||||
handleAcceptCall() {
|
||||
this.props.onAcceptCall(this.props.topic);
|
||||
}
|
||||
|
||||
handleRejectCall() {
|
||||
this.props.onReject(this.props.topic, this.props.seq);
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
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
|
||||
tinode={this.props.tinode}
|
||||
avatar={this.state.avatar}
|
||||
readOnly={true}
|
||||
uid={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>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -18,7 +18,7 @@ import TopicSecurity from '../widgets/topic-security.jsx';
|
||||
import { MAX_TITLE_LENGTH, MAX_TOPIC_DESCRIPTION_LENGTH, NO_ACCESS_MODE } from '../config.js';
|
||||
|
||||
import { makeImageUrl } from '../lib/blob-helpers.js';
|
||||
import { theCard } from '../lib/utils.js';
|
||||
import { theCard, clipStr } from '../lib/utils.js';
|
||||
|
||||
const messages = defineMessages({
|
||||
info: {
|
||||
@@ -78,10 +78,6 @@ const messages = defineMessages({
|
||||
},
|
||||
});
|
||||
|
||||
function _clip(str, length) {
|
||||
return str && str.substring(0, length);
|
||||
}
|
||||
|
||||
class InfoView extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@@ -206,11 +202,11 @@ class InfoView extends React.Component {
|
||||
deleter: acs && acs.isDeleter(),
|
||||
muted: acs && acs.isMuted(),
|
||||
|
||||
fullName: _clip(topic.public ? topic.public.fn : undefined, MAX_TITLE_LENGTH),
|
||||
description: _clip(topic.public ? topic.public.note : undefined, MAX_TOPIC_DESCRIPTION_LENGTH),
|
||||
fullName: clipStr(topic.public && topic.public.fn, MAX_TITLE_LENGTH),
|
||||
description: clipStr(topic.public && topic.public.note, MAX_TOPIC_DESCRIPTION_LENGTH),
|
||||
avatar: makeImageUrl(topic.public ? topic.public.photo : null),
|
||||
trustedBadges: badges,
|
||||
private: _clip(topic.private ? topic.private.comment : null, MAX_TITLE_LENGTH),
|
||||
private: clipStr(topic.private && topic.private.comment, MAX_TITLE_LENGTH),
|
||||
archived: topic.isArchived(),
|
||||
address: topic.name,
|
||||
groupTopic: topic.isGroupType(),
|
||||
|
||||
+76
-16
@@ -5,6 +5,7 @@ import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import { Drafty, Tinode } from 'tinode-sdk';
|
||||
|
||||
import CallPanel from '../widgets/call-panel.jsx';
|
||||
import ChatMessage from '../widgets/chat-message.jsx';
|
||||
import ContactBadges from '../widgets/contact-badges.jsx';
|
||||
import DocPreview from '../widgets/doc-preview.jsx';
|
||||
@@ -21,9 +22,11 @@ import SendMessage from '../widgets/send-message.jsx';
|
||||
import { DEFAULT_P2P_ACCESS_MODE, IMAGE_PREVIEW_DIM, KEYPRESS_DELAY,
|
||||
MESSAGES_PAGE, MAX_EXTERN_ATTACHMENT_SIZE, MAX_IMAGE_DIM, MAX_INBAND_ATTACHMENT_SIZE,
|
||||
READ_DELAY, QUOTED_REPLY_LENGTH } from '../config.js';
|
||||
import { CALL_STATE_OUTGOING_INITATED, CALL_STATE_IN_PROGRESS } from '../constants.js';
|
||||
import { blobToBase64, fileToBase64, imageScaled, makeImageUrl } from '../lib/blob-helpers.js';
|
||||
import HashNavigation from '../lib/navigation.js';
|
||||
import { bytesToHumanSize, shortDateFormat } from '../lib/strformat.js';
|
||||
import { isVideoCall } from '../lib/utils.js';
|
||||
|
||||
// Run timer with this frequency (ms) for checking notification queue.
|
||||
const NOTIFICATION_EXEC_INTERVAL = 300;
|
||||
@@ -80,6 +83,13 @@ function isPeerRestricted(acs) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function shouldPresentCallPanel(callState) {
|
||||
// Show call panel if either:
|
||||
// - call is outgoing (and the client is waiting for the other side to pick up) or,
|
||||
// - call is already in progress.
|
||||
return callState == CALL_STATE_OUTGOING_INITATED || callState == CALL_STATE_IN_PROGRESS;
|
||||
}
|
||||
|
||||
class MessagesView extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@@ -118,6 +128,7 @@ class MessagesView extends React.Component {
|
||||
this.handlePickReply = this.handlePickReply.bind(this);
|
||||
this.handleCancelReply = this.handleCancelReply.bind(this);
|
||||
this.handleQuoteClick = this.handleQuoteClick.bind(this);
|
||||
this.handleCallHangup = this.handleCallHangup.bind(this);
|
||||
|
||||
this.chatMessageRefs = {};
|
||||
this.getOrCreateMessageRef = this.getOrCreateMessageRef.bind(this);
|
||||
@@ -167,6 +178,9 @@ class MessagesView extends React.Component {
|
||||
if (this.state.topic != prevState.topic) {
|
||||
if (prevState.topic && !Tinode.isNewGroupTopicName(prevState.topic)) {
|
||||
this.leave(prevState.topic);
|
||||
if (prevState.rtcPanel) {
|
||||
this.handleCallHangup(prevState.topic, prevProps.callSeq);
|
||||
}
|
||||
}
|
||||
|
||||
if (topic) {
|
||||
@@ -215,6 +229,7 @@ class MessagesView extends React.Component {
|
||||
docPreview: null,
|
||||
imagePreview: null,
|
||||
imagePostview: null,
|
||||
rtcPanel: null,
|
||||
typingIndicator: false,
|
||||
scrollPosition: 0,
|
||||
fetchingMessages: false,
|
||||
@@ -226,29 +241,27 @@ class MessagesView extends React.Component {
|
||||
} else if (nextProps.topic != prevState.topic) {
|
||||
const topic = nextProps.tinode.getTopic(nextProps.topic);
|
||||
|
||||
let reply = null;
|
||||
if (nextProps.forwardMessage) {
|
||||
// We are forwarding a message. Show preview.
|
||||
const preview = nextProps.forwardMessage.preview;
|
||||
reply = {
|
||||
content: preview,
|
||||
seq: null
|
||||
};
|
||||
}
|
||||
|
||||
nextState = {
|
||||
topic: nextProps.topic,
|
||||
docPreview: null,
|
||||
imagePreview: null,
|
||||
imagePostview: null,
|
||||
rtcPanel: null,
|
||||
typingIndicator: false,
|
||||
scrollPosition: 0,
|
||||
fetchingMessages: false,
|
||||
reply: reply,
|
||||
showGoToLastButton: false,
|
||||
deleted: topic._deleted
|
||||
};
|
||||
|
||||
if (nextProps.forwardMessage) {
|
||||
// We are forwarding a message. Show preview.
|
||||
nextState.reply = {
|
||||
content: nextProps.forwardMessage.preview,
|
||||
seq: null
|
||||
};
|
||||
}
|
||||
|
||||
if (topic) {
|
||||
// Topic exists.
|
||||
const subs = [];
|
||||
@@ -292,6 +305,10 @@ class MessagesView extends React.Component {
|
||||
latestClearId: topic.maxClearId(),
|
||||
channel: topic.isChannelType()
|
||||
});
|
||||
|
||||
if (nextProps.callTopic == topic.name && shouldPresentCallPanel(nextProps.callState)) {
|
||||
nextState.rtcPanel = nextProps.callTopic;
|
||||
}
|
||||
} else {
|
||||
// Invalid topic.
|
||||
Object.assign(nextState, {
|
||||
@@ -304,6 +321,12 @@ class MessagesView extends React.Component {
|
||||
channel: false
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// We are still in same topic. Show the call panel if necessary.
|
||||
if (nextProps.callTopic == prevState.topic && !prevState.rtcPanel &&
|
||||
shouldPresentCallPanel(nextProps.callState)) {
|
||||
nextState.rtcPanel = nextProps.callTopic;
|
||||
}
|
||||
}
|
||||
|
||||
if (nextProps.acs) {
|
||||
@@ -402,6 +425,7 @@ class MessagesView extends React.Component {
|
||||
if (!oldTopicName || !this.props.tinode.isTopicCached(oldTopicName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldTopic = this.props.tinode.getTopic(oldTopicName);
|
||||
if (oldTopic && oldTopic.isSubscribed()) {
|
||||
oldTopic.leave(false)
|
||||
@@ -434,7 +458,8 @@ class MessagesView extends React.Component {
|
||||
const pos = event.target.scrollHeight - event.target.scrollTop - event.target.offsetHeight;
|
||||
this.setState({
|
||||
scrollPosition: pos,
|
||||
showGoToLastButton: pos > SHOW_GO_TO_LAST_DIST,
|
||||
// Show [go to latest message] if far enough from bottom and scrolling down.
|
||||
showGoToLastButton: (pos > SHOW_GO_TO_LAST_DIST) && (pos < this.state.scrollPosition),
|
||||
});
|
||||
|
||||
if (this.state.fetchingMessages) {
|
||||
@@ -582,8 +607,8 @@ class MessagesView extends React.Component {
|
||||
clearTimeout(this.keyPressTimer)
|
||||
this.setState({messageCount: topic.messageCount(), typingIndicator: false});
|
||||
|
||||
// Scroll to the bottom if the message is added to the end of the message list.
|
||||
// TODO: This should be replaced by showing a "scroll to bottom" button.
|
||||
// Scroll to the bottom if the message is added to the end of the message
|
||||
// list if already at the bottom, otherwise show [go to latest] button.
|
||||
if (topic.isNewMessage(msg.seq)) {
|
||||
if (this.state.scrollPosition > SHOW_GO_TO_LAST_DIST) {
|
||||
this.setState({showGoToLastButton: true});
|
||||
@@ -788,6 +813,14 @@ class MessagesView extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleCallHangup(topic, seq) {
|
||||
this.props.onVideoCallClosed();
|
||||
this.setState({
|
||||
rtcPanel: null
|
||||
});
|
||||
this.props.onCallHangup(topic, seq);
|
||||
}
|
||||
|
||||
// sendImageAttachment sends the image bits inband as Drafty message.
|
||||
sendImageAttachment(caption, blob) {
|
||||
const mime = this.state.imagePreview.mime;
|
||||
@@ -926,7 +959,6 @@ class MessagesView extends React.Component {
|
||||
|
||||
// seq: seq ID of the source message
|
||||
// context: message content.
|
||||
// forwarded: true if the source message is also a forwarded message.
|
||||
// senderId: UID of the sender of the source message.
|
||||
// senderName: full name of the sender of the original message.
|
||||
handlePickReply(seq, content, senderId, senderName) {
|
||||
@@ -1013,6 +1045,26 @@ class MessagesView extends React.Component {
|
||||
onClose={this.handleClosePreview}
|
||||
onSendMessage={this.sendFileAttachment} />
|
||||
);
|
||||
} else if (this.state.rtcPanel) {
|
||||
const topic = this.props.tinode.getTopic(this.state.topic);
|
||||
component2 = (
|
||||
<CallPanel
|
||||
topic={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}
|
||||
|
||||
onError={this.props.onError}
|
||||
onHangup={this.handleCallHangup}
|
||||
onInvite={this.props.onCallInvite}
|
||||
onSendOffer={this.props.onCallSendOffer}
|
||||
onIceCandidate={this.props.onCallIceCandidate}
|
||||
onSendAnswer={this.props.onCallSendAnswer}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
const topic = this.props.tinode.getTopic(this.state.topic);
|
||||
const isChannel = topic.isChannelType();
|
||||
@@ -1088,12 +1140,21 @@ 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}
|
||||
timestamp={msg.ts}
|
||||
duration={duration}
|
||||
response={isReply}
|
||||
seq={msg.seq}
|
||||
isGroup={groupTopic}
|
||||
@@ -1235,7 +1296,6 @@ class MessagesView extends React.Component {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
component = <div id="topic-view">{component2}</div>
|
||||
}
|
||||
return component;
|
||||
|
||||
+235
-27
@@ -13,11 +13,15 @@ import ContextMenu from '../widgets/context-menu.jsx';
|
||||
import ForwardDialog from '../widgets/forward-dialog.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';
|
||||
|
||||
import { API_KEY, APP_NAME, DEFAULT_P2P_ACCESS_MODE, FORWARDED_PREVIEW_LENGTH, LOGGING_ENABLED,
|
||||
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';
|
||||
import { PACKAGE_VERSION } from '../version.js';
|
||||
import { base64ReEncode, makeImageUrl } from '../lib/blob-helpers.js';
|
||||
import { detectServerAddress, isLocalHost, isSecureConnection } from '../lib/host-name.js';
|
||||
@@ -64,6 +68,11 @@ const messages = defineMessages({
|
||||
id: 'menu_item_info',
|
||||
defaultMessage: 'Info',
|
||||
description: 'Show extended topic information'
|
||||
},
|
||||
menu_item_video_call: {
|
||||
id: 'menu_item_video_call',
|
||||
defaultMessage: 'Video Call',
|
||||
description: 'Start video call'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -145,6 +154,19 @@ class TinodeWeb extends React.Component {
|
||||
this.handleShowForwardDialog = this.handleShowForwardDialog.bind(this);
|
||||
this.handleHideForwardDialog = this.handleHideForwardDialog.bind(this);
|
||||
|
||||
this.handleStartVideoCall = this.handleStartVideoCall.bind(this);
|
||||
this.handleInfoMessage = this.handleInfoMessage.bind(this);
|
||||
this.handleCallClose = this.handleCallClose.bind(this);
|
||||
|
||||
this.handleCallInvite = this.handleCallInvite.bind(this);
|
||||
this.handleCallRinging = this.handleCallRinging.bind(this);
|
||||
this.handleCallHangup = this.handleCallHangup.bind(this);
|
||||
this.handleCallSendOffer = this.handleCallSendOffer.bind(this);
|
||||
this.handleCallIceCandidate = this.handleCallIceCandidate.bind(this);
|
||||
this.handleCallSendAnswer = this.handleCallSendAnswer.bind(this);
|
||||
|
||||
this.handleCallAcceptCall = this.handleCallAcceptCall.bind(this);
|
||||
|
||||
this.sendMessageToTopic = this.sendMessageToTopic.bind(this);
|
||||
}
|
||||
|
||||
@@ -199,6 +221,13 @@ class TinodeWeb extends React.Component {
|
||||
infoPanel: undefined,
|
||||
mobilePanel: 'sidepanel',
|
||||
|
||||
// Video calls.
|
||||
callTopic: undefined,
|
||||
callState: CALL_STATE_NONE,
|
||||
// If true, call state should be transitioned to CALL_STATE_IN_PROGRESS upon
|
||||
// switching to the call topic.
|
||||
callShouldStart: false,
|
||||
|
||||
contextMenuVisible: false,
|
||||
contextMenuBounds: null,
|
||||
contextMenuClickAt: null,
|
||||
@@ -254,6 +283,7 @@ class TinodeWeb extends React.Component {
|
||||
this.tinode.onConnect = this.handleConnected;
|
||||
this.tinode.onDisconnect = this.handleDisconnect;
|
||||
this.tinode.onAutoreconnectIteration = this.handleAutoreconnectIteration;
|
||||
this.tinode.onInfoMessage = this.handleInfoMessage;
|
||||
}).then(() => {
|
||||
// Initialize desktop alerts.
|
||||
if (this.state.desktopAlertsEnabled) {
|
||||
@@ -317,21 +347,7 @@ class TinodeWeb extends React.Component {
|
||||
|
||||
// Tinode received a push notification from the server.
|
||||
handlePushMessage(payload) {
|
||||
const data = payload.data || {};
|
||||
switch (data.what) {
|
||||
case 'msg':
|
||||
if (Tinode.isChannelTopicName(data.topic)) {
|
||||
// The last argument is a fake user Id: otherwise the update is seen as one from the current user.
|
||||
this.tinode.oobNotification('msg', data.topic, data.seq, 'fake-uid');
|
||||
}
|
||||
break;
|
||||
case 'read':
|
||||
// Message read on another device, clear it here.
|
||||
this.tinode.oobNotification('read', data.topic, data.seq);
|
||||
break;
|
||||
default:
|
||||
console.warn("Unknown push type ignored", data.what, data);
|
||||
}
|
||||
this.tinode.oobNotification(payload.data || {});
|
||||
}
|
||||
|
||||
initFCMessaging() {
|
||||
@@ -501,7 +517,13 @@ class TinodeWeb extends React.Component {
|
||||
}
|
||||
|
||||
handleError(err, level, action, actionText) {
|
||||
this.setState({errorText: err, errorLevel: level, errorAction: action, errorActionText: actionText});
|
||||
this.setState({
|
||||
errorText: err,
|
||||
errorLevel: level,
|
||||
errorAction: action,
|
||||
errorActionText: actionText,
|
||||
callShouldStart: false,
|
||||
});
|
||||
}
|
||||
|
||||
// User clicked Login button in the side panel.
|
||||
@@ -586,7 +608,7 @@ class TinodeWeb extends React.Component {
|
||||
this.handleError(
|
||||
formatMessage(messages.reconnect_countdown, {seconds: timeLeft}),
|
||||
'warn',
|
||||
() => {
|
||||
_ => {
|
||||
clearInterval(this.reconnectCountdown);
|
||||
this.tinode.reconnect();
|
||||
},
|
||||
@@ -964,7 +986,8 @@ class TinodeWeb extends React.Component {
|
||||
// - head - head dictionary to be attached to the message
|
||||
handleSendMessage(msg, uploadCompletionPromise, uploader, head) {
|
||||
const topic = this.tinode.getTopic(this.state.topicSelected);
|
||||
this.sendMessageToTopic(topic, msg, uploadCompletionPromise, uploader, head);
|
||||
/* TODO: check if return is required */
|
||||
return this.sendMessageToTopic(topic, msg, uploadCompletionPromise, uploader, head);
|
||||
}
|
||||
|
||||
sendMessageToTopic(topic, msg, uploadCompletionPromise, uploader, head) {
|
||||
@@ -999,11 +1022,13 @@ class TinodeWeb extends React.Component {
|
||||
completion.push(subscribePromise);
|
||||
}
|
||||
|
||||
topic.publishDraft(msg, Promise.all(completion))
|
||||
.then(_ => {
|
||||
// TODO: check if return is required.
|
||||
return topic.publishDraft(msg, Promise.all(completion))
|
||||
.then((ctrl) => {
|
||||
if (topic.isArchived()) {
|
||||
topic.archive(false);
|
||||
}
|
||||
return ctrl;
|
||||
})
|
||||
.catch((err) => {
|
||||
this.handleError(err.message, 'err');
|
||||
@@ -1257,14 +1282,19 @@ class TinodeWeb extends React.Component {
|
||||
|
||||
// New topic was created, here is the new topic name.
|
||||
handleNewTopicCreated(oldName, newName) {
|
||||
let nextState = {};
|
||||
if (this.state.callShouldStart) {
|
||||
nextState = {callState: CALL_STATE_IN_PROGRESS, callShouldStart: false};
|
||||
}
|
||||
if (this.state.topicSelected == oldName && oldName != newName) {
|
||||
// If the current URl contains the old topic name, replace it with new.
|
||||
// Update the name of the selected topic first so the navigator doen't clear
|
||||
// the state.
|
||||
this.setState({topicSelected: newName}, () => {
|
||||
HashNavigation.navigateTo(HashNavigation.setUrlTopic('', newName));
|
||||
});
|
||||
nextState.topicSelected = newName;
|
||||
}
|
||||
this.setState(nextState, _ => {
|
||||
HashNavigation.navigateTo(HashNavigation.setUrlTopic('', newName));
|
||||
});
|
||||
}
|
||||
|
||||
handleTopicUpdateRequest(topicName, pub, priv, defacs) {
|
||||
@@ -1376,7 +1406,7 @@ class TinodeWeb extends React.Component {
|
||||
}
|
||||
|
||||
handleDeleteAccount() {
|
||||
this.tinode.delCurrentUser(true).then((ctrl) => {
|
||||
this.tinode.delCurrentUser(true).then(_ => {
|
||||
this.handleLogout();
|
||||
});
|
||||
}
|
||||
@@ -1428,7 +1458,7 @@ class TinodeWeb extends React.Component {
|
||||
return;
|
||||
}
|
||||
|
||||
topic.updateMode(null, '-JP').then((ctrl) => {
|
||||
topic.updateMode(null, '-JP').then(_ => {
|
||||
// Hide MessagesView and InfoView panels.
|
||||
HashNavigation.navigateTo(HashNavigation.setUrlTopic(window.location.hash, ''));
|
||||
}).catch((err) => {
|
||||
@@ -1443,7 +1473,7 @@ class TinodeWeb extends React.Component {
|
||||
}
|
||||
|
||||
// Publish spam report.
|
||||
this.tinode.report('report',topicName);
|
||||
this.tinode.report('report', topicName);
|
||||
|
||||
// Remove J and P permissions.
|
||||
topic.updateMode(null, '-JP').then((ctrl) => {
|
||||
@@ -1488,6 +1518,12 @@ class TinodeWeb extends React.Component {
|
||||
defaultTopicContextMenu(topicName) {
|
||||
const topic = this.tinode.getTopic(topicName);
|
||||
|
||||
if (topic._deleted) {
|
||||
return [
|
||||
'topic_delete'
|
||||
];
|
||||
}
|
||||
|
||||
let muted = false, blocked = false, self_blocked = false, subscribed = false, deleter = false, archived = false;
|
||||
if (topic) {
|
||||
subscribed = topic.isSubscribed();
|
||||
@@ -1507,6 +1543,10 @@ class TinodeWeb extends React.Component {
|
||||
title: this.props.intl.formatMessage(messages.menu_item_info),
|
||||
handler: this.handleShowInfoView
|
||||
} : null,
|
||||
subscribed && Tinode.isP2PTopicName(topicName) ? {
|
||||
title: this.props.intl.formatMessage(messages.menu_item_video_call),
|
||||
handler: this.handleStartVideoCall
|
||||
} : null,
|
||||
subscribed ? 'messages_clear' : null,
|
||||
subscribed && deleter ? 'messages_clear_hard' : null,
|
||||
muted ? (blocked ? null : 'topic_unmute') : 'topic_mute',
|
||||
@@ -1637,6 +1677,148 @@ class TinodeWeb extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleStartVideoCall() {
|
||||
this.setState({
|
||||
callTopic: this.state.topicSelected,
|
||||
callState: CALL_STATE_OUTGOING_INITATED
|
||||
});
|
||||
}
|
||||
|
||||
handleCallInvite(callTopic, callSeq, callState) {
|
||||
switch (callState) {
|
||||
case CALL_STATE_OUTGOING_INITATED:
|
||||
let head = {mime: CALL_MESSAGE_MIME_TYPE };
|
||||
this.handleSendMessage('started', undefined, undefined, head)
|
||||
.then((ctrl) => {
|
||||
if (ctrl.code < 200 || ctrl.code >= 300 || !ctrl.params || !ctrl.params.seq) {
|
||||
this.handleCallClose();
|
||||
return;
|
||||
}
|
||||
this.setState({callSeq: ctrl.params['seq']});
|
||||
});
|
||||
break;
|
||||
case CALL_STATE_IN_PROGRESS:
|
||||
const topic = this.tinode.getTopic(callTopic);
|
||||
if (!topic) {
|
||||
return;
|
||||
}
|
||||
// We've accepted the call. Let the other side know.
|
||||
topic.videoCall('accept', callSeq);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
handleCallRinging(callTopic, callSeq) {
|
||||
const topic = this.tinode.getTopic(callTopic);
|
||||
if (!topic) {
|
||||
return;
|
||||
}
|
||||
|
||||
topic.videoCall('ringing', callSeq);
|
||||
}
|
||||
|
||||
handleCallHangup(callTopic, callSeq) {
|
||||
const topic = this.tinode.getTopic(callTopic);
|
||||
if (!topic) {
|
||||
return;
|
||||
}
|
||||
|
||||
topic.videoCall('hang-up', callSeq);
|
||||
}
|
||||
|
||||
handleCallSendOffer(callTopic, callSeq, sdp) {
|
||||
const topic = this.tinode.getTopic(callTopic);
|
||||
if (!topic) {
|
||||
return;
|
||||
}
|
||||
|
||||
topic.videoCall('offer', callSeq, sdp);
|
||||
}
|
||||
|
||||
handleCallIceCandidate(callTopic, callSeq, candidate) {
|
||||
const topic = this.tinode.getTopic(callTopic);
|
||||
if (!topic) {
|
||||
return;
|
||||
}
|
||||
|
||||
topic.videoCall('ice-candidate', callSeq, candidate);
|
||||
}
|
||||
handleCallSendAnswer(callTopic, callSeq, sdp) {
|
||||
const topic = this.tinode.getTopic(callTopic);
|
||||
if (!topic) {
|
||||
return;
|
||||
}
|
||||
|
||||
topic.videoCall('answer', callSeq, sdp);
|
||||
}
|
||||
|
||||
handleCallClose() {
|
||||
if (this.state.callTimeout) {
|
||||
console.log('closing call timeout ', this.state.callTimeout);
|
||||
clearTimeout(this.state.callTimeout);
|
||||
}
|
||||
this.setState({
|
||||
callTopic: undefined,
|
||||
callState: CALL_STATE_NONE
|
||||
});
|
||||
}
|
||||
|
||||
handleCallAcceptCall(topicName) {
|
||||
const topic = this.tinode.getTopic(topicName);
|
||||
if (!topic) {
|
||||
return;
|
||||
}
|
||||
if (topic.isSubscribed()) {
|
||||
this.handleTopicSelected(this.state.callTopic);
|
||||
this.setState({
|
||||
callState: CALL_STATE_IN_PROGRESS
|
||||
});
|
||||
} else {
|
||||
// We need to switch and subscribe to callTopic first.
|
||||
this.setState({
|
||||
callShouldStart: true,
|
||||
}, () => this.handleTopicSelected(this.state.callTopic));
|
||||
}
|
||||
}
|
||||
|
||||
handleInfoMessage(info) {
|
||||
if (info.what != 'call') {
|
||||
return;
|
||||
}
|
||||
switch (info.event) {
|
||||
case 'invite':
|
||||
if (info.from != this.state.myUserId) {
|
||||
// Incoming call.
|
||||
this.setState({
|
||||
callTopic: info.src,
|
||||
callState: CALL_STATE_INCOMING_RECEIVED,
|
||||
callSeq: info.seq
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'accept':
|
||||
console.log('TinodeWeb: received accept msg ', info);
|
||||
// If another my session has accepted the call.
|
||||
if (Tinode.isMeTopicName(info.topic) && this.tinode.isMe(info.from)) {
|
||||
this.setState({
|
||||
callTopic: null,
|
||||
callState: CALL_STATE_NONE,
|
||||
callSeq: null
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (info.topic == this.state.callTopic) {
|
||||
// Update state.
|
||||
this.setState({callState: CALL_STATE_IN_PROGRESS});
|
||||
}
|
||||
break;
|
||||
case 'hang-up':
|
||||
// Remote hangup.
|
||||
this.handleCallClose();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div id="app-container" ref={this.selfRef}>
|
||||
@@ -1774,6 +1956,16 @@ class TinodeWeb extends React.Component {
|
||||
forwardMessage={this.state.forwardMessage}
|
||||
onCancelForwardMessage={this.handleHideForwardDialog}
|
||||
|
||||
callTopic={this.state.callTopic}
|
||||
callSeq={this.state.callSeq}
|
||||
callState={this.state.callState}
|
||||
onCallHangup={this.handleCallHangup}
|
||||
|
||||
onCallInvite={this.handleCallInvite}
|
||||
onCallSendOffer={this.handleCallSendOffer}
|
||||
onCallIceCandidate={this.handleCallIceCandidate}
|
||||
onCallSendAnswer={this.handleCallSendAnswer}
|
||||
|
||||
errorText={this.state.errorText}
|
||||
errorLevel={this.state.errorLevel}
|
||||
errorAction={this.state.errorAction}
|
||||
@@ -1788,7 +1980,8 @@ class TinodeWeb extends React.Component {
|
||||
showContextMenu={this.handleShowContextMenu}
|
||||
onChangePermissions={this.handleChangePermissions}
|
||||
onNewChat={this.handleNewChatInvitation}
|
||||
sendMessage={this.handleSendMessage} />
|
||||
sendMessage={this.handleSendMessage}
|
||||
onVideoCallClosed={this.handleCallClose} />
|
||||
|
||||
{this.state.infoPanel ?
|
||||
<InfoView
|
||||
@@ -1826,6 +2019,21 @@ 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,7 +1,6 @@
|
||||
// Audio recorder widget.
|
||||
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { secondsToTime } from '../lib/strformat';
|
||||
import { base64ToIntArray } from '../lib/blob-helpers';
|
||||
@@ -13,9 +12,9 @@ const LINE_WIDTH = 3 * CANVAS_UPSCALING;
|
||||
// Spacing between two visualization bars.
|
||||
const SPACING = 2 * CANVAS_UPSCALING;
|
||||
// Color of histogram bars.
|
||||
const BAR_COLOR = '#bbb';
|
||||
const BAR_COLOR_DARK = '#888';
|
||||
const THUMB_COLOR = '#666';
|
||||
const BAR_COLOR = '#888A';
|
||||
const BAR_COLOR_DARK = '#666C';
|
||||
const THUMB_COLOR = '#444E';
|
||||
// Minimum number of amplitude bars to draw.
|
||||
const MIN_PREVIEW_LENGTH = 16;
|
||||
|
||||
@@ -24,7 +23,7 @@ export default class AudioPlayer extends React.PureComponent {
|
||||
super(props);
|
||||
|
||||
let preview = base64ToIntArray(this.props.preview);
|
||||
if (Array.isArray(preview) && preview.length < MIN_PREVIEW_LENGTH) {
|
||||
if (!Array.isArray(preview) || preview.length < MIN_PREVIEW_LENGTH) {
|
||||
preview = null;
|
||||
}
|
||||
|
||||
@@ -57,9 +56,7 @@ export default class AudioPlayer extends React.PureComponent {
|
||||
this.initAudio();
|
||||
}
|
||||
|
||||
if (this.state.preview) {
|
||||
this.initCanvas();
|
||||
}
|
||||
this.initCanvas();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@@ -79,7 +76,7 @@ export default class AudioPlayer extends React.PureComponent {
|
||||
|
||||
if (this.props.preview != prevProps.preview) {
|
||||
let preview = base64ToIntArray(this.props.preview);
|
||||
if (Array.isArray(preview) && preview.length < MIN_PREVIEW_LENGTH) {
|
||||
if (!Array.isArray(preview) || preview.length < MIN_PREVIEW_LENGTH) {
|
||||
preview = null;
|
||||
}
|
||||
this.setState({preview: preview}, this.initCanvas);
|
||||
@@ -99,7 +96,7 @@ export default class AudioPlayer extends React.PureComponent {
|
||||
}
|
||||
|
||||
initCanvas() {
|
||||
// Force canvas aspect ration to match one of the element + upscale canvas to reduce blurring.
|
||||
// Force canvas aspect ratio to match one of the element + upscale canvas to reduce blurring.
|
||||
this.canvasRef.current.width = this.canvasRef.current.offsetWidth * CANVAS_UPSCALING;
|
||||
this.canvasRef.current.height = this.canvasRef.current.offsetHeight * CANVAS_UPSCALING;
|
||||
|
||||
@@ -122,7 +119,7 @@ export default class AudioPlayer extends React.PureComponent {
|
||||
this.canvasContext.lineWidth = LINE_WIDTH;
|
||||
|
||||
const drawFrame = () => {
|
||||
if (!this.canvasRef.current) {
|
||||
if (!this.canvasRef.current || !this.audioPlayer) {
|
||||
// The component is unmounted.
|
||||
return;
|
||||
}
|
||||
@@ -134,7 +131,7 @@ export default class AudioPlayer extends React.PureComponent {
|
||||
window.requestAnimationFrame(drawFrame);
|
||||
}
|
||||
|
||||
// Current pplayback position.
|
||||
// Current playback position.
|
||||
const thumbAt = this.props.duration ?
|
||||
Math.max(0, Math.min(this.audioPlayer.currentTime * 1000 / this.props.duration, 1)) * (width - LINE_WIDTH * 2) : -1;
|
||||
|
||||
@@ -173,27 +170,36 @@ export default class AudioPlayer extends React.PureComponent {
|
||||
|
||||
// Quick and dirty downsampling of the original preview bars into a smaller (or equal) number of bars we can display here.
|
||||
resampleBars(original) {
|
||||
const barCount = Math.min(original.length, ((this.canvasRef.current.width - SPACING) / (LINE_WIDTH + SPACING)) | 0);
|
||||
const factor = original.length / barCount;
|
||||
let amps = [];
|
||||
let max = -1;
|
||||
for (let i=0;i<barCount;i++) {
|
||||
let lo = (i * factor) | 0; // low bound;
|
||||
let hi = ((i + 1) * factor) | 0; // high bound;
|
||||
let amp = 0.0;
|
||||
for (let j=lo; j<hi; j++) {
|
||||
amp += original[j]
|
||||
}
|
||||
amps[i] = Math.max(0, amp / (hi - lo));
|
||||
max = Math.max(amps[i], max);
|
||||
const dstCount = ((this.canvasRef.current.width - SPACING) / (LINE_WIDTH + SPACING)) | 0;
|
||||
// Remove extra padding on the right due to fractional bar which is not drawn.
|
||||
this.effectiveWidth = dstCount * (LINE_WIDTH + SPACING) + SPACING;
|
||||
|
||||
if (!Array.isArray(original) || original.length == 0) {
|
||||
return Array.apply(null, Array(dstCount)).map(_ => 0.01);
|
||||
}
|
||||
|
||||
if (max > 0) {
|
||||
// Remove extra padding on the right due to fractional bar which is not drawn.
|
||||
this.effectiveWidth = barCount * (LINE_WIDTH + SPACING) + SPACING;
|
||||
return amps.map(a => a / max);
|
||||
const factor = original.length / dstCount;
|
||||
let amps = [];
|
||||
let maxAmp = -1;
|
||||
for (let i=0; i<dstCount; i++) {
|
||||
let lo = (i * factor) | 0; // low bound;
|
||||
let hi = ((i + 1) * factor) | 0; // high bound;
|
||||
if (hi == lo) {
|
||||
amps[i] = original[lo];
|
||||
} else {
|
||||
let amp = 0.0;
|
||||
for (let j=lo; j<hi; j++) {
|
||||
amp += original[j]
|
||||
}
|
||||
amps[i] = Math.max(0, amp / (hi - lo));
|
||||
}
|
||||
maxAmp = Math.max(amps[i], maxAmp);
|
||||
}
|
||||
return null;
|
||||
|
||||
if (maxAmp > 0) {
|
||||
return amps.map(a => a / maxAmp);
|
||||
}
|
||||
return Array.apply(null, Array(dstCount)).map(_ => 0.01);
|
||||
}
|
||||
|
||||
handlePlay(e) {
|
||||
@@ -211,7 +217,7 @@ export default class AudioPlayer extends React.PureComponent {
|
||||
}
|
||||
|
||||
handleError(err) {
|
||||
console.log(err);
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
handleSeek(e) {
|
||||
@@ -221,7 +227,7 @@ export default class AudioPlayer extends React.PureComponent {
|
||||
const offset = (e.clientX - rect.left) / this.effectiveWidth * CANVAS_UPSCALING;
|
||||
this.audioPlayer.currentTime = this.props.duration * offset / 1000;
|
||||
this.setState({currentTime: secondsToTime(this.audioPlayer.currentTime, this.state.longMin)});
|
||||
if (!this.state.playing && this.state.preview) {
|
||||
if (!this.state.playing) {
|
||||
this.visualize();
|
||||
}
|
||||
}
|
||||
@@ -237,25 +243,14 @@ export default class AudioPlayer extends React.PureComponent {
|
||||
</a>);
|
||||
return (<div className="audio-player">{this.props.short ?
|
||||
<>
|
||||
<div>
|
||||
{this.state.preview ?
|
||||
<canvas className="playback" ref={this.canvasRef} onClick={this.handleSeek} /> :
|
||||
<div className="playback"> - </div>
|
||||
}
|
||||
</div>
|
||||
<canvas className="playback" ref={this.canvasRef} onClick={this.handleSeek} />
|
||||
{play}
|
||||
</>
|
||||
:
|
||||
<>
|
||||
{play}
|
||||
<div>
|
||||
{this.state.preview ?
|
||||
<canvas className="playback" ref={this.canvasRef} onClick={this.handleSeek} /> :
|
||||
<div className="playback">
|
||||
<FormattedMessage
|
||||
id="preview_unavailable" defaultMessage="unavailable" description="Message shown when media preview is not available" />
|
||||
</div>
|
||||
}
|
||||
<canvas className="playback" ref={this.canvasRef} onClick={this.handleSeek} />
|
||||
<div className="timer">{this.state.currentTime}/{this.state.duration}</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -8,6 +8,7 @@ import fixWebmDuration from 'fix-webm-duration';
|
||||
|
||||
import { intArrayToBase64 } from '../lib/blob-helpers.js'
|
||||
import { secondsToTime } from '../lib/strformat';
|
||||
import { MAX_DURATION, MIN_DURATION } from '../config.js';
|
||||
|
||||
// FFT resolution.
|
||||
const BUFFER_SIZE = 256;
|
||||
@@ -20,13 +21,9 @@ const SPACING = 2 * CANVAS_UPSCALING;
|
||||
// Duration represented by one visualization bar.
|
||||
const MILLIS_PER_BAR = 100;
|
||||
// Color of histogram bars
|
||||
const BAR_COLOR = '#bbb';
|
||||
const BAR_COLOR = '#BBBD';
|
||||
// Vertical scaling for visualization bars.
|
||||
const BAR_SCALE = 64.0;
|
||||
// Minimum duration of a recording in milliseconds.
|
||||
const MIN_DURATION = 200;
|
||||
// Maximum duration of a recording in milliseconds (10 min).
|
||||
const MAX_DURATION = 600000;
|
||||
// Number of bars in preview.
|
||||
const VISUALIZATION_BARS = 96;
|
||||
// Maximum number of samples per bar.
|
||||
|
||||
@@ -0,0 +1,381 @@
|
||||
// CallPanel: displays local and remote viewports and controls.
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import LetterTile from '../widgets/letter-tile.jsx';
|
||||
|
||||
import { CALL_WEBRTC_CONFIG, MAX_TITLE_LENGTH } from '../config.js';
|
||||
import { CALL_STATE_OUTGOING_INITATED, CALL_STATE_IN_PROGRESS } from '../constants.js';
|
||||
|
||||
import { clipStr } from '../lib/utils.js'
|
||||
|
||||
export default class CallPanel extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
localStream: undefined,
|
||||
pc: undefined,
|
||||
|
||||
previousOnInfo: undefined
|
||||
};
|
||||
|
||||
this.localStreamConstraints = {
|
||||
audio: true,
|
||||
video: true
|
||||
};
|
||||
|
||||
this.localRef = React.createRef();
|
||||
this.remoteRef = React.createRef();
|
||||
|
||||
this.onInfo = this.onInfo.bind(this);
|
||||
this.start = this.start.bind(this);
|
||||
|
||||
this.createPeerConnection = this.createPeerConnection.bind(this);
|
||||
|
||||
this.handleNegotiationNeededEvent = this.handleNegotiationNeededEvent.bind(this);
|
||||
this.handleICECandidateEvent = this.handleICECandidateEvent.bind(this);
|
||||
this.handleNewICECandidateMsg = this.handleNewICECandidateMsg.bind(this);
|
||||
this.handleICEConnectionStateChangeEvent = this.handleICEConnectionStateChangeEvent.bind(this);
|
||||
this.handleSignalingStateChangeEvent = this.handleSignalingStateChangeEvent.bind(this);
|
||||
this.handleICEGatheringStateChangeEvent = this.handleICEGatheringStateChangeEvent.bind(this);
|
||||
this.handleTrackEvent = this.handleTrackEvent.bind(this);
|
||||
|
||||
this.handleVideoOfferMsg = this.handleVideoOfferMsg.bind(this);
|
||||
this.handleVideoAnswerMsg = this.handleVideoAnswerMsg.bind(this);
|
||||
this.handleNewICECandidateMsg = this.handleNewICECandidateMsg.bind(this);
|
||||
|
||||
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);
|
||||
this.handleToggleCameraClick = this.handleToggleCameraClick.bind(this);
|
||||
this.handleToggleMicClick = this.handleToggleMicClick.bind(this);
|
||||
this.toggleMedia = this.toggleMedia.bind(this);
|
||||
|
||||
this.handleRemoteHangup = this.handleRemoteHangup.bind(this);
|
||||
this.handleVideoCallAccepted = this.handleVideoCallAccepted.bind(this);
|
||||
}
|
||||
|
||||
handleVideoCallAccepted(info) {
|
||||
const pc = this.createPeerConnection();
|
||||
const stream = this.state.localStream;
|
||||
stream.getTracks().forEach(track => {
|
||||
pc.addTrack(track, stream);
|
||||
});
|
||||
}
|
||||
|
||||
onInfo(info) {
|
||||
if (info.what != 'call') {
|
||||
return;
|
||||
}
|
||||
switch (info.event) {
|
||||
case 'accept':
|
||||
this.handleVideoCallAccepted(info);
|
||||
break;
|
||||
case 'offer':
|
||||
this.handleVideoOfferMsg(info);
|
||||
break;
|
||||
case 'answer':
|
||||
this.handleVideoAnswerMsg(info);
|
||||
break;
|
||||
case 'ice-candidate':
|
||||
this.handleNewICECandidateMsg(info);
|
||||
break;
|
||||
case 'hang-up':
|
||||
this.handleRemoteHangup(info);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
createPeerConnection() {
|
||||
var pc = new RTCPeerConnection(CALL_WEBRTC_CONFIG);
|
||||
|
||||
pc.onicecandidate = this.handleICECandidateEvent;
|
||||
pc.oniceconnectionstatechange = this.handleICEConnectionStateChangeEvent;
|
||||
pc.onicegatheringstatechange = this.handleICEGatheringStateChangeEvent;
|
||||
pc.onsignalingstatechange = this.handleSignalingStateChangeEvent;
|
||||
pc.onnegotiationneeded = this.handleNegotiationNeededEvent;
|
||||
pc.ontrack = this.handleTrackEvent;
|
||||
|
||||
this.setState({pc: pc});
|
||||
return pc;
|
||||
}
|
||||
|
||||
handleVideoAnswerMsg(info) {
|
||||
// Configure the remote description, which is the SDP payload
|
||||
// in 'info' message.
|
||||
var desc = new RTCSessionDescription(info.payload);
|
||||
this.state.pc.setRemoteDescription(desc).catch(this.reportError);
|
||||
}
|
||||
|
||||
reportError(err) {
|
||||
this.props.onError(err.message, 'err');
|
||||
}
|
||||
|
||||
handleNegotiationNeededEvent() {
|
||||
this.state.pc.createOffer().then(offer => {
|
||||
return this.state.pc.setLocalDescription(offer);
|
||||
})
|
||||
.then(_ => {
|
||||
this.props.onSendOffer(this.props.topic.name, 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());
|
||||
}
|
||||
}
|
||||
|
||||
handleNewICECandidateMsg(info) {
|
||||
var candidate = new RTCIceCandidate(info.payload);
|
||||
|
||||
this.state.pc.addIceCandidate(candidate)
|
||||
.catch(this.reportError);
|
||||
}
|
||||
|
||||
handleICEConnectionStateChangeEvent(event) {
|
||||
switch (this.state.pc.iceConnectionState) {
|
||||
case 'closed':
|
||||
case 'failed':
|
||||
this.handleCloseClick();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
handleSignalingStateChangeEvent(event) {
|
||||
if (this.state.pc.signalingState == 'closed') {
|
||||
this.handleCloseClick();
|
||||
}
|
||||
}
|
||||
|
||||
handleICEGatheringStateChangeEvent(event) {
|
||||
console.log('ICE gathering change state: ', event);
|
||||
}
|
||||
|
||||
handleTrackEvent(event) {
|
||||
// Remote video becomes available.
|
||||
this.remoteRef.current.srcObject = event.streams[0];
|
||||
// Make sure we display the title (peer's name) over the remote video.
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
handleGetUserMediaError(e) {
|
||||
switch(e.name) {
|
||||
case 'NotFoundError':
|
||||
// Cannot start the call b/c no camera and/or microphone found.
|
||||
this.reportError(e);
|
||||
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);
|
||||
break;
|
||||
}
|
||||
|
||||
// Make sure we shut down our end of the RTCPeerConnection so we're
|
||||
// ready to try again.
|
||||
this.handleCloseClick();
|
||||
}
|
||||
|
||||
handleVideoOfferMsg(info) {
|
||||
var localStream = null;
|
||||
|
||||
const pc = this.createPeerConnection();
|
||||
|
||||
var desc = new RTCSessionDescription(info.payload);
|
||||
|
||||
pc.setRemoteDescription(desc).then(() => {
|
||||
return navigator.mediaDevices.getUserMedia(this.localStreamConstraints);
|
||||
})
|
||||
.then(stream => {
|
||||
localStream = stream;
|
||||
this.localRef.current.srcObject = stream;
|
||||
this.setState({localStream: stream});
|
||||
|
||||
localStream.getTracks().forEach(track => {
|
||||
pc.addTrack(track, localStream);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
return pc.createAnswer();
|
||||
})
|
||||
.then(answer => {
|
||||
return pc.setLocalDescription(answer);
|
||||
})
|
||||
.then(() => {
|
||||
this.props.onSendAnswer(this.props.topic.name, this.props.seq, pc.localDescription.toJSON());
|
||||
})
|
||||
.catch(this.handleGetUserMediaError);
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
// Mute/unmute audio/video (specified by kind).
|
||||
toggleMedia(kind) {
|
||||
const stream = this.state.localStream;
|
||||
stream.getTracks().forEach(track => {
|
||||
if (track.kind != kind) {
|
||||
return;
|
||||
}
|
||||
track.enabled = !track.enabled;
|
||||
});
|
||||
// Make sure we redraw the mute/unmute icons (e.g. mic -> mic_off).
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
handleToggleCameraClick() {
|
||||
this.toggleMedia('video');
|
||||
}
|
||||
|
||||
handleToggleMicClick() {
|
||||
this.toggleMedia('audio');
|
||||
}
|
||||
|
||||
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';
|
||||
|
||||
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">
|
||||
<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>
|
||||
{remoteActive ?
|
||||
<div className="video-title">{clipStr(this.props.title, MAX_TITLE_LENGTH)}</div>
|
||||
:
|
||||
null}
|
||||
</div>
|
||||
</div>
|
||||
<div id="video-container-controls">
|
||||
<button className="video-call-hangup" 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>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -9,7 +9,8 @@ import LetterTile from './letter-tile.jsx';
|
||||
import ReceivedMarker from './received-marker.jsx'
|
||||
|
||||
import { fullFormatter } from '../lib/formatters.js';
|
||||
import { sanitizeUrl } from '../lib/utils.js';
|
||||
import { isVideoCall, sanitizeUrl } from '../lib/utils.js';
|
||||
import { secondsToTime } from '../lib/strformat.js';
|
||||
|
||||
class BaseChatMessage extends React.PureComponent {
|
||||
constructor(props) {
|
||||
@@ -140,6 +141,24 @@ class BaseChatMessage extends React.PureComponent {
|
||||
}, this);
|
||||
const tree = Drafty.format(content, fullFormatter, this.formatterContext);
|
||||
content = React.createElement(React.Fragment, null, tree);
|
||||
} else if (this.props.deleted) {
|
||||
// Message represents a range of deleted messages.
|
||||
content = <><i className="material-icons gray">block</i> <i className="gray">
|
||||
<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,6 +9,7 @@ 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';
|
||||
|
||||
@@ -77,12 +78,16 @@ 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) {
|
||||
const msg = c.latestMessage(true);
|
||||
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' ?
|
||||
msg.content.substr(0, MESSAGE_PREVIEW_LENGTH) :
|
||||
@@ -98,6 +103,8 @@ 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}
|
||||
unread={this.props.showUnread ? c.unread : 0}
|
||||
@@ -109,12 +116,12 @@ class ContactList extends React.Component {
|
||||
selected={selected}
|
||||
showOnline={this.props.showOnline && !isChannel}
|
||||
isChannel={isChannel}
|
||||
onSelected={this.props.onTopicSelected}
|
||||
showContextMenu={this.props.showContextMenu}
|
||||
isVerified={c.trusted && c.trusted.verified}
|
||||
isStaff={c.trusted && c.trusted.staff}
|
||||
isDangerous={c.trusted && c.trusted.danger}
|
||||
deleted={c._deleted}
|
||||
onSelected={this.props.onTopicSelected}
|
||||
item={key}
|
||||
index={contactNodes.length}
|
||||
key={key} />
|
||||
|
||||
@@ -70,7 +70,10 @@ class Contact extends React.Component {
|
||||
|
||||
// The this.props.preview contains alreay shortened Drafty or string.
|
||||
let preview;
|
||||
if (typeof this.props.preview == 'string') {
|
||||
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') {
|
||||
preview = this.props.preview;
|
||||
} else if (Drafty.isValid(this.props.preview)) {
|
||||
preview = React.createElement(React.Fragment, null, Drafty.format(this.props.preview, previewFormatter,
|
||||
|
||||
@@ -84,7 +84,8 @@ class SendMessage extends React.PureComponent {
|
||||
|
||||
if (prevProps.topicName != this.props.topicName) {
|
||||
this.setState({message: '', audioRec: false, quote: null});
|
||||
} else if (prevProps.reply != this.props.reply) {
|
||||
}
|
||||
if (prevProps.reply != this.props.reply) {
|
||||
this.setState({quote: this.formatReply()});
|
||||
}
|
||||
}
|
||||
@@ -103,12 +104,8 @@ class SendMessage extends React.PureComponent {
|
||||
}
|
||||
// FIXME: handle large files too.
|
||||
if (filePasted(e,
|
||||
(bits, mime, width, height, fname) => {
|
||||
this.props.onAttachImage(mime, bits, width, height, fname);
|
||||
},
|
||||
(mime, bits, fname) => {
|
||||
this.props.onAttachFile(mime, bits, fname);
|
||||
},
|
||||
file => { this.props.onAttachImage(file); },
|
||||
file => { this.props.onAttachFile(file); },
|
||||
this.props.onError)) {
|
||||
|
||||
// If a file was pasted, don't paste base64 data into input field.
|
||||
@@ -196,17 +193,17 @@ class SendMessage extends React.PureComponent {
|
||||
formatMessage(messages[this.props.messagePrompt]) :
|
||||
formatMessage(messages.type_new_message));
|
||||
|
||||
const quote =
|
||||
const quote = this.state.quote ?
|
||||
(<div id="reply-quote-preview">
|
||||
<div className="cancel">
|
||||
<a href="#" onClick={(e) => {e.preventDefault(); this.props.onCancelReply();}}><i className="material-icons gray">close</i></a>
|
||||
</div>
|
||||
{this.state.quote}
|
||||
</div>);
|
||||
</div>) : null;
|
||||
const audioEnabled = this.state.audioAvailable && this.props.onAttachAudio;
|
||||
return (
|
||||
<div id="send-message-wrapper">
|
||||
{this.state.quote && !this.props.noInput ? quote : null}
|
||||
{!this.props.noInput ? quote : null}
|
||||
<div id="send-message-panel">
|
||||
{!this.props.disabled ?
|
||||
<>
|
||||
@@ -222,7 +219,7 @@ class SendMessage extends React.PureComponent {
|
||||
:
|
||||
null}
|
||||
{this.props.noInput ?
|
||||
(this.state.quote ? quote : <div className="hr thin" />) :
|
||||
(quote || <div className="hr thin" />) :
|
||||
(this.state.audioRec ?
|
||||
<AudioRecorder
|
||||
onDeleted={_ => this.setState({audioRec: false})}
|
||||
|
||||
@@ -143,7 +143,7 @@ class TopicSecurity extends React.PureComponent {
|
||||
this.props.onShowAlert(
|
||||
formatMessage(messages.report_chat), // title
|
||||
formatMessage(messages.report_chat_warning), // content
|
||||
(() => { this.props.onReportTopic(this.props.topic); }), // onConfirm
|
||||
(_ => { this.props.onReportTopic(this.props.topic); }), // onConfirm
|
||||
null, // "OK"
|
||||
true, // Show Reject button
|
||||
null // "Cancel"
|
||||
|
||||
+2060
-761
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
+4325
-6326
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.19.0-alpha1";
|
||||
const PACKAGE_VERSION = "0.20.0-alpha1";
|
||||
|
||||
Reference in New Issue
Block a user