Calling support
|
@ -853,6 +853,14 @@
|
|||
"message": "To send audio messages, allow Signal Desktop to access your microphone.",
|
||||
"description": "Shown if the user attempts to send an audio message without audio permssions turned on"
|
||||
},
|
||||
"audioCallingPermissionNeeded": {
|
||||
"message": "For calling, you must allow Signal Desktop to access your microphone.",
|
||||
"description": "Shown if the user attempts access the microphone for calling without audio permssions turned on"
|
||||
},
|
||||
"videoCallingPermissionNeeded": {
|
||||
"message": "For video calling, you must allow Signal Desktop to access your camera.",
|
||||
"description": "Shown if the user attempts access the camera for video calling without video permssions turned on"
|
||||
},
|
||||
"allowAccess": {
|
||||
"message": "Allow Access",
|
||||
"description": "Button shown in popup asking to enable microphon/video permissions to send audio messages"
|
||||
|
@ -1101,12 +1109,28 @@
|
|||
"message": "Theme",
|
||||
"description": "Header for theme settings"
|
||||
},
|
||||
"calling": {
|
||||
"message": "Calling",
|
||||
"description": "Header for calling options on the settings screen"
|
||||
},
|
||||
"alwaysRelayCallsDescription": {
|
||||
"message": "Always relay calls",
|
||||
"description": "Description of the always relay calls setting"
|
||||
},
|
||||
"alwaysRelayCallsDetail": {
|
||||
"message": "Relay all calls through the Signal server to avoid revealing your IP address to your contact. Enabling will reduce call quality.",
|
||||
"description": "Details describing the always relay calls setting"
|
||||
},
|
||||
"permissions": {
|
||||
"message": "Permissions",
|
||||
"description": "Header for permissions section of settings"
|
||||
},
|
||||
"mediaPermissionsDescription": {
|
||||
"message": "Allow access to camera and microphone",
|
||||
"message": "Allow access to the microphone",
|
||||
"description": "Description of the media permission description"
|
||||
},
|
||||
"mediaCameraPermissionsDescription": {
|
||||
"message": "Allow access to the camera",
|
||||
"description": "Description of the media permission description"
|
||||
},
|
||||
"general": {
|
||||
|
@ -1539,6 +1563,18 @@
|
|||
"message": "Play audio notification",
|
||||
"description": "Description for audio notification setting"
|
||||
},
|
||||
"callRingtoneNotificationDescription": {
|
||||
"message": "Play calling sounds",
|
||||
"description": "Description for call ringtone notification setting"
|
||||
},
|
||||
"callSystemNotificationDescription": {
|
||||
"message": "Show notifications for calls",
|
||||
"description": "Description for call notification setting"
|
||||
},
|
||||
"incomingCallNotificationDescription": {
|
||||
"message": "Enable incoming calls",
|
||||
"description": "Description for incoming calls setting"
|
||||
},
|
||||
"safetyNumberChanged": {
|
||||
"message": "Safety Number has changed",
|
||||
"description": "A notification shown in the conversation when a contact reinstalls"
|
||||
|
@ -2074,6 +2110,18 @@
|
|||
"message": "Close current conversation",
|
||||
"description": "Shown in the shortcuts guide"
|
||||
},
|
||||
"Keyboard--calling-header": {
|
||||
"message": "Calling",
|
||||
"description": "Header of the keyboard shortcuts guide - calling section"
|
||||
},
|
||||
"Keyboard--toggle-audio": {
|
||||
"message": "Toggle mute on and off",
|
||||
"description": "Shown in the shortcuts guide"
|
||||
},
|
||||
"Keyboard--toggle-video": {
|
||||
"message": "Toggle video on and off",
|
||||
"description": "Shown in the shortcuts guide"
|
||||
},
|
||||
"close-popup": {
|
||||
"message": "Close Popup",
|
||||
"description": "Used as alt text for any button closing a popup"
|
||||
|
@ -2533,5 +2581,87 @@
|
|||
"example": "Jeff Smith"
|
||||
}
|
||||
}
|
||||
},
|
||||
"acceptCall": {
|
||||
"message": "Answer",
|
||||
"description": "Shown in tooltip for the button to accept a call (audio or video)"
|
||||
},
|
||||
"acceptCallWithoutVideo": {
|
||||
"message": "Answer without video",
|
||||
"description": "Shown in tooltip for the button to accept a video call without video"
|
||||
},
|
||||
"declineCall": {
|
||||
"message": "Decline",
|
||||
"description": "Shown in tooltip for the button to decline a call (audio or video)"
|
||||
},
|
||||
"declinedIncomingAudioCall": {
|
||||
"message": "You declined an audio call",
|
||||
"description": "Shown in conversation history when you declined an incoming audio call"
|
||||
},
|
||||
"declinedIncomingVideoCall": {
|
||||
"message": "You declined a video call",
|
||||
"description": "Shown in conversation history when you declined an incoming video call"
|
||||
},
|
||||
"acceptedIncomingAudioCall": {
|
||||
"message": "Incoming audio call",
|
||||
"description": "Shown in conversation history when you accepted an incoming audio call"
|
||||
},
|
||||
"acceptedIncomingVideoCall": {
|
||||
"message": "Incoming video call",
|
||||
"description": "Shown in conversation history when you accepted an incoming video call"
|
||||
},
|
||||
"missedIncomingAudioCall": {
|
||||
"message": "Missed audio call",
|
||||
"description": "Shown in conversation history when you missed an incoming audio call"
|
||||
},
|
||||
"missedIncomingVideoCall": {
|
||||
"message": "Missed video call",
|
||||
"description": "Shown in conversation history when you missed an incoming video call"
|
||||
},
|
||||
"acceptedOutgoingAudioCall": {
|
||||
"message": "Outgoing audio call",
|
||||
"description": "Shown in conversation history when you made an outgoing audio call"
|
||||
},
|
||||
"acceptedOutgoingVideoCall": {
|
||||
"message": "Outgoing video call",
|
||||
"description": "Shown in conversation history when you made an outgoing video call"
|
||||
},
|
||||
"missedOrDeclinedOutgoingAudioCall": {
|
||||
"message": "Unanswered audio call",
|
||||
"description": "Shown in conversation history when your audio call is missed or declined"
|
||||
},
|
||||
"missedOrDeclinedOutgoingVideoCall": {
|
||||
"message": "Unanswered video call",
|
||||
"description": "Shown in conversation history when your video call is missed or declined"
|
||||
},
|
||||
"incomingAudioCall": {
|
||||
"message": "Incoming audio call...",
|
||||
"description": "Shown in both the incoming call bar and notification for an incoming audio call"
|
||||
},
|
||||
"incomingVideoCall": {
|
||||
"message": "Incoming video call...",
|
||||
"description": "Shown in both the incoming call bar and notification for an incoming video call"
|
||||
},
|
||||
"outgoingCallPrering": {
|
||||
"message": "Calling...",
|
||||
"description": "Shown in the call screen when placing an outgoing call that isn't ringing yet"
|
||||
},
|
||||
"outgoingCallRinging": {
|
||||
"message": "Ringing...",
|
||||
"description": "Shown in the call screen when placing an outgoing call that is now ringing"
|
||||
},
|
||||
"callReconnecting": {
|
||||
"message": "Reconnecting...",
|
||||
"description": "Shown in the call screen when the call is reconnecting due to network issues"
|
||||
},
|
||||
"callDuration": {
|
||||
"message": "Signal $duration$",
|
||||
"description": "Shown in the call screen to indicate how long the call has been connected",
|
||||
"placeholders": {
|
||||
"duration": {
|
||||
"content": "$1",
|
||||
"example": "00:01"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ const PERMISSIONS = {
|
|||
notifications: true, // required to show OS notifications for new messages
|
||||
|
||||
// Off by default, can be enabled by user
|
||||
media: false, // required for access to microphone, used for voice notes
|
||||
media: false, // required for access to microphone and camera, used for voice notes and calling
|
||||
|
||||
// Not allowed
|
||||
geolocation: false,
|
||||
|
@ -17,9 +17,21 @@ const PERMISSIONS = {
|
|||
};
|
||||
|
||||
function _createPermissionHandler(userConfig) {
|
||||
return (webContents, permission, callback) => {
|
||||
// We default 'media' permission to false, but the user can override that
|
||||
if (permission === 'media' && userConfig.get('mediaPermissions')) {
|
||||
return (webContents, permission, callback, details) => {
|
||||
// We default 'media' permission to false, but the user can override that for
|
||||
// the microphone and camera.
|
||||
if (
|
||||
permission === 'media' &&
|
||||
details.mediaTypes.includes('audio') &&
|
||||
userConfig.get('mediaPermissions')
|
||||
) {
|
||||
return callback(true);
|
||||
}
|
||||
if (
|
||||
permission === 'media' &&
|
||||
details.mediaTypes.includes('video') &&
|
||||
userConfig.get('mediaCameraPermissions')
|
||||
) {
|
||||
return callback(true);
|
||||
}
|
||||
|
||||
|
|
|
@ -52,22 +52,25 @@
|
|||
</script>
|
||||
|
||||
<script type='text/x-tmpl-mustache' id='two-column'>
|
||||
<div class='gutter'>
|
||||
<div class='left-pane-placeholder'></div>
|
||||
</div>
|
||||
<div class='conversation-stack'>
|
||||
<div class='conversation placeholder'>
|
||||
<div class='conversation-header'></div>
|
||||
<div class='container'>
|
||||
<div class='content'>
|
||||
<div class="module-splash-screen__logo module-img--128 module-logo-blue"></div>
|
||||
<h3>{{ welcomeToSignal }}</h3>
|
||||
<p>{{ selectAContact }}</p>
|
||||
<div class='call-manager-placeholder'></div>
|
||||
<div class='inbox-container'>
|
||||
<div class='gutter'>
|
||||
<div class='left-pane-placeholder'></div>
|
||||
</div>
|
||||
<div class='conversation-stack'>
|
||||
<div class='conversation placeholder'>
|
||||
<div class='conversation-header'></div>
|
||||
<div class='container'>
|
||||
<div class='content'>
|
||||
<div class="module-splash-screen__logo module-img--128 module-logo-blue"></div>
|
||||
<h3>{{ welcomeToSignal }}</h3>
|
||||
<p>{{ selectAContact }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='lightbox-container'></div>
|
||||
</div>
|
||||
<div class='lightbox-container'></div>
|
||||
</script>
|
||||
|
||||
<script type='text/x-tmpl-mustache' id='banner'>
|
||||
|
|
|
@ -10,6 +10,8 @@
|
|||
<true/>
|
||||
<key>com.apple.security.device.microphone</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.camera</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.downloads.read-write</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-write</key>
|
||||
|
|
1
images/icons/v2/mic-off-solid-28.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 28 28"><title>mic-off-solid-28</title><path d="M25.56,3.56l-22,22L2.5,24.5l4.67-4.67A9.22,9.22,0,0,1,5.5,14.5H7a7.74,7.74,0,0,0,1.25,4.25l1.37-1.37A4.9,4.9,0,0,1,9,15V7A5,5,0,0,1,19,7V8l5.5-5.5ZM19,15V12.24l-7.22,7.22A5,5,0,0,0,19,15Zm-5,7a6.62,6.62,0,0,1-3.65-1.11L9.28,22a8.09,8.09,0,0,0,4,1.5V28h1.5V23.46a8.82,8.82,0,0,0,7.75-9H21A7.27,7.27,0,0,1,14,22Z"/></svg>
|
After Width: | Height: | Size: 442 B |
1
images/icons/v2/mic-solid-28.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 28 28"><title>mic-solid-28</title><path d="M14,20h0a5,5,0,0,1-5-5V7a5,5,0,0,1,5-5h0a5,5,0,0,1,5,5v8A5,5,0,0,1,14,20Zm8.5-5.5H21A7.27,7.27,0,0,1,14,22a7.27,7.27,0,0,1-7-7.5H5.5a8.82,8.82,0,0,0,7.75,9V28h1.5V23.46A8.82,8.82,0,0,0,22.5,14.5Z"/></svg>
|
After Width: | Height: | Size: 323 B |
1
images/icons/v2/phone-down-24.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg id="Export" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>phone-down-24</title><path d="M2.51,17a25.59,25.59,0,0,1,4.85-1.39,1.32,1.32,0,0,0,.88-.53,1.34,1.34,0,0,0,.25-1,9.74,9.74,0,0,0-.41-1.61,4.32,4.32,0,0,0-.62-1.11,12.88,12.88,0,0,1,9.07,0,4.61,4.61,0,0,0-.61,1.11A9,9,0,0,0,15.53,14a1.41,1.41,0,0,0,1.11,1.66A26.39,26.39,0,0,1,21.5,17a1.38,1.38,0,0,0,1,0,1.41,1.41,0,0,0,.76-.64,9.88,9.88,0,0,0,.47-1,4.21,4.21,0,0,0-1.31-4.77h0a16.61,16.61,0,0,0-20.83,0l0,0A4.2,4.2,0,0,0,.27,15.34a8.66,8.66,0,0,0,.48,1.08,1.41,1.41,0,0,0,.76.64A1.38,1.38,0,0,0,2.51,17Z"/></svg>
|
After Width: | Height: | Size: 592 B |
1
images/icons/v2/phone-down-28.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 28 28"><title>phone-down-28</title><path d="M26.92,14c-.17-1-.73-2.64-3.62-4A22.94,22.94,0,0,0,4.7,10c-2.89,1.39-3.45,3-3.62,4a4.92,4.92,0,0,0,.23,2.61A2.2,2.2,0,0,0,3.79,18.1l4.12-.73a2.18,2.18,0,0,0,1.82-2.22c0-.56,0-.93-.06-1.4a1.12,1.12,0,0,1,.9-1.21,23.65,23.65,0,0,1,6.86,0,1.12,1.12,0,0,1,.9,1.21c0,.47,0,.84-.06,1.4a2.18,2.18,0,0,0,1.82,2.22l4.12.73a2.2,2.2,0,0,0,2.48-1.49A4.92,4.92,0,0,0,26.92,14Z"/></svg>
|
After Width: | Height: | Size: 492 B |
1
images/icons/v2/phone-right-outline-24.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg id="Export" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>phone-right-outline-24</title><path d="M7.34,2.5a.76.76,0,0,1,.41.13A.75.75,0,0,1,8,3L9.47,7a.69.69,0,0,1,0,.45.71.71,0,0,1-.26.38,9.6,9.6,0,0,1-1.34.82l-1.36.71.73,1.35a15.28,15.28,0,0,0,6.05,6l1.35.74.7-1.36a10.4,10.4,0,0,1,.83-1.35.72.72,0,0,1,.25-.21.62.62,0,0,1,.33-.07.73.73,0,0,1,.25,0L21,16a.71.71,0,0,1,.36.28.75.75,0,0,1,.12.44A9.39,9.39,0,0,1,21,19.28a3.27,3.27,0,0,1-3.07,2.22,3.79,3.79,0,0,1-.58-.05A18.45,18.45,0,0,1,2.55,6.68,3.25,3.25,0,0,1,3,4.44,3.3,3.3,0,0,1,4.72,3,9.38,9.38,0,0,1,7.29,2.5Zm0-1.5H7.19a11.19,11.19,0,0,0-3,.6A4.75,4.75,0,0,0,1.08,7,20,20,0,0,0,6.59,17.42a19.9,19.9,0,0,0,10.47,5.5,4.89,4.89,0,0,0,.85.08,4.82,4.82,0,0,0,2.75-.89,4.75,4.75,0,0,0,1.73-2.32,10.89,10.89,0,0,0,.6-3,2.22,2.22,0,0,0-1.46-2.23l-4-1.46a2.22,2.22,0,0,0-2.55.77,12.73,12.73,0,0,0-1,1.54A13.8,13.8,0,0,1,8.57,10a11.65,11.65,0,0,0,1.54-1,2.17,2.17,0,0,0,.81-1.14,2.23,2.23,0,0,0,0-1.4l-1.46-4A2.35,2.35,0,0,0,8.61,1.4,2.22,2.22,0,0,0,7.34,1Z"/></svg>
|
After Width: | Height: | Size: 1 KiB |
1
images/icons/v2/phone-right-solid-24.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><title>phone-right-solid-24</title><path d="M21.11,16.608a26.24,26.24,0,0,1-4.513-2.5,1.384,1.384,0,0,0-1.918.284,9.116,9.116,0,0,0-.866,1.465,4.5,4.5,0,0,0-.357,1.242A13.113,13.113,0,0,1,6.9,10.542a4.474,4.474,0,0,0,1.243-.355,9.019,9.019,0,0,0,1.343-.779,1.444,1.444,0,0,0,.4-2A26.2,26.2,0,0,1,7.357,2.9a1.42,1.42,0,0,0-1.71-.825,8.63,8.63,0,0,0-1.1.421,4.284,4.284,0,0,0-2.5,4.392l-.014,0A16.948,16.948,0,0,0,17.073,21.953l0-.016a4.308,4.308,0,0,0,4.441-2.492,8.732,8.732,0,0,0,.431-1.13A1.42,1.42,0,0,0,21.11,16.608Z"/></svg>
|
After Width: | Height: | Size: 612 B |
1
images/icons/v2/video-off-solid-24.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg id="Export" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>video-off-solid-24</title><path d="M23,8.06v7.88a1,1,0,0,1-1.42.91L18,15.19V8.81l3.58-1.66A1,1,0,0,1,23,8.06Zm-2.94-5L19,2,15.35,5.65A3,3,0,0,0,13.5,5H4A3,3,0,0,0,1,8v8a3,3,0,0,0,1.45,2.55L.5,20.5l1.06,1.06L4.12,19,16.27,6.85ZM13.5,19a3,3,0,0,0,3-3V8.74L6.24,19Z"/></svg>
|
After Width: | Height: | Size: 350 B |
1
images/icons/v2/video-off-solid-28.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 28 28"><title>video-off-solid-28</title><path d="M22,12l3.29-3.29a1,1,0,0,1,1.71.7v9.18a1,1,0,0,1-1.71.7L22,16Zm2.5-9.5L19.85,7.15A3,3,0,0,0,17.5,6H6A3,3,0,0,0,3,9V19a3,3,0,0,0,2.14,2.86L2.5,24.5l1.06,1.06,22-22ZM9.24,22H17.5a3,3,0,0,0,3-3V10.74Z"/></svg>
|
After Width: | Height: | Size: 331 B |
1
images/icons/v2/video-outline-24.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><title>video-outline-24</title><path d="M22,6.56a1.06,1.06,0,0,0-.42.09L18,8.31V6.5a3,3,0,0,0-3-3H4a3,3,0,0,0-3,3v11a3,3,0,0,0,3,3H15a3,3,0,0,0,3-3V15.69l3.58,1.66a1.06,1.06,0,0,0,.42.09,1,1,0,0,0,1-1V7.56A1,1,0,0,0,22,6.56ZM16.5,17.5A1.5,1.5,0,0,1,15,19H4a1.5,1.5,0,0,1-1.5-1.5V6.5A1.5,1.5,0,0,1,4,5H15a1.5,1.5,0,0,1,1.5,1.5Zm5-9.16V16l-1-.81L18,14V10l2.48-1.15,1-.81Z"/></svg>
|
After Width: | Height: | Size: 461 B |
1
images/icons/v2/video-solid-24.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><title>video-solid-24</title><path d="M23,8.06v7.88a1,1,0,0,1-1.42.91L18,15.2V8.8l3.58-1.65A1,1,0,0,1,23,8.06ZM16.5,17V7a3,3,0,0,0-3-3H4A3,3,0,0,0,1,7V17a3,3,0,0,0,3,3h9.5A3,3,0,0,0,16.5,17Z"/></svg>
|
After Width: | Height: | Size: 282 B |
1
images/icons/v2/video-solid-28.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 28 28"><title>video-solid-28</title><path d="M22,12l3.29-3.29a1,1,0,0,1,1.71.7v9.18a1,1,0,0,1-1.71.7L22,16Zm-1.5,7V9a3,3,0,0,0-3-3H6A3,3,0,0,0,3,9V19a3,3,0,0,0,3,3H17.5A3,3,0,0,0,20.5,19Z"/></svg>
|
After Width: | Height: | Size: 272 B |
|
@ -367,12 +367,27 @@
|
|||
storage.put('notification-setting', value),
|
||||
getAudioNotification: () => storage.get('audio-notification'),
|
||||
setAudioNotification: value => storage.put('audio-notification', value),
|
||||
getCallRingtoneNotification: () =>
|
||||
storage.get('call-ringtone-notification', true),
|
||||
setCallRingtoneNotification: value =>
|
||||
storage.put('call-ringtone-notification', value),
|
||||
getCallSystemNotification: () =>
|
||||
storage.get('call-system-notification', true),
|
||||
setCallSystemNotification: value =>
|
||||
storage.put('call-system-notification', value),
|
||||
getIncomingCallNotification: () =>
|
||||
storage.get('incoming-call-notification', true),
|
||||
setIncomingCallNotification: value =>
|
||||
storage.put('incoming-call-notification', value),
|
||||
|
||||
getSpellCheck: () => storage.get('spell-check', true),
|
||||
setSpellCheck: value => {
|
||||
storage.put('spell-check', value);
|
||||
},
|
||||
|
||||
getAlwaysRelayCalls: () => storage.get('always-relay-calls'),
|
||||
setAlwaysRelayCalls: value => storage.put('always-relay-calls', value),
|
||||
|
||||
// eslint-disable-next-line eqeqeq
|
||||
isPrimary: () => textsecure.storage.user.getDeviceId() == '1',
|
||||
getSyncRequest: () =>
|
||||
|
@ -586,6 +601,7 @@
|
|||
window.reduxActions.updates,
|
||||
window.Whisper.events
|
||||
);
|
||||
window.Signal.Services.calling.initialize(window.reduxActions.calling);
|
||||
window.reduxActions.expiration.hydrateExpirationStatus(
|
||||
window.Signal.Util.hasExpired()
|
||||
);
|
||||
|
@ -638,6 +654,10 @@
|
|||
|
||||
// Binding these actions to our redux store and exposing them allows us to update
|
||||
// redux when things change in the backbone world.
|
||||
actions.calling = Signal.State.bindActionCreators(
|
||||
Signal.State.Ducks.calling.actions,
|
||||
store.dispatch
|
||||
);
|
||||
actions.conversations = Signal.State.bindActionCreators(
|
||||
Signal.State.Ducks.conversations.actions,
|
||||
store.dispatch
|
||||
|
|
|
@ -1033,6 +1033,30 @@
|
|||
}
|
||||
},
|
||||
|
||||
async addCallHistory(callHistoryDetails) {
|
||||
const { acceptedTime, endedTime, wasDeclined } = callHistoryDetails;
|
||||
const message = {
|
||||
conversationId: this.id,
|
||||
type: 'call-history',
|
||||
sent_at: endedTime,
|
||||
received_at: endedTime,
|
||||
unread: !wasDeclined && !acceptedTime,
|
||||
callHistoryDetails,
|
||||
};
|
||||
|
||||
const id = await window.Signal.Data.saveMessage(message, {
|
||||
Message: Whisper.Message,
|
||||
});
|
||||
const model = MessageController.register(
|
||||
id,
|
||||
new Whisper.Message({
|
||||
...message,
|
||||
id,
|
||||
})
|
||||
);
|
||||
this.trigger('newmessage', model);
|
||||
},
|
||||
|
||||
async onReadMessage(message, readAt) {
|
||||
// We mark as read everything older than this message - to clean up old stuff
|
||||
// still marked unread in the database. If the user generally doesn't read in
|
||||
|
|
|
@ -178,6 +178,11 @@
|
|||
type: 'resetSessionNotification',
|
||||
data: this.getPropsForResetSessionNotification(),
|
||||
};
|
||||
} else if (this.isCallHistory()) {
|
||||
return {
|
||||
type: 'callHistory',
|
||||
data: this.getPropsForCallHistory(),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -366,6 +371,9 @@
|
|||
// eslint-disable-next-line no-bitwise
|
||||
return !!(this.get('flags') & flag);
|
||||
},
|
||||
isCallHistory() {
|
||||
return this.get('type') === 'call-history';
|
||||
},
|
||||
|
||||
// Props for each message type
|
||||
getPropsForUnsupportedMessage() {
|
||||
|
@ -500,6 +508,11 @@
|
|||
// It doesn't need anything right now!
|
||||
return {};
|
||||
},
|
||||
getPropsForCallHistory() {
|
||||
return {
|
||||
callHistoryDetails: this.get('callHistoryDetails'),
|
||||
};
|
||||
},
|
||||
getAttachmentsForMessage() {
|
||||
const sticker = this.get('sticker');
|
||||
if (sticker && sticker.data) {
|
||||
|
|
|
@ -49,6 +49,7 @@ const { createTimeline } = require('../../ts/state/roots/createTimeline');
|
|||
const {
|
||||
createCompositionArea,
|
||||
} = require('../../ts/state/roots/createCompositionArea');
|
||||
const { createCallManager } = require('../../ts/state/roots/createCallManager');
|
||||
const { createLeftPane } = require('../../ts/state/roots/createLeftPane');
|
||||
const {
|
||||
createStickerManager,
|
||||
|
@ -61,6 +62,7 @@ const {
|
|||
} = require('../../ts/state/roots/createShortcutGuideModal');
|
||||
|
||||
const { createStore } = require('../../ts/state/createStore');
|
||||
const callingDuck = require('../../ts/state/ducks/calling');
|
||||
const conversationsDuck = require('../../ts/state/ducks/conversations');
|
||||
const emojisDuck = require('../../ts/state/ducks/emojis');
|
||||
const expirationDuck = require('../../ts/state/ducks/expiration');
|
||||
|
@ -100,6 +102,8 @@ const {
|
|||
const {
|
||||
initializeUpdateListener,
|
||||
} = require('../../ts/services/updateListener');
|
||||
const { notify } = require('../../ts/services/notify');
|
||||
const { calling } = require('../../ts/services/calling');
|
||||
|
||||
function initializeMigrations({
|
||||
userDataPath,
|
||||
|
@ -277,6 +281,7 @@ exports.setup = (options = {}) => {
|
|||
};
|
||||
|
||||
const Roots = {
|
||||
createCallManager,
|
||||
createCompositionArea,
|
||||
createLeftPane,
|
||||
createShortcutGuideModal,
|
||||
|
@ -286,6 +291,7 @@ exports.setup = (options = {}) => {
|
|||
};
|
||||
|
||||
const Ducks = {
|
||||
calling: callingDuck,
|
||||
conversations: conversationsDuck,
|
||||
emojis: emojisDuck,
|
||||
expiration: expirationDuck,
|
||||
|
@ -305,6 +311,8 @@ exports.setup = (options = {}) => {
|
|||
const Services = {
|
||||
initializeNetworkObserver,
|
||||
initializeUpdateListener,
|
||||
notify,
|
||||
calling,
|
||||
};
|
||||
|
||||
const State = {
|
||||
|
|
|
@ -21,15 +21,6 @@
|
|||
MESSAGE: 'message',
|
||||
};
|
||||
|
||||
function filter(text) {
|
||||
return (text || '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
|
||||
Whisper.Notifications = new (Backbone.Collection.extend({
|
||||
initialize() {
|
||||
this.isEnabled = false;
|
||||
|
@ -164,13 +155,16 @@
|
|||
|
||||
drawAttention();
|
||||
|
||||
this.lastNotification = new Notification(title, {
|
||||
body: window.platform === 'linux' ? filter(message) : message,
|
||||
this.lastNotification = window.Signal.Services.notify({
|
||||
platform: window.platform,
|
||||
title,
|
||||
icon: iconUrl,
|
||||
message,
|
||||
silent: !status.shouldPlayNotificationSound,
|
||||
onNotificationClick: () => {
|
||||
this.trigger('click', last.conversationId, last.messageId);
|
||||
},
|
||||
});
|
||||
this.lastNotification.onclick = () =>
|
||||
this.trigger('click', last.conversationId, last.messageId);
|
||||
|
||||
// We continue to build up more and more messages for our notifications
|
||||
// until the user comes back to our app or closes the app. Then we’ll
|
||||
|
|
|
@ -27,13 +27,28 @@ window.subscribeToSystemThemeChange(() => {
|
|||
applyTheme();
|
||||
});
|
||||
|
||||
let message;
|
||||
if (window.forCalling) {
|
||||
if (window.forCamera) {
|
||||
message = i18n('videoCallingPermissionNeeded');
|
||||
} else {
|
||||
message = i18n('audioCallingPermissionNeeded');
|
||||
}
|
||||
} else {
|
||||
message = i18n('audioPermissionNeeded');
|
||||
}
|
||||
|
||||
window.view = new Whisper.ConfirmationDialogView({
|
||||
message: i18n('audioPermissionNeeded'),
|
||||
message,
|
||||
okText: i18n('allowAccess'),
|
||||
resolve: () => {
|
||||
'use strict';
|
||||
|
||||
window.setMediaPermissions(true);
|
||||
if (!window.forCamera) {
|
||||
window.setMediaPermissions(true);
|
||||
} else {
|
||||
window.setMediaCameraPermissions(true);
|
||||
}
|
||||
window.closePermissionsPopup();
|
||||
},
|
||||
reject: window.closePermissionsPopup,
|
||||
|
|
|
@ -39,7 +39,13 @@ const getInitialData = async () => ({
|
|||
|
||||
spellCheck: await window.getSpellCheck(),
|
||||
|
||||
incomingCallNotification: await window.getIncomingCallNotification(),
|
||||
callRingtoneNotification: await window.getCallRingtoneNotification(),
|
||||
callSystemNotification: await window.getCallSystemNotification(),
|
||||
alwaysRelayCalls: await window.getAlwaysRelayCalls(),
|
||||
|
||||
mediaPermissions: await window.getMediaPermissions(),
|
||||
mediaCameraPermissions: await window.getMediaCameraPermissions(),
|
||||
|
||||
isPrimary: await window.isPrimary(),
|
||||
lastSyncTime: await window.getLastSyncTime(),
|
||||
|
|
|
@ -395,6 +395,22 @@
|
|||
|
||||
// These are view only and don't update the Conversation model, so they
|
||||
// need a manual update call.
|
||||
onOutgoingAudioCallInConversation: async () => {
|
||||
const conversation = this.model;
|
||||
const isVideoCall = false;
|
||||
await window.Signal.Services.calling.startOutgoingCall(
|
||||
conversation,
|
||||
isVideoCall
|
||||
);
|
||||
},
|
||||
onOutgoingVideoCallInConversation: async () => {
|
||||
const conversation = this.model;
|
||||
const isVideoCall = true;
|
||||
await window.Signal.Services.calling.startOutgoingCall(
|
||||
conversation,
|
||||
isVideoCall
|
||||
);
|
||||
},
|
||||
onShowSafetyNumber: () => {
|
||||
this.showSafetyNumber();
|
||||
},
|
||||
|
|
|
@ -91,6 +91,7 @@
|
|||
this.startConnectionListener();
|
||||
} else {
|
||||
this.setupLeftPane();
|
||||
this.setupCallManagerUI();
|
||||
}
|
||||
|
||||
Whisper.events.on('pack-install-failed', () => {
|
||||
|
@ -106,6 +107,19 @@
|
|||
events: {
|
||||
click: 'onClick',
|
||||
},
|
||||
setupCallManagerUI() {
|
||||
if (!window.CALLING) {
|
||||
return;
|
||||
}
|
||||
if (this.callManagerView) {
|
||||
return;
|
||||
}
|
||||
this.callManagerView = new Whisper.ReactWrapperView({
|
||||
className: 'call-manager-wrapper',
|
||||
JSX: Signal.State.Roots.createCallManager(window.reduxStore),
|
||||
});
|
||||
this.$('.call-manager-placeholder').append(this.callManagerView.el);
|
||||
},
|
||||
setupLeftPane() {
|
||||
if (this.leftPaneView) {
|
||||
return;
|
||||
|
@ -144,6 +158,7 @@
|
|||
},
|
||||
onEmpty() {
|
||||
this.setupLeftPane();
|
||||
this.setupCallManagerUI();
|
||||
|
||||
const view = this.appLoadingScreen;
|
||||
if (view) {
|
||||
|
|
|
@ -50,6 +50,25 @@
|
|||
},
|
||||
});
|
||||
|
||||
const MediaCameraPermissionsSettingView = Whisper.View.extend({
|
||||
initialize(options) {
|
||||
this.value = options.value;
|
||||
this.setFn = options.setFn;
|
||||
this.populate();
|
||||
},
|
||||
events: {
|
||||
change: 'change',
|
||||
},
|
||||
change(e) {
|
||||
this.value = e.target.checked;
|
||||
this.setFn(this.value);
|
||||
window.log.info('media-camera-permissions changed to', this.value);
|
||||
},
|
||||
populate() {
|
||||
this.$('input').prop('checked', Boolean(this.value));
|
||||
},
|
||||
});
|
||||
|
||||
const RadioButtonGroupView = Whisper.View.extend({
|
||||
initialize(options) {
|
||||
this.name = options.name;
|
||||
|
@ -126,11 +145,40 @@
|
|||
setFn: window.setHideMenuBar,
|
||||
});
|
||||
}
|
||||
new CheckboxView({
|
||||
el: this.$('.always-relay-calls-setting'),
|
||||
name: 'always-relay-calls-setting',
|
||||
value: window.initialData.alwaysRelayCalls,
|
||||
setFn: window.setAlwaysRelayCalls,
|
||||
});
|
||||
new CheckboxView({
|
||||
el: this.$('.call-ringtone-notification-setting'),
|
||||
name: 'call-ringtone-notification-setting',
|
||||
value: window.initialData.callRingtoneNotification,
|
||||
setFn: window.setCallRingtoneNotification,
|
||||
});
|
||||
new CheckboxView({
|
||||
el: this.$('.call-system-notification-setting'),
|
||||
name: 'call-system-notification-setting',
|
||||
value: window.initialData.callSystemNotification,
|
||||
setFn: window.setCallSystemNotification,
|
||||
});
|
||||
new CheckboxView({
|
||||
el: this.$('.incoming-call-notification-setting'),
|
||||
name: 'incoming-call-notification-setting',
|
||||
value: window.initialData.incomingCallNotification,
|
||||
setFn: window.setIncomingCallNotification,
|
||||
});
|
||||
new MediaPermissionsSettingView({
|
||||
el: this.$('.media-permissions'),
|
||||
value: window.initialData.mediaPermissions,
|
||||
setFn: window.setMediaPermissions,
|
||||
});
|
||||
new MediaCameraPermissionsSettingView({
|
||||
el: this.$('.media-camera-permissions'),
|
||||
value: window.initialData.mediaCameraPermissions,
|
||||
setFn: window.setMediaCameraPermissions,
|
||||
});
|
||||
if (!window.initialData.isPrimary) {
|
||||
const syncView = new SyncView().render();
|
||||
this.$('.sync-setting').append(syncView.el);
|
||||
|
@ -167,8 +215,23 @@
|
|||
clearDataHeader: i18n('clearDataHeader'),
|
||||
clearDataButton: i18n('clearDataButton'),
|
||||
clearDataExplanation: i18n('clearDataExplanation'),
|
||||
calling: i18n('calling'),
|
||||
alwaysRelayCallsDescription: i18n('alwaysRelayCallsDescription'),
|
||||
alwaysRelayCallsDetail: i18n('alwaysRelayCallsDetail'),
|
||||
callRingtoneNotificationDescription: i18n(
|
||||
'callRingtoneNotificationDescription'
|
||||
),
|
||||
callSystemNotificationDescription: i18n(
|
||||
'callSystemNotificationDescription'
|
||||
),
|
||||
incomingCallNotificationDescription: i18n(
|
||||
'incomingCallNotificationDescription'
|
||||
),
|
||||
permissions: i18n('permissions'),
|
||||
mediaPermissionsDescription: i18n('mediaPermissionsDescription'),
|
||||
mediaCameraPermissionsDescription: i18n(
|
||||
'mediaCameraPermissionsDescription'
|
||||
),
|
||||
generalHeader: i18n('general'),
|
||||
spellCheckDescription: i18n('spellCheckDescription'),
|
||||
spellCheckHidden: spellCheckDirty ? 'false' : 'true',
|
||||
|
|
133
main.js
|
@ -45,7 +45,8 @@ app.setAppUserModelId(appUserModelId);
|
|||
|
||||
// We don't navigate, but this is the way of the future
|
||||
// https://github.com/electron/electron/issues/18397
|
||||
app.allowRendererProcessReuse = true;
|
||||
// TODO: Make ringrtc-node context-aware and change this to true.
|
||||
app.allowRendererProcessReuse = false;
|
||||
|
||||
// Keep a global reference of the window object, if you don't, the window will
|
||||
// be closed automatically when the JavaScript object is garbage collected.
|
||||
|
@ -83,6 +84,7 @@ const development =
|
|||
// data directory has been set.
|
||||
const attachments = require('./app/attachments');
|
||||
const attachmentChannel = require('./app/attachment_channel');
|
||||
const bounce = require('./ts/services/bounce');
|
||||
const updater = require('./ts/updater/index');
|
||||
const createTrayIcon = require('./app/tray_icon');
|
||||
const dockIcon = require('./app/dock_icon');
|
||||
|
@ -384,6 +386,9 @@ async function createWindow() {
|
|||
|
||||
handleCommonWindowEvents(mainWindow);
|
||||
|
||||
// App dock icon bounce
|
||||
bounce.init(mainWindow);
|
||||
|
||||
// Emitted when the window is about to be closed.
|
||||
// Note: We do most of our shutdown logic here because all windows are closed by
|
||||
// Electron before the app quits.
|
||||
|
@ -789,52 +794,60 @@ async function showDebugLogWindow() {
|
|||
}
|
||||
|
||||
let permissionsPopupWindow;
|
||||
async function showPermissionsPopupWindow() {
|
||||
if (permissionsPopupWindow) {
|
||||
permissionsPopupWindow.show();
|
||||
return;
|
||||
}
|
||||
if (!mainWindow) {
|
||||
return;
|
||||
}
|
||||
function showPermissionsPopupWindow(forCalling, forCamera) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
if (permissionsPopupWindow) {
|
||||
permissionsPopupWindow.show();
|
||||
reject(new Error('Permission window already showing'));
|
||||
}
|
||||
if (!mainWindow) {
|
||||
reject(new Error('No main window'));
|
||||
}
|
||||
|
||||
const theme = await pify(getDataFromMainWindow)('theme-setting');
|
||||
const size = mainWindow.getSize();
|
||||
const options = {
|
||||
width: Math.min(400, size[0]),
|
||||
height: Math.min(150, size[1]),
|
||||
resizable: false,
|
||||
title: locale.messages.allowAccess.message,
|
||||
autoHideMenuBar: true,
|
||||
backgroundColor: '#3a76f0',
|
||||
show: false,
|
||||
modal: true,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
nodeIntegrationInWorker: false,
|
||||
contextIsolation: false,
|
||||
preload: path.join(__dirname, 'permissions_popup_preload.js'),
|
||||
nativeWindowOpen: true,
|
||||
},
|
||||
parent: mainWindow,
|
||||
};
|
||||
const theme = await pify(getDataFromMainWindow)('theme-setting');
|
||||
const size = mainWindow.getSize();
|
||||
const options = {
|
||||
width: Math.min(400, size[0]),
|
||||
height: Math.min(150, size[1]),
|
||||
resizable: false,
|
||||
title: locale.messages.allowAccess.message,
|
||||
autoHideMenuBar: true,
|
||||
backgroundColor: '#3a76f0',
|
||||
show: false,
|
||||
modal: true,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
nodeIntegrationInWorker: false,
|
||||
contextIsolation: false,
|
||||
preload: path.join(__dirname, 'permissions_popup_preload.js'),
|
||||
nativeWindowOpen: true,
|
||||
},
|
||||
parent: mainWindow,
|
||||
};
|
||||
|
||||
permissionsPopupWindow = new BrowserWindow(options);
|
||||
permissionsPopupWindow = new BrowserWindow(options);
|
||||
|
||||
handleCommonWindowEvents(permissionsPopupWindow);
|
||||
handleCommonWindowEvents(permissionsPopupWindow);
|
||||
|
||||
permissionsPopupWindow.loadURL(
|
||||
prepareURL([__dirname, 'permissions_popup.html'], { theme })
|
||||
);
|
||||
permissionsPopupWindow.loadURL(
|
||||
prepareURL([__dirname, 'permissions_popup.html'], {
|
||||
theme,
|
||||
forCalling,
|
||||
forCamera,
|
||||
})
|
||||
);
|
||||
|
||||
permissionsPopupWindow.on('closed', () => {
|
||||
removeDarkOverlay();
|
||||
permissionsPopupWindow = null;
|
||||
});
|
||||
permissionsPopupWindow.on('closed', () => {
|
||||
removeDarkOverlay();
|
||||
permissionsPopupWindow = null;
|
||||
|
||||
permissionsPopupWindow.once('ready-to-show', () => {
|
||||
addDarkOverlay();
|
||||
permissionsPopupWindow.show();
|
||||
resolve();
|
||||
});
|
||||
|
||||
permissionsPopupWindow.once('ready-to-show', () => {
|
||||
addDarkOverlay();
|
||||
permissionsPopupWindow.show();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1203,7 +1216,16 @@ ipc.on('close-debug-log', () => {
|
|||
|
||||
// Permissions Popup-related IPC calls
|
||||
|
||||
ipc.on('show-permissions-popup', showPermissionsPopupWindow);
|
||||
ipc.on('show-permissions-popup', () => {
|
||||
showPermissionsPopupWindow(false, false);
|
||||
});
|
||||
ipc.handle('show-calling-permissions-popup', async (event, forCamera) => {
|
||||
try {
|
||||
await showPermissionsPopupWindow(true, forCamera);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
ipc.on('close-permissions-popup', () => {
|
||||
if (permissionsPopupWindow) {
|
||||
permissionsPopupWindow.close();
|
||||
|
@ -1245,7 +1267,17 @@ installSettingsSetter('audio-notification');
|
|||
installSettingsGetter('spell-check');
|
||||
installSettingsSetter('spell-check');
|
||||
|
||||
// This one is different because its single source of truth is userConfig, not IndexedDB
|
||||
installSettingsGetter('always-relay-calls');
|
||||
installSettingsSetter('always-relay-calls');
|
||||
installSettingsGetter('call-ringtone-notification');
|
||||
installSettingsSetter('call-ringtone-notification');
|
||||
installSettingsGetter('call-system-notification');
|
||||
installSettingsSetter('call-system-notification');
|
||||
installSettingsGetter('incoming-call-notification');
|
||||
installSettingsSetter('incoming-call-notification');
|
||||
|
||||
// These ones are different because its single source of truth is userConfig,
|
||||
// not IndexedDB
|
||||
ipc.on('get-media-permissions', event => {
|
||||
event.sender.send(
|
||||
'get-success-media-permissions',
|
||||
|
@ -1253,6 +1285,13 @@ ipc.on('get-media-permissions', event => {
|
|||
userConfig.get('mediaPermissions') || false
|
||||
);
|
||||
});
|
||||
ipc.on('get-media-camera-permissions', event => {
|
||||
event.sender.send(
|
||||
'get-success-media-camera-permissions',
|
||||
null,
|
||||
userConfig.get('mediaCameraPermissions') || false
|
||||
);
|
||||
});
|
||||
ipc.on('set-media-permissions', (event, value) => {
|
||||
userConfig.set('mediaPermissions', value);
|
||||
|
||||
|
@ -1261,6 +1300,14 @@ ipc.on('set-media-permissions', (event, value) => {
|
|||
|
||||
event.sender.send('set-success-media-permissions', null);
|
||||
});
|
||||
ipc.on('set-media-camera-permissions', (event, value) => {
|
||||
userConfig.set('mediaCameraPermissions', value);
|
||||
|
||||
// We reinstall permissions handler to ensure that a revoked permission takes effect
|
||||
installPermissionsHandler({ session, userConfig });
|
||||
|
||||
event.sender.send('set-success-media-camera-permissions', null);
|
||||
});
|
||||
|
||||
installSettingsGetter('is-primary');
|
||||
installSettingsGetter('sync-request');
|
||||
|
|
|
@ -122,6 +122,7 @@
|
|||
"react-redux": "7.1.0",
|
||||
"react-router-dom": "5.0.1",
|
||||
"react-sortable-hoc": "1.9.1",
|
||||
"react-tooltip-lite": "1.12.0",
|
||||
"react-virtualized": "9.21.0",
|
||||
"read-last-lines": "1.3.0",
|
||||
"redux": "4.0.1",
|
||||
|
@ -130,6 +131,7 @@
|
|||
"redux-ts-utils": "3.2.2",
|
||||
"reselect": "4.0.0",
|
||||
"rimraf": "2.6.2",
|
||||
"ringrtc": "https://github.com/signalapp/signal-ringrtc-node.git#95afcf7effb0b34e2cdcf3df40bc9519324db8cd",
|
||||
"sanitize-filename": "1.6.3",
|
||||
"sanitize.css": "11.0.0",
|
||||
"semver": "5.4.1",
|
||||
|
@ -367,7 +369,8 @@
|
|||
"main.js",
|
||||
"images/**",
|
||||
"fonts/**",
|
||||
"build/assets",
|
||||
"sounds/*",
|
||||
"build/icons",
|
||||
"node_modules/**",
|
||||
"sticker-creator/preload.js",
|
||||
"sticker-creator/dist/**",
|
||||
|
@ -394,7 +397,8 @@
|
|||
"node_modules/sharp/build/**",
|
||||
"!node_modules/@journeyapps/sqlcipher/deps/*",
|
||||
"!node_modules/@journeyapps/sqlcipher/build/*",
|
||||
"!node_modules/@journeyapps/sqlcipher/lib/binding/node-*"
|
||||
"!node_modules/@journeyapps/sqlcipher/lib/binding/node-*",
|
||||
"node_modules/ringrtc/build/**"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,8 @@ window.getEnvironment = () => config.environment;
|
|||
window.getVersion = () => config.version;
|
||||
window.theme = config.theme;
|
||||
window.i18n = i18n.setup(locale, localeMessages);
|
||||
window.forCalling = config.forCalling === 'true';
|
||||
window.forCamera = config.forCamera === 'true';
|
||||
|
||||
function setSystemTheme() {
|
||||
window.systemTheme = nativeTheme.shouldUseDarkColors ? 'dark' : 'light';
|
||||
|
@ -34,7 +36,7 @@ require('./js/logging');
|
|||
window.closePermissionsPopup = () =>
|
||||
ipcRenderer.send('close-permissions-popup');
|
||||
|
||||
window.getMediaPermissions = makeGetter('media-permissions');
|
||||
window.setMediaPermissions = makeSetter('media-permissions');
|
||||
window.setMediaCameraPermissions = makeSetter('media-camera-permissions');
|
||||
window.getThemeSetting = makeGetter('theme-setting');
|
||||
window.setThemeSetting = makeSetter('theme-setting');
|
||||
|
|
89
preload.js
|
@ -13,6 +13,9 @@ try {
|
|||
const { app } = remote;
|
||||
const { nativeTheme } = remote.require('electron');
|
||||
|
||||
// Enable calling
|
||||
window.CALLING = true;
|
||||
|
||||
window.PROTO_ROOT = 'protos';
|
||||
const config = require('url').parse(window.location.toString(), true).query;
|
||||
|
||||
|
@ -113,6 +116,8 @@ try {
|
|||
|
||||
window.showSettings = () => ipc.send('show-settings');
|
||||
window.showPermissionsPopup = () => ipc.send('show-permissions-popup');
|
||||
window.showCallingPermissionsPopup = forCamera =>
|
||||
ipc.invoke('show-calling-permissions-popup', forCamera);
|
||||
|
||||
ipc.on('show-keyboard-shortcuts', () => {
|
||||
window.Events.showKeyboardShortcuts();
|
||||
|
@ -139,6 +144,75 @@ try {
|
|||
installGetter('spell-check', 'getSpellCheck');
|
||||
installSetter('spell-check', 'setSpellCheck');
|
||||
|
||||
installGetter('always-relay-calls', 'getAlwaysRelayCalls');
|
||||
installSetter('always-relay-calls', 'setAlwaysRelayCalls');
|
||||
|
||||
installGetter('call-ringtone-notification', 'getCallRingtoneNotification');
|
||||
installSetter('call-ringtone-notification', 'setCallRingtoneNotification');
|
||||
|
||||
window.getCallRingtoneNotification = () =>
|
||||
new Promise((resolve, reject) => {
|
||||
ipc.once(
|
||||
'get-success-call-ringtone-notification',
|
||||
(_event, error, value) => {
|
||||
if (error) {
|
||||
return reject(new Error(error));
|
||||
}
|
||||
|
||||
return resolve(value);
|
||||
}
|
||||
);
|
||||
ipc.send('get-call-ringtone-notification');
|
||||
});
|
||||
|
||||
installGetter('call-system-notification', 'getCallSystemNotification');
|
||||
installSetter('call-system-notification', 'setCallSystemNotification');
|
||||
|
||||
window.getCallSystemNotification = () =>
|
||||
new Promise((resolve, reject) => {
|
||||
ipc.once(
|
||||
'get-success-call-system-notification',
|
||||
(_event, error, value) => {
|
||||
if (error) {
|
||||
return reject(new Error(error));
|
||||
}
|
||||
|
||||
return resolve(value);
|
||||
}
|
||||
);
|
||||
ipc.send('get-call-system-notification');
|
||||
});
|
||||
|
||||
installGetter('incoming-call-notification', 'getIncomingCallNotification');
|
||||
installSetter('incoming-call-notification', 'setIncomingCallNotification');
|
||||
|
||||
window.getIncomingCallNotification = () =>
|
||||
new Promise((resolve, reject) => {
|
||||
ipc.once(
|
||||
'get-success-incoming-call-notification',
|
||||
(_event, error, value) => {
|
||||
if (error) {
|
||||
return reject(new Error(error));
|
||||
}
|
||||
|
||||
return resolve(value);
|
||||
}
|
||||
);
|
||||
ipc.send('get-incoming-call-notification');
|
||||
});
|
||||
|
||||
window.getAlwaysRelayCalls = () =>
|
||||
new Promise((resolve, reject) => {
|
||||
ipc.once('get-success-always-relay-calls', (_event, error, value) => {
|
||||
if (error) {
|
||||
return reject(new Error(error));
|
||||
}
|
||||
|
||||
return resolve(value);
|
||||
});
|
||||
ipc.send('get-always-relay-calls');
|
||||
});
|
||||
|
||||
window.getMediaPermissions = () =>
|
||||
new Promise((resolve, reject) => {
|
||||
ipc.once('get-success-media-permissions', (_event, error, value) => {
|
||||
|
@ -151,6 +225,21 @@ try {
|
|||
ipc.send('get-media-permissions');
|
||||
});
|
||||
|
||||
window.getMediaCameraPermissions = () =>
|
||||
new Promise((resolve, reject) => {
|
||||
ipc.once(
|
||||
'get-success-media-camera-permissions',
|
||||
(_event, error, value) => {
|
||||
if (error) {
|
||||
return reject(new Error(error));
|
||||
}
|
||||
|
||||
return resolve(value);
|
||||
}
|
||||
);
|
||||
ipc.send('get-media-camera-permissions');
|
||||
});
|
||||
|
||||
window.getBuiltInImages = () =>
|
||||
new Promise((resolve, reject) => {
|
||||
ipc.once('get-success-built-in-images', (_event, error, value) => {
|
||||
|
|
|
@ -30,44 +30,64 @@ message Envelope {
|
|||
message Content {
|
||||
optional DataMessage dataMessage = 1;
|
||||
optional SyncMessage syncMessage = 2;
|
||||
optional CallMessage callMessage = 3;
|
||||
optional CallingMessage callingMessage = 3;
|
||||
optional NullMessage nullMessage = 4;
|
||||
optional ReceiptMessage receiptMessage = 5;
|
||||
optional TypingMessage typingMessage = 6;
|
||||
}
|
||||
|
||||
message CallMessage {
|
||||
// Everything in CallingMessage must be kept in sync with RingRTC (ringrtc-node).
|
||||
// Whenever you change this, make sure you change textsecure.d.ts and RingRTC.
|
||||
message CallingMessage {
|
||||
message Offer {
|
||||
optional uint64 id = 1;
|
||||
optional string description = 2;
|
||||
enum Type {
|
||||
OFFER_AUDIO_CALL = 0;
|
||||
OFFER_VIDEO_CALL = 1;
|
||||
OFFER_NEEDS_PERMISSION = 2;
|
||||
}
|
||||
|
||||
optional uint64 callId = 1;
|
||||
optional string sdp = 2;
|
||||
optional Type type = 3;
|
||||
}
|
||||
|
||||
message Answer {
|
||||
optional uint64 id = 1;
|
||||
optional string description = 2;
|
||||
optional uint64 callId = 1;
|
||||
optional string sdp = 2;
|
||||
}
|
||||
|
||||
message IceUpdate {
|
||||
optional uint64 id = 1;
|
||||
optional string sdpMid = 2;
|
||||
optional uint32 sdpMLineIndex = 3;
|
||||
optional string sdp = 4;
|
||||
message IceCandidate {
|
||||
optional uint64 callId = 1;
|
||||
optional string mid = 2;
|
||||
optional uint32 midIndex = 3;
|
||||
optional string sdp = 4;
|
||||
}
|
||||
|
||||
message Busy {
|
||||
optional uint64 id = 1;
|
||||
optional uint64 callId = 1;
|
||||
}
|
||||
|
||||
message Hangup {
|
||||
optional uint64 id = 1;
|
||||
enum Type {
|
||||
HANGUP_NORMAL = 0;
|
||||
HANGUP_ACCEPTED = 1;
|
||||
HANGUP_DECLINED = 2;
|
||||
HANGUP_BUSY = 3;
|
||||
}
|
||||
|
||||
optional uint64 callId = 1;
|
||||
optional Type type = 2;
|
||||
optional uint32 deviceId = 3;
|
||||
}
|
||||
|
||||
|
||||
optional Offer offer = 1;
|
||||
optional Answer answer = 2;
|
||||
repeated IceUpdate iceUpdate = 3;
|
||||
optional Hangup hangup = 4;
|
||||
optional Busy busy = 5;
|
||||
optional Offer offer = 1;
|
||||
optional Answer answer = 2;
|
||||
repeated IceCandidate iceCandidates = 3;
|
||||
optional Hangup legacyHangup = 4;
|
||||
optional Busy busy = 5;
|
||||
optional Hangup hangup = 7;
|
||||
optional bool supportsMultiRing = 8;
|
||||
optional uint32 destinationDeviceId = 9;
|
||||
}
|
||||
|
||||
message DataMessage {
|
||||
|
|
|
@ -105,12 +105,41 @@
|
|||
</p>
|
||||
</div>
|
||||
<hr>
|
||||
<div class='calling-setting'>
|
||||
<h3>{{ calling }}</h3>
|
||||
<div class='always-relay-calls-setting'>
|
||||
<input type='checkbox' name='always-relay-calls' id='always-relay-calls' />
|
||||
<label for='always-relay-calls'>{{ alwaysRelayCallsDescription }}</label>
|
||||
<p>
|
||||
<div class='detail'>
|
||||
{{ alwaysRelayCallsDetail }}
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
<div class='call-ringtone-notification-setting'>
|
||||
<input type='checkbox' name='call-ringtone-notification' id='call-ringtone-notification'/>
|
||||
<label for='call-ringtone-notification'>{{ callRingtoneNotificationDescription }}</label>
|
||||
</div>
|
||||
<div class='call-system-notification-setting'>
|
||||
<input type='checkbox' name='call-system-notification' id='call-system-notification'/>
|
||||
<label for='call-system-notification'>{{ callSystemNotificationDescription }}</label>
|
||||
</div>
|
||||
<div class='incoming-call-notification-setting'>
|
||||
<input type='checkbox' name='incoming-call-notification' id='incoming-call-notification'/>
|
||||
<label for='incoming-call-notification'>{{ incomingCallNotificationDescription }}</label>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class='permissions-setting'>
|
||||
<h3>{{ permissions }}</h3>
|
||||
<div class='media-permissions'>
|
||||
<input type='checkbox' name='media-permissions' id='media-permissions' />
|
||||
<label for='media-permissions'>{{ mediaPermissionsDescription }}</label>
|
||||
</div>
|
||||
<div class='media-camera-permissions'>
|
||||
<input type='checkbox' name='media-camera-permissions' id='media-camera-permissions' />
|
||||
<label for='media-camera-permissions'>{{ mediaCameraPermissionsDescription }}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class='sync-setting'></div>
|
||||
<hr>
|
||||
|
|
|
@ -55,13 +55,24 @@ window.setHideMenuBar = makeSetter('hide-menu-bar');
|
|||
window.getSpellCheck = makeGetter('spell-check');
|
||||
window.setSpellCheck = makeSetter('spell-check');
|
||||
|
||||
window.getAlwaysRelayCalls = makeGetter('always-relay-calls');
|
||||
window.setAlwaysRelayCalls = makeSetter('always-relay-calls');
|
||||
|
||||
window.getNotificationSetting = makeGetter('notification-setting');
|
||||
window.setNotificationSetting = makeSetter('notification-setting');
|
||||
window.getAudioNotification = makeGetter('audio-notification');
|
||||
window.setAudioNotification = makeSetter('audio-notification');
|
||||
window.getCallRingtoneNotification = makeGetter('call-ringtone-notification');
|
||||
window.setCallRingtoneNotification = makeSetter('call-ringtone-notification');
|
||||
window.getCallSystemNotification = makeGetter('call-system-notification');
|
||||
window.setCallSystemNotification = makeSetter('call-system-notification');
|
||||
window.getIncomingCallNotification = makeGetter('incoming-call-notification');
|
||||
window.setIncomingCallNotification = makeSetter('incoming-call-notification');
|
||||
|
||||
window.getMediaPermissions = makeGetter('media-permissions');
|
||||
window.setMediaPermissions = makeSetter('media-permissions');
|
||||
window.getMediaCameraPermissions = makeGetter('media-camera-permissions');
|
||||
window.setMediaCameraPermissions = makeSetter('media-camera-permissions');
|
||||
|
||||
window.isPrimary = makeGetter('is-primary');
|
||||
window.makeSyncRequest = makeGetter('sync-request');
|
||||
|
|
BIN
sounds/navigation-cancel.ogg
Executable file
BIN
sounds/ringtone_minimal.ogg
Executable file
|
@ -1,12 +1,18 @@
|
|||
.conversation-stack,
|
||||
.new-conversation,
|
||||
.inbox,
|
||||
.inbox-container,
|
||||
.gutter {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.inbox {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.expired {
|
||||
.conversation-stack,
|
||||
.gutter {
|
||||
|
|
|
@ -2378,6 +2378,83 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
|
|||
}
|
||||
}
|
||||
|
||||
.module-message-calling--notification {
|
||||
.module-message__metadata__date {
|
||||
@include light-theme {
|
||||
color: $color-gray-90;
|
||||
}
|
||||
@include dark-theme {
|
||||
color: $color-gray-05;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.module-message-calling {
|
||||
&--audio {
|
||||
text-align: center;
|
||||
|
||||
@include light-theme {
|
||||
color: $color-gray-90;
|
||||
}
|
||||
@include dark-theme {
|
||||
color: $color-gray-05;
|
||||
}
|
||||
}
|
||||
|
||||
&--audio__icon {
|
||||
height: 24px;
|
||||
margin-bottom: 4px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: 24px;
|
||||
|
||||
@include light-theme {
|
||||
@include color-svg(
|
||||
'../images/icons/v2/phone-right-outline-24.svg',
|
||||
$color-gray-75
|
||||
);
|
||||
}
|
||||
@include dark-theme {
|
||||
@include color-svg(
|
||||
'../images/icons/v2/phone-right-outline-24.svg',
|
||||
$color-gray-15
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
&--video {
|
||||
text-align: center;
|
||||
|
||||
@include light-theme {
|
||||
color: $color-gray-90;
|
||||
}
|
||||
@include dark-theme {
|
||||
color: $color-gray-05;
|
||||
}
|
||||
}
|
||||
|
||||
&--video__icon {
|
||||
height: 24px;
|
||||
margin-bottom: 4px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: 24px;
|
||||
|
||||
@include light-theme {
|
||||
@include color-svg(
|
||||
'../images/icons/v2/video-outline-24.svg',
|
||||
$color-gray-75
|
||||
);
|
||||
}
|
||||
@include dark-theme {
|
||||
@include color-svg(
|
||||
'../images/icons/v2/video-outline-24.svg',
|
||||
$color-gray-15
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.module-message-unsynced {
|
||||
padding-bottom: 24px;
|
||||
text-align: center;
|
||||
|
@ -2791,6 +2868,61 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
|
|||
}
|
||||
}
|
||||
|
||||
.module-conversation-header__audio-calling-button {
|
||||
@include light-theme {
|
||||
@include color-svg(
|
||||
'../images/icons/v2/phone-right-outline-24.svg',
|
||||
$color-gray-75
|
||||
);
|
||||
}
|
||||
@include dark-theme {
|
||||
@include color-svg(
|
||||
'../images/icons/v2/phone-right-solid-24.svg',
|
||||
$color-gray-15
|
||||
);
|
||||
}
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
margin-left: 12px;
|
||||
border: none;
|
||||
opacity: 0;
|
||||
transition: opacity 250ms ease-out;
|
||||
|
||||
&:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&--show {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.module-conversation-header__video-calling-button {
|
||||
@include light-theme {
|
||||
@include color-svg(
|
||||
'../images/icons/v2/video-outline-24.svg',
|
||||
$color-gray-75
|
||||
);
|
||||
}
|
||||
@include dark-theme {
|
||||
@include color-svg('../images/icons/v2/video-solid-24.svg', $color-gray-15);
|
||||
}
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
margin-left: 12px;
|
||||
border: none;
|
||||
opacity: 0;
|
||||
transition: opacity 250ms ease-out;
|
||||
|
||||
&:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&--show {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Module: Message Detail
|
||||
|
||||
.module-message-detail {
|
||||
|
@ -5402,6 +5534,334 @@ button.module-image__border-overlay:focus {
|
|||
}
|
||||
}
|
||||
|
||||
.module-incoming-call {
|
||||
align-items: center;
|
||||
background-color: $color-gray-75;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.module-incoming-call__contact {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
|
||||
&--avatar {
|
||||
margin-bottom: 8px;
|
||||
margin-left: 16px;
|
||||
margin-top: 8px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&--name {
|
||||
align-items: stretch;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&--name-header {
|
||||
@include font-body-1-bold;
|
||||
color: #ffffff;
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&--message-text {
|
||||
@include font-body-2;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.module-incoming-call__actions {
|
||||
display: flex;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.module-incoming-call__button--accept-video-as-audio {
|
||||
background-color: $color-gray-45;
|
||||
|
||||
@include keyboard-mode {
|
||||
&:focus {
|
||||
box-shadow: 0px 0px 0px 4px $ultramarine-ui-light;
|
||||
}
|
||||
}
|
||||
|
||||
@include mouse-mode {
|
||||
&:hover {
|
||||
box-shadow: 0px 0px 0px 2px $ultramarine-ui-light;
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
@include color-svg(
|
||||
'../images/icons/v2/video-off-solid-24.svg',
|
||||
$color-white
|
||||
);
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.module-incoming-call__button--accept-video {
|
||||
background-color: $color-accent-green;
|
||||
|
||||
@include keyboard-mode {
|
||||
&:focus {
|
||||
box-shadow: 0px 0px 0px 4px $ultramarine-ui-light;
|
||||
}
|
||||
}
|
||||
|
||||
@include mouse-mode {
|
||||
&:hover {
|
||||
box-shadow: 0px 0px 0px 2px $ultramarine-ui-light;
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
@include color-svg('../images/icons/v2/video-solid-24.svg', $color-white);
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.module-incoming-call__button--accept-audio {
|
||||
background-color: $color-accent-green;
|
||||
|
||||
@include keyboard-mode {
|
||||
&:focus {
|
||||
box-shadow: 0px 0px 0px 4px $ultramarine-ui-light;
|
||||
}
|
||||
}
|
||||
|
||||
@include mouse-mode {
|
||||
&:hover {
|
||||
box-shadow: 0px 0px 0px 2px $ultramarine-ui-light;
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
@include color-svg(
|
||||
'../images/icons/v2/phone-right-solid-24.svg',
|
||||
$color-white
|
||||
);
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.module-incoming-call__button--decline {
|
||||
background-color: $color-accent-red;
|
||||
|
||||
@include keyboard-mode {
|
||||
&:focus {
|
||||
box-shadow: 0px 0px 0px 4px $ultramarine-ui-light;
|
||||
}
|
||||
}
|
||||
|
||||
@include mouse-mode {
|
||||
&:hover {
|
||||
box-shadow: 0px 0px 0px 2px $ultramarine-ui-light;
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
@include color-svg('../images/icons/v2/phone-down-24.svg', $color-white);
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.module-incoming-call__icon,
|
||||
.module-ongoing-call__icon {
|
||||
align-items: center;
|
||||
border-radius: 40px;
|
||||
border: none;
|
||||
display: flex;
|
||||
height: 40px;
|
||||
justify-content: center;
|
||||
margin-left: 24px;
|
||||
outline: none;
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.module-ongoing-call__icon {
|
||||
border-radius: 56px;
|
||||
height: 56px;
|
||||
width: 56px;
|
||||
|
||||
&--audio {
|
||||
&--enabled {
|
||||
background-color: $color-gray-45;
|
||||
|
||||
div {
|
||||
@include color-svg('../images/icons/v2/mic-solid-28.svg', $color-white);
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
}
|
||||
}
|
||||
&--disabled {
|
||||
background-color: $color-white;
|
||||
|
||||
div {
|
||||
@include color-svg(
|
||||
'../images/icons/v2/mic-off-solid-28.svg',
|
||||
$color-black
|
||||
);
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--video {
|
||||
&--enabled {
|
||||
background-color: $color-gray-45;
|
||||
|
||||
div {
|
||||
@include color-svg(
|
||||
'../images/icons/v2/video-solid-28.svg',
|
||||
$color-white
|
||||
);
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
}
|
||||
}
|
||||
&--disabled {
|
||||
background-color: $color-white;
|
||||
|
||||
div {
|
||||
@include color-svg(
|
||||
'../images/icons/v2/video-off-solid-28.svg',
|
||||
$color-black
|
||||
);
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--hangup {
|
||||
background-color: $color-accent-red;
|
||||
|
||||
div {
|
||||
@include color-svg('../images/icons/v2/phone-down-28.svg', $color-white);
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.module-ongoing-call {
|
||||
background-color: $color-gray-95;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.module-ongoing-call__remote-video-enabled {
|
||||
background-color: $color-gray-95;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.module-ongoing-call__remote-video-disabled {
|
||||
background-color: $color-gray-95;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.module-ongoing-call__local-video {
|
||||
transform: rotateY(180deg);
|
||||
background-color: transparent;
|
||||
bottom: 160px;
|
||||
height: 152px;
|
||||
position: absolute;
|
||||
right: 32px;
|
||||
width: 210px;
|
||||
}
|
||||
|
||||
.module-ongoing-call__header {
|
||||
background-color: $color-black-alpha-60;
|
||||
padding-bottom: 24px;
|
||||
padding-top: 24px;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
|
||||
font-style: normal;
|
||||
color: #ffffff;
|
||||
text-shadow: 0px 0px 4px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.module-ongoing-call__header-name {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
line-height: 21px;
|
||||
letter-spacing: -0.009em;
|
||||
}
|
||||
|
||||
.module-ongoing-call__header-message {
|
||||
font-weight: normal;
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.0025em;
|
||||
}
|
||||
|
||||
.module-ongoing-call__actions {
|
||||
background-color: $color-black-alpha-60;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-bottom: 32px;
|
||||
padding-top: 32px;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@keyframes module-ongoing-call__controls--fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes module-ongoing-call__controls--fade-out {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.module-ongoing-call__controls--fadeIn {
|
||||
animation: {
|
||||
name: module-ongoing-call__controls--fade-in;
|
||||
duration: 400ms;
|
||||
timing-function: $ease-out-expo;
|
||||
fill-mode: forwards;
|
||||
}
|
||||
}
|
||||
|
||||
.module-ongoing-call__controls--fadeOut {
|
||||
animation: {
|
||||
name: module-ongoing-call__controls--fade-out;
|
||||
duration: 1200ms;
|
||||
timing-function: $ease-out-expo;
|
||||
fill-mode: forwards;
|
||||
}
|
||||
}
|
||||
|
||||
// Module: Left Pane
|
||||
|
||||
.module-left-pane {
|
||||
|
@ -7953,6 +8413,30 @@ button.module-image__border-overlay:focus {
|
|||
padding-right: 0px;
|
||||
}
|
||||
|
||||
/* Third-party module: react-tooltip-lite */
|
||||
|
||||
.react-tooltip-lite {
|
||||
border-radius: 8px;
|
||||
|
||||
@include light-theme {
|
||||
background-color: $color-gray-02;
|
||||
color: $color-gray-75;
|
||||
}
|
||||
@include dark-theme {
|
||||
background-color: $color-gray-65;
|
||||
color: $color-gray-05;
|
||||
}
|
||||
}
|
||||
|
||||
.react-tooltip-lite-arrow {
|
||||
@include light-theme {
|
||||
border-color: $color-gray-02;
|
||||
}
|
||||
@include dark-theme {
|
||||
border-color: $color-gray-65;
|
||||
}
|
||||
}
|
||||
|
||||
/* Third-party module: react-contextmenu*/
|
||||
|
||||
.react-contextmenu {
|
||||
|
|
|
@ -68,4 +68,10 @@
|
|||
margin-top: 0.3em;
|
||||
margin-left: 1.5em;
|
||||
}
|
||||
.detail {
|
||||
margin-top: 0.3em;
|
||||
margin-left: 1.5em;
|
||||
@include font-body-2;
|
||||
color: $color-gray-60;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,22 +34,25 @@
|
|||
</script>
|
||||
|
||||
<script type='text/x-tmpl-mustache' id='two-column'>
|
||||
<div class='gutter'>
|
||||
<div class='left-pane-placeholder'></div>
|
||||
</div>
|
||||
<div class='conversation-stack'>
|
||||
<div class='conversation placeholder'>
|
||||
<div class='conversation-header'></div>
|
||||
<div class='container'>
|
||||
<div class='content'>
|
||||
<div class="module-splash-screen__logo module-img--128"></div>
|
||||
<h3>{{ welcomeToSignal }}</h3>
|
||||
<p>{{ selectAContact }}</p>
|
||||
<div class='call-manager-placeholder'></div>
|
||||
<div class='inbox-container'>
|
||||
<div class='gutter'>
|
||||
<div class='left-pane-placeholder'></div>
|
||||
</div>
|
||||
<div class='conversation-stack'>
|
||||
<div class='conversation placeholder'>
|
||||
<div class='conversation-header'></div>
|
||||
<div class='container'>
|
||||
<div class='content'>
|
||||
<div class="module-splash-screen__logo module-img--128 module-logo-blue"></div>
|
||||
<h3>{{ welcomeToSignal }}</h3>
|
||||
<p>{{ selectAContact }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='lightbox-container'></div>
|
||||
</div>
|
||||
<div class='lightbox-container'></div>
|
||||
</script>
|
||||
|
||||
<script type='text/x-tmpl-mustache' id='scroll-down-button-view'>
|
||||
|
|
65
ts/components/CallManager.stories.tsx
Normal file
|
@ -0,0 +1,65 @@
|
|||
import * as React from 'react';
|
||||
import { CallManager } from './CallManager';
|
||||
import { CallState } from '../types/Calling';
|
||||
import { ColorType } from '../types/Util';
|
||||
|
||||
// @ts-ignore
|
||||
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||
// @ts-ignore
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const callDetails = {
|
||||
avatarPath: undefined,
|
||||
callId: 0,
|
||||
contactColor: 'ultramarine' as ColorType,
|
||||
isIncoming: true,
|
||||
isVideoCall: true,
|
||||
name: 'Rick Sanchez',
|
||||
phoneNumber: '3051234567',
|
||||
profileName: 'Rick Sanchez',
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
acceptCall: action('accept-call'),
|
||||
callDetails,
|
||||
callState: CallState.Accepted,
|
||||
declineCall: action('decline-call'),
|
||||
getVideoCapturer: () => ({}),
|
||||
getVideoRenderer: () => ({}),
|
||||
hangUp: action('hang-up'),
|
||||
hasLocalAudio: true,
|
||||
hasLocalVideo: true,
|
||||
hasRemoteVideo: true,
|
||||
i18n,
|
||||
setLocalAudio: action('set-local-audio'),
|
||||
setLocalVideo: action('set-local-video'),
|
||||
setVideoCapturer: action('set-video-capturer'),
|
||||
setVideoRenderer: action('set-video-renderer'),
|
||||
};
|
||||
|
||||
const permutations = [
|
||||
{
|
||||
title: 'Call Manager (ongoing)',
|
||||
props: {},
|
||||
},
|
||||
{
|
||||
title: 'Call Manager (ringing)',
|
||||
props: {
|
||||
callState: CallState.Ringing,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
storiesOf('Components/CallManager', module).add('Iterations', () => {
|
||||
return permutations.map(({ props, title }) => (
|
||||
<>
|
||||
<h3>{title}</h3>
|
||||
<CallManager {...defaultProps} {...props} />
|
||||
</>
|
||||
));
|
||||
});
|
77
ts/components/CallManager.tsx
Normal file
|
@ -0,0 +1,77 @@
|
|||
import React from 'react';
|
||||
import { CallScreen, PropsType as CallScreenPropsType } from './CallScreen';
|
||||
import {
|
||||
IncomingCallBar,
|
||||
PropsType as IncomingCallBarPropsType,
|
||||
} from './IncomingCallBar';
|
||||
import { CallState } from '../types/Calling';
|
||||
import { CallDetailsType } from '../state/ducks/calling';
|
||||
|
||||
type CallManagerPropsType = {
|
||||
callDetails?: CallDetailsType;
|
||||
callState?: CallState;
|
||||
};
|
||||
type PropsType = IncomingCallBarPropsType &
|
||||
CallScreenPropsType &
|
||||
CallManagerPropsType;
|
||||
|
||||
export const CallManager = ({
|
||||
acceptCall,
|
||||
callDetails,
|
||||
callState,
|
||||
declineCall,
|
||||
getVideoCapturer,
|
||||
getVideoRenderer,
|
||||
hangUp,
|
||||
hasLocalAudio,
|
||||
hasLocalVideo,
|
||||
hasRemoteVideo,
|
||||
i18n,
|
||||
setLocalAudio,
|
||||
setLocalVideo,
|
||||
setVideoCapturer,
|
||||
setVideoRenderer,
|
||||
}: PropsType): JSX.Element | null => {
|
||||
if (!callDetails || !callState) {
|
||||
return null;
|
||||
}
|
||||
const incoming = callDetails.isIncoming;
|
||||
const outgoing = !incoming;
|
||||
const ongoing =
|
||||
callState === CallState.Accepted || callState === CallState.Reconnecting;
|
||||
const ringing = callState === CallState.Ringing;
|
||||
|
||||
if (outgoing || ongoing) {
|
||||
return (
|
||||
<CallScreen
|
||||
callDetails={callDetails}
|
||||
callState={callState}
|
||||
getVideoCapturer={getVideoCapturer}
|
||||
getVideoRenderer={getVideoRenderer}
|
||||
hangUp={hangUp}
|
||||
hasLocalAudio={hasLocalAudio}
|
||||
hasLocalVideo={hasLocalVideo}
|
||||
i18n={i18n}
|
||||
hasRemoteVideo={hasRemoteVideo}
|
||||
setVideoCapturer={setVideoCapturer}
|
||||
setVideoRenderer={setVideoRenderer}
|
||||
setLocalAudio={setLocalAudio}
|
||||
setLocalVideo={setLocalVideo}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (incoming && ringing) {
|
||||
return (
|
||||
<IncomingCallBar
|
||||
acceptCall={acceptCall}
|
||||
callDetails={callDetails}
|
||||
declineCall={declineCall}
|
||||
i18n={i18n}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Ended || (Incoming && Prering)
|
||||
return null;
|
||||
};
|
120
ts/components/CallScreen.stories.tsx
Normal file
|
@ -0,0 +1,120 @@
|
|||
import * as React from 'react';
|
||||
import { CallState } from '../types/Calling';
|
||||
import { ColorType } from '../types/Util';
|
||||
import { CallScreen } from './CallScreen';
|
||||
|
||||
// @ts-ignore
|
||||
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||
// @ts-ignore
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { boolean, select } from '@storybook/addon-knobs';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const callDetails = {
|
||||
avatarPath: undefined,
|
||||
callId: 0,
|
||||
contactColor: 'ultramarine' as ColorType,
|
||||
isIncoming: true,
|
||||
isVideoCall: true,
|
||||
name: 'Rick Sanchez',
|
||||
phoneNumber: '3051234567',
|
||||
profileName: 'Rick Sanchez',
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
callDetails,
|
||||
callState: CallState.Accepted,
|
||||
getVideoCapturer: () => ({}),
|
||||
getVideoRenderer: () => ({}),
|
||||
hangUp: action('hang-up'),
|
||||
hasLocalAudio: true,
|
||||
hasLocalVideo: true,
|
||||
hasRemoteVideo: true,
|
||||
i18n,
|
||||
setLocalAudio: action('set-local-audio'),
|
||||
setLocalVideo: action('set-local-video'),
|
||||
setVideoCapturer: action('set-video-capturer'),
|
||||
setVideoRenderer: action('set-video-renderer'),
|
||||
};
|
||||
|
||||
const permutations = [
|
||||
{
|
||||
title: 'Call Screen',
|
||||
props: {},
|
||||
},
|
||||
{
|
||||
title: 'Call Screen (Pre-ring)',
|
||||
props: {
|
||||
callState: CallState.Prering,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Call Screen (Ringing)',
|
||||
props: {
|
||||
callState: CallState.Ringing,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Call Screen (Reconnecting)',
|
||||
props: {
|
||||
callState: CallState.Reconnecting,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Call Screen (Ended)',
|
||||
props: {
|
||||
callState: CallState.Ended,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Calling (no local audio)',
|
||||
props: {
|
||||
...defaultProps,
|
||||
hasLocalAudio: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Calling (no local video)',
|
||||
props: {
|
||||
...defaultProps,
|
||||
hasLocalVideo: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Calling (no remote video)',
|
||||
props: {
|
||||
...defaultProps,
|
||||
hasRemoteVideo: false,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
storiesOf('Components/CallScreen', module)
|
||||
.add('Knobs Playground', () => {
|
||||
const callState = select('callState', CallState, CallState.Accepted);
|
||||
const hasLocalAudio = boolean('hasLocalAudio', true);
|
||||
const hasLocalVideo = boolean('hasLocalVideo', true);
|
||||
const hasRemoteVideo = boolean('hasRemoteVideo', true);
|
||||
|
||||
return (
|
||||
<CallScreen
|
||||
{...defaultProps}
|
||||
callState={callState}
|
||||
hasLocalAudio={hasLocalAudio}
|
||||
hasLocalVideo={hasLocalVideo}
|
||||
hasRemoteVideo={hasRemoteVideo}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.add('Iterations', () => {
|
||||
return permutations.map(({ props, title }) => (
|
||||
<>
|
||||
<h3>{title}</h3>
|
||||
<CallScreen {...defaultProps} {...props} />
|
||||
</>
|
||||
));
|
||||
});
|
380
ts/components/CallScreen.tsx
Normal file
|
@ -0,0 +1,380 @@
|
|||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
CallDetailsType,
|
||||
HangUpType,
|
||||
SetLocalAudioType,
|
||||
SetLocalVideoType,
|
||||
SetVideoCapturerType,
|
||||
SetVideoRendererType,
|
||||
} from '../state/ducks/calling';
|
||||
import { Avatar } from './Avatar';
|
||||
import { CallState } from '../types/Calling';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
import { CanvasVideoRenderer, GumVideoCapturer } from '../window.d';
|
||||
|
||||
type CallingButtonProps = {
|
||||
classNameSuffix: string;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
const CallingButton = ({
|
||||
classNameSuffix,
|
||||
onClick,
|
||||
}: CallingButtonProps): JSX.Element => {
|
||||
const className = classNames(
|
||||
'module-ongoing-call__icon',
|
||||
`module-ongoing-call__icon${classNameSuffix}`
|
||||
);
|
||||
|
||||
return (
|
||||
<button className={className} onClick={onClick}>
|
||||
<div />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export type PropsType = {
|
||||
callDetails?: CallDetailsType;
|
||||
callState?: CallState;
|
||||
getVideoCapturer: (
|
||||
ref: React.RefObject<HTMLVideoElement>
|
||||
) => GumVideoCapturer;
|
||||
getVideoRenderer: (
|
||||
ref: React.RefObject<HTMLCanvasElement>
|
||||
) => CanvasVideoRenderer;
|
||||
hangUp: (_: HangUpType) => void;
|
||||
hasLocalAudio: boolean;
|
||||
hasLocalVideo: boolean;
|
||||
hasRemoteVideo: boolean;
|
||||
i18n: LocalizerType;
|
||||
setLocalAudio: (_: SetLocalAudioType) => void;
|
||||
setLocalVideo: (_: SetLocalVideoType) => void;
|
||||
setVideoCapturer: (_: SetVideoCapturerType) => void;
|
||||
setVideoRenderer: (_: SetVideoRendererType) => void;
|
||||
};
|
||||
|
||||
type StateType = {
|
||||
acceptedTime: number | null;
|
||||
acceptedDuration: number | null;
|
||||
showControls: boolean;
|
||||
};
|
||||
|
||||
export class CallScreen extends React.Component<PropsType, StateType> {
|
||||
private interval: any;
|
||||
private controlsFadeTimer: any;
|
||||
private readonly localVideoRef: React.RefObject<HTMLVideoElement>;
|
||||
private readonly remoteVideoRef: React.RefObject<HTMLCanvasElement>;
|
||||
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
acceptedTime: null,
|
||||
acceptedDuration: null,
|
||||
showControls: true,
|
||||
};
|
||||
|
||||
this.interval = null;
|
||||
this.controlsFadeTimer = null;
|
||||
this.localVideoRef = React.createRef();
|
||||
this.remoteVideoRef = React.createRef();
|
||||
|
||||
this.setVideoCapturerAndRenderer(
|
||||
props.getVideoCapturer(this.localVideoRef),
|
||||
props.getVideoRenderer(this.remoteVideoRef)
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
// It's really jump with a value of 500ms.
|
||||
this.interval = setInterval(this.updateAcceptedTimer, 100);
|
||||
this.fadeControls();
|
||||
|
||||
document.addEventListener('keydown', this.handleKeyDown);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
document.removeEventListener('keydown', this.handleKeyDown);
|
||||
|
||||
if (this.interval) {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
if (this.controlsFadeTimer) {
|
||||
clearTimeout(this.controlsFadeTimer);
|
||||
}
|
||||
this.setVideoCapturerAndRenderer(null, null);
|
||||
}
|
||||
|
||||
updateAcceptedTimer = () => {
|
||||
const { acceptedTime } = this.state;
|
||||
const { callState } = this.props;
|
||||
|
||||
if (acceptedTime) {
|
||||
this.setState({
|
||||
acceptedTime,
|
||||
acceptedDuration: Date.now() - acceptedTime,
|
||||
});
|
||||
} else if (
|
||||
callState === CallState.Accepted ||
|
||||
callState === CallState.Reconnecting
|
||||
) {
|
||||
this.setState({
|
||||
acceptedTime: Date.now(),
|
||||
acceptedDuration: 1,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handleKeyDown = (event: KeyboardEvent) => {
|
||||
const { callDetails } = this.props;
|
||||
|
||||
if (!callDetails) {
|
||||
return;
|
||||
}
|
||||
|
||||
let eventHandled = false;
|
||||
|
||||
if (event.key === 'V') {
|
||||
this.toggleVideo();
|
||||
eventHandled = true;
|
||||
} else if (event.key === 'M') {
|
||||
this.toggleAudio();
|
||||
eventHandled = true;
|
||||
}
|
||||
|
||||
if (eventHandled) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.showControls();
|
||||
}
|
||||
};
|
||||
|
||||
showControls = () => {
|
||||
if (!this.state.showControls) {
|
||||
this.setState({
|
||||
showControls: true,
|
||||
});
|
||||
}
|
||||
|
||||
this.fadeControls();
|
||||
};
|
||||
|
||||
fadeControls = () => {
|
||||
if (this.controlsFadeTimer) {
|
||||
clearTimeout(this.controlsFadeTimer);
|
||||
}
|
||||
|
||||
this.controlsFadeTimer = setTimeout(() => {
|
||||
this.setState({
|
||||
showControls: false,
|
||||
});
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
toggleAudio = () => {
|
||||
const { callDetails, hasLocalAudio, setLocalAudio } = this.props;
|
||||
|
||||
if (!callDetails) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLocalAudio({
|
||||
callId: callDetails.callId,
|
||||
enabled: !hasLocalAudio,
|
||||
});
|
||||
};
|
||||
|
||||
toggleVideo = () => {
|
||||
const { callDetails, hasLocalVideo, setLocalVideo } = this.props;
|
||||
|
||||
if (!callDetails) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLocalVideo({ callId: callDetails.callId, enabled: !hasLocalVideo });
|
||||
};
|
||||
|
||||
public render() {
|
||||
const {
|
||||
callDetails,
|
||||
callState,
|
||||
hangUp,
|
||||
hasLocalAudio,
|
||||
hasLocalVideo,
|
||||
hasRemoteVideo,
|
||||
} = this.props;
|
||||
const { showControls } = this.state;
|
||||
const isAudioOnly = !hasLocalVideo && !hasRemoteVideo;
|
||||
|
||||
if (!callDetails || !callState) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const controlsFadeClass = classNames({
|
||||
'module-ongoing-call__controls--fadeIn':
|
||||
(showControls || isAudioOnly) && callState !== CallState.Accepted,
|
||||
'module-ongoing-call__controls--fadeOut':
|
||||
!showControls && !isAudioOnly && callState === CallState.Accepted,
|
||||
});
|
||||
|
||||
const toggleAudioSuffix = hasLocalAudio
|
||||
? '--audio--enabled'
|
||||
: '--audio--disabled';
|
||||
const toggleVideoSuffix = hasLocalVideo
|
||||
? '--video--enabled'
|
||||
: '--video--disabled';
|
||||
|
||||
return (
|
||||
<div
|
||||
className="module-ongoing-call"
|
||||
onMouseMove={this.showControls}
|
||||
role="group"
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'module-ongoing-call__header',
|
||||
controlsFadeClass
|
||||
)}
|
||||
>
|
||||
<div className="module-ongoing-call__header-name">
|
||||
{callDetails.name}
|
||||
</div>
|
||||
{this.renderMessage(callState)}
|
||||
</div>
|
||||
{hasRemoteVideo
|
||||
? this.renderRemoteVideo()
|
||||
: this.renderAvatar(callDetails)}
|
||||
{hasLocalVideo ? this.renderLocalVideo() : null}
|
||||
<div
|
||||
className={classNames(
|
||||
'module-ongoing-call__actions',
|
||||
controlsFadeClass
|
||||
)}
|
||||
>
|
||||
<CallingButton
|
||||
classNameSuffix={toggleVideoSuffix}
|
||||
onClick={this.toggleVideo}
|
||||
/>
|
||||
<CallingButton
|
||||
classNameSuffix={toggleAudioSuffix}
|
||||
onClick={this.toggleAudio}
|
||||
/>
|
||||
<CallingButton
|
||||
classNameSuffix="--hangup"
|
||||
onClick={() => {
|
||||
hangUp({ callId: callDetails.callId });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderAvatar(callDetails: CallDetailsType) {
|
||||
const { i18n } = this.props;
|
||||
const {
|
||||
avatarPath,
|
||||
contactColor,
|
||||
name,
|
||||
phoneNumber,
|
||||
profileName,
|
||||
} = callDetails;
|
||||
return (
|
||||
<div className="module-ongoing-call__remote-video-disabled">
|
||||
<Avatar
|
||||
avatarPath={avatarPath}
|
||||
color={contactColor || 'ultramarine'}
|
||||
noteToSelf={false}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
name={name}
|
||||
phoneNumber={phoneNumber}
|
||||
profileName={profileName}
|
||||
size={112}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderLocalVideo() {
|
||||
return (
|
||||
<video
|
||||
className="module-ongoing-call__local-video"
|
||||
ref={this.localVideoRef}
|
||||
autoPlay
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
private renderRemoteVideo() {
|
||||
return (
|
||||
<canvas
|
||||
className="module-ongoing-call__remote-video-enabled"
|
||||
ref={this.remoteVideoRef}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
private renderMessage(callState: CallState) {
|
||||
const { i18n } = this.props;
|
||||
|
||||
let message = null;
|
||||
if (callState === CallState.Prering) {
|
||||
message = i18n('outgoingCallPrering');
|
||||
} else if (callState === CallState.Ringing) {
|
||||
message = i18n('outgoingCallRinging');
|
||||
} else if (callState === CallState.Reconnecting) {
|
||||
message = i18n('callReconnecting');
|
||||
} else if (
|
||||
callState === CallState.Accepted &&
|
||||
this.state.acceptedDuration
|
||||
) {
|
||||
message = i18n('callDuration', [
|
||||
this.renderDuration(this.state.acceptedDuration),
|
||||
]);
|
||||
}
|
||||
|
||||
if (!message) {
|
||||
return null;
|
||||
}
|
||||
return <div className="module-ongoing-call__header-message">{message}</div>;
|
||||
}
|
||||
|
||||
private renderDuration(ms: number): string {
|
||||
const secs = Math.floor((ms / 1000) % 60)
|
||||
.toString()
|
||||
.padStart(2, '0');
|
||||
const mins = Math.floor((ms / 60000) % 60)
|
||||
.toString()
|
||||
.padStart(2, '0');
|
||||
const hours = Math.floor(ms / 3600000);
|
||||
if (hours > 0) {
|
||||
return `${hours}:${mins}:${secs}`;
|
||||
}
|
||||
return `${mins}:${secs}`;
|
||||
}
|
||||
|
||||
private setVideoCapturerAndRenderer(
|
||||
capturer: GumVideoCapturer | null,
|
||||
renderer: CanvasVideoRenderer | null
|
||||
) {
|
||||
const { callDetails, setVideoCapturer, setVideoRenderer } = this.props;
|
||||
|
||||
if (!callDetails) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { callId } = callDetails;
|
||||
|
||||
setVideoCapturer({
|
||||
callId,
|
||||
capturer,
|
||||
});
|
||||
|
||||
setVideoRenderer({
|
||||
callId,
|
||||
renderer,
|
||||
});
|
||||
}
|
||||
}
|
101
ts/components/IncomingCallBar.stories.tsx
Normal file
|
@ -0,0 +1,101 @@
|
|||
import * as React from 'react';
|
||||
import { IncomingCallBar } from './IncomingCallBar';
|
||||
import { ColorType } from '../types/Util';
|
||||
|
||||
// @ts-ignore
|
||||
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||
// @ts-ignore
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { boolean, select, text } from '@storybook/addon-knobs';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const defaultProps = {
|
||||
acceptCall: action('accept-call'),
|
||||
callDetails: {
|
||||
avatarPath: undefined,
|
||||
callId: 0,
|
||||
contactColor: 'ultramarine' as ColorType,
|
||||
isIncoming: true,
|
||||
isVideoCall: true,
|
||||
name: 'Rick Sanchez',
|
||||
phoneNumber: '3051234567',
|
||||
profileName: 'Rick Sanchez',
|
||||
},
|
||||
declineCall: action('decline-call'),
|
||||
i18n,
|
||||
};
|
||||
|
||||
const colors: Array<ColorType> = [
|
||||
'blue',
|
||||
'blue_grey',
|
||||
'brown',
|
||||
'deep_orange',
|
||||
'green',
|
||||
'grey',
|
||||
'indigo',
|
||||
'light_green',
|
||||
'pink',
|
||||
'purple',
|
||||
'red',
|
||||
'teal',
|
||||
'ultramarine',
|
||||
];
|
||||
|
||||
const permutations = [
|
||||
{
|
||||
title: 'Incoming Call Bar (no call details)',
|
||||
props: {},
|
||||
},
|
||||
{
|
||||
title: 'Incoming Call Bar (video)',
|
||||
props: {
|
||||
callDetails: {
|
||||
...defaultProps.callDetails,
|
||||
isVideoCall: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Incoming Call Bar (audio)',
|
||||
props: {
|
||||
callDetails: {
|
||||
...defaultProps.callDetails,
|
||||
isVideoCall: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
storiesOf('Components/IncomingCallBar', module)
|
||||
.add('Knobs Playground', () => {
|
||||
const contactColor = select('contactColor', colors, 'ultramarine');
|
||||
const isVideoCall = boolean('isVideoCall', false);
|
||||
const name = text(
|
||||
'name',
|
||||
'Rick Sanchez Foo Bar Baz Spool Cool Mango Fango Wand Mars Venus Jupiter Spark Mirage Water Loop Branch Zeus Element Sail Bananas Cars Horticulture Turtle Lion Zebra Micro Music Garage Iguana Ohio Retro Joy Entertainment Logo Understanding Diary'
|
||||
);
|
||||
|
||||
return (
|
||||
<IncomingCallBar
|
||||
{...defaultProps}
|
||||
callDetails={{
|
||||
...defaultProps.callDetails,
|
||||
contactColor,
|
||||
isVideoCall,
|
||||
name,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.add('Iterations', () => {
|
||||
return permutations.map(({ props, title }) => (
|
||||
<>
|
||||
<h3>{title}</h3>
|
||||
<IncomingCallBar {...defaultProps} {...props} />
|
||||
</>
|
||||
));
|
||||
});
|
158
ts/components/IncomingCallBar.tsx
Normal file
|
@ -0,0 +1,158 @@
|
|||
import React from 'react';
|
||||
import Tooltip from 'react-tooltip-lite';
|
||||
import { Avatar } from './Avatar';
|
||||
import { ContactName } from './conversation/ContactName';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
import {
|
||||
AcceptCallType,
|
||||
CallDetailsType,
|
||||
DeclineCallType,
|
||||
} from '../state/ducks/calling';
|
||||
|
||||
export type PropsType = {
|
||||
acceptCall: (_: AcceptCallType) => void;
|
||||
callDetails?: CallDetailsType;
|
||||
declineCall: (_: DeclineCallType) => void;
|
||||
i18n: LocalizerType;
|
||||
};
|
||||
|
||||
type CallButtonProps = {
|
||||
classSuffix: string;
|
||||
tabIndex: number;
|
||||
tooltipContent: string;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
const CallButton = ({
|
||||
classSuffix,
|
||||
onClick,
|
||||
tabIndex,
|
||||
tooltipContent,
|
||||
}: CallButtonProps): JSX.Element => {
|
||||
return (
|
||||
<button
|
||||
className={`module-incoming-call__icon module-incoming-call__button--${classSuffix}`}
|
||||
onClick={onClick}
|
||||
tabIndex={tabIndex}
|
||||
>
|
||||
<Tooltip
|
||||
arrowSize={6}
|
||||
content={tooltipContent}
|
||||
direction="bottom"
|
||||
distance={16}
|
||||
hoverDelay={0}
|
||||
>
|
||||
<div />
|
||||
</Tooltip>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
// tslint:disable-next-line max-func-body-length
|
||||
export const IncomingCallBar = ({
|
||||
acceptCall,
|
||||
callDetails,
|
||||
declineCall,
|
||||
i18n,
|
||||
}: PropsType): JSX.Element | null => {
|
||||
if (!callDetails) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
avatarPath,
|
||||
callId,
|
||||
contactColor,
|
||||
name,
|
||||
phoneNumber,
|
||||
profileName,
|
||||
} = callDetails;
|
||||
|
||||
return (
|
||||
<div className="module-incoming-call">
|
||||
<div className="module-incoming-call__contact">
|
||||
<div className="module-incoming-call__contact--avatar">
|
||||
<Avatar
|
||||
avatarPath={avatarPath}
|
||||
color={contactColor || 'ultramarine'}
|
||||
noteToSelf={false}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
name={name}
|
||||
phoneNumber={phoneNumber}
|
||||
profileName={profileName}
|
||||
size={52}
|
||||
/>
|
||||
</div>
|
||||
<div className="module-incoming-call__contact--name">
|
||||
<div className="module-incoming-call__contact--name-header">
|
||||
<ContactName
|
||||
phoneNumber={phoneNumber}
|
||||
name={name}
|
||||
profileName={profileName}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
dir="auto"
|
||||
className="module-incoming-call__contact--message-text"
|
||||
>
|
||||
{i18n(
|
||||
callDetails.isVideoCall
|
||||
? 'incomingVideoCall'
|
||||
: 'incomingAudioCall'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="module-incoming-call__actions">
|
||||
{callDetails.isVideoCall ? (
|
||||
<>
|
||||
<CallButton
|
||||
classSuffix="decline"
|
||||
onClick={() => {
|
||||
declineCall({ callId });
|
||||
}}
|
||||
tabIndex={0}
|
||||
tooltipContent={i18n('declineCall')}
|
||||
/>
|
||||
<CallButton
|
||||
classSuffix="accept-video-as-audio"
|
||||
onClick={() => {
|
||||
acceptCall({ callId, asVideoCall: false });
|
||||
}}
|
||||
tabIndex={0}
|
||||
tooltipContent={i18n('acceptCallWithoutVideo')}
|
||||
/>
|
||||
<CallButton
|
||||
classSuffix="accept-video"
|
||||
onClick={() => {
|
||||
acceptCall({ callId, asVideoCall: true });
|
||||
}}
|
||||
tabIndex={0}
|
||||
tooltipContent={i18n('acceptCall')}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CallButton
|
||||
classSuffix="decline"
|
||||
onClick={() => {
|
||||
declineCall({ callId });
|
||||
}}
|
||||
tabIndex={0}
|
||||
tooltipContent={i18n('declineCall')}
|
||||
/>
|
||||
<CallButton
|
||||
classSuffix="accept-audio"
|
||||
onClick={() => {
|
||||
acceptCall({ callId, asVideoCall: false });
|
||||
}}
|
||||
tabIndex={0}
|
||||
tooltipContent={i18n('acceptCall')}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -185,6 +185,17 @@ const COMPOSER_SHORTCUTS: Array<ShortcutType> = [
|
|||
},
|
||||
];
|
||||
|
||||
const CALLING_SHORTCUTS: Array<ShortcutType> = [
|
||||
{
|
||||
description: 'Keyboard--toggle-audio',
|
||||
keys: [['shift', 'M']],
|
||||
},
|
||||
{
|
||||
description: 'Keyboard--toggle-video',
|
||||
keys: [['shift', 'V']],
|
||||
},
|
||||
];
|
||||
|
||||
export const ShortcutGuide = (props: Props) => {
|
||||
const focusRef = React.useRef<HTMLDivElement>(null);
|
||||
const { i18n, close, hasInstalledStickers, platform } = props;
|
||||
|
@ -248,6 +259,16 @@ export const ShortcutGuide = (props: Props) => {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="module-shortcut-guide__section">
|
||||
<div className="module-shortcut-guide__section-header">
|
||||
{i18n('Keyboard--calling-header')}
|
||||
</div>
|
||||
<div className="module-shortcut-guide__section-list">
|
||||
{CALLING_SHORTCUTS.map((shortcut, index) =>
|
||||
renderShortcut(shortcut, index, isMacOS, i18n)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
94
ts/components/conversation/CallingNotification.tsx
Normal file
|
@ -0,0 +1,94 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Timestamp } from './Timestamp';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
import { CallHistoryDetailsType } from '../../services/calling';
|
||||
|
||||
export type PropsData = {
|
||||
// Can be undefined because it comes from JS.
|
||||
callHistoryDetails?: CallHistoryDetailsType;
|
||||
};
|
||||
|
||||
type PropsHousekeeping = {
|
||||
i18n: LocalizerType;
|
||||
};
|
||||
|
||||
type Props = PropsData & PropsHousekeeping;
|
||||
|
||||
function getMessage(
|
||||
callHistoryDetails: CallHistoryDetailsType,
|
||||
i18n: LocalizerType
|
||||
): string {
|
||||
const {
|
||||
wasIncoming,
|
||||
wasVideoCall,
|
||||
wasDeclined,
|
||||
acceptedTime,
|
||||
} = callHistoryDetails;
|
||||
const wasAccepted = Boolean(acceptedTime);
|
||||
|
||||
if (wasIncoming) {
|
||||
if (wasDeclined) {
|
||||
if (wasVideoCall) {
|
||||
return i18n('declinedIncomingVideoCall');
|
||||
} else {
|
||||
return i18n('declinedIncomingAudioCall');
|
||||
}
|
||||
} else if (wasAccepted) {
|
||||
if (wasVideoCall) {
|
||||
return i18n('acceptedIncomingVideoCall');
|
||||
} else {
|
||||
return i18n('acceptedIncomingAudioCall');
|
||||
}
|
||||
} else {
|
||||
if (wasVideoCall) {
|
||||
return i18n('missedIncomingVideoCall');
|
||||
} else {
|
||||
return i18n('missedIncomingAudioCall');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (wasAccepted) {
|
||||
if (wasVideoCall) {
|
||||
return i18n('acceptedOutgoingVideoCall');
|
||||
} else {
|
||||
return i18n('acceptedOutgoingAudioCall');
|
||||
}
|
||||
} else {
|
||||
if (wasVideoCall) {
|
||||
return i18n('missedOrDeclinedOutgoingVideoCall');
|
||||
} else {
|
||||
return i18n('missedOrDeclinedOutgoingAudioCall');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const CallingNotification = (props: Props): JSX.Element | null => {
|
||||
const { callHistoryDetails, i18n } = props;
|
||||
if (!callHistoryDetails) {
|
||||
return null;
|
||||
}
|
||||
const { acceptedTime, endedTime, wasVideoCall } = callHistoryDetails;
|
||||
const callType = wasVideoCall ? 'video' : 'audio';
|
||||
return (
|
||||
<div
|
||||
className={`module-message-calling--notification module-message-calling--${callType}`}
|
||||
>
|
||||
<div className={`module-message-calling--${callType}__icon`} />
|
||||
{getMessage(callHistoryDetails, i18n)}
|
||||
<div>
|
||||
<Timestamp
|
||||
i18n={i18n}
|
||||
timestamp={acceptedTime || endedTime}
|
||||
extended={true}
|
||||
direction="outgoing"
|
||||
withImageNoCaption={false}
|
||||
withSticker={false}
|
||||
withTapToViewExpired={false}
|
||||
module="module-message__metadata__date"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -34,6 +34,12 @@ const actionProps: PropsActions = {
|
|||
onDeleteMessages: action('onDeleteMessages'),
|
||||
onResetSession: action('onResetSession'),
|
||||
onSearchInConversation: action('onSearchInConversation'),
|
||||
onOutgoingAudioCallInConversation: action(
|
||||
'onOutgoingAudioCallInConversation'
|
||||
),
|
||||
onOutgoingVideoCallInConversation: action(
|
||||
'onOutgoingVideoCallInConversation'
|
||||
),
|
||||
|
||||
onShowSafetyNumber: action('onShowSafetyNumber'),
|
||||
onShowAllMedia: action('onShowAllMedia'),
|
||||
|
|
|
@ -42,6 +42,8 @@ export interface PropsActions {
|
|||
onDeleteMessages: () => void;
|
||||
onResetSession: () => void;
|
||||
onSearchInConversation: () => void;
|
||||
onOutgoingAudioCallInConversation: () => void;
|
||||
onOutgoingVideoCallInConversation: () => void;
|
||||
|
||||
onShowSafetyNumber: () => void;
|
||||
onShowAllMedia: () => void;
|
||||
|
@ -220,6 +222,54 @@ export class ConversationHeader extends React.Component<Props> {
|
|||
);
|
||||
}
|
||||
|
||||
public renderOutgoingAudioCallButton() {
|
||||
if (!window.CALLING) {
|
||||
return null;
|
||||
}
|
||||
if (this.props.isGroup || this.props.isMe) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { onOutgoingAudioCallInConversation, showBackButton } = this.props;
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={onOutgoingAudioCallInConversation}
|
||||
className={classNames(
|
||||
'module-conversation-header__audio-calling-button',
|
||||
showBackButton
|
||||
? null
|
||||
: 'module-conversation-header__audio-calling-button--show'
|
||||
)}
|
||||
disabled={showBackButton}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public renderOutgoingVideoCallButton() {
|
||||
if (!window.CALLING) {
|
||||
return null;
|
||||
}
|
||||
if (this.props.isGroup || this.props.isMe) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { onOutgoingVideoCallInConversation, showBackButton } = this.props;
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={onOutgoingVideoCallInConversation}
|
||||
className={classNames(
|
||||
'module-conversation-header__video-calling-button',
|
||||
showBackButton
|
||||
? null
|
||||
: 'module-conversation-header__video-calling-button--show'
|
||||
)}
|
||||
disabled={showBackButton}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public renderMenu(triggerId: string) {
|
||||
const {
|
||||
i18n,
|
||||
|
@ -298,6 +348,8 @@ export class ConversationHeader extends React.Component<Props> {
|
|||
</div>
|
||||
{this.renderExpirationLength()}
|
||||
{this.renderSearchButton()}
|
||||
{this.renderOutgoingVideoCallButton()}
|
||||
{this.renderOutgoingAudioCallButton()}
|
||||
{this.renderMoreButton(triggerId)}
|
||||
{this.renderMenu(triggerId)}
|
||||
</div>
|
||||
|
|
|
@ -74,17 +74,181 @@ storiesOf('Components/Conversation/TimelineItem', module)
|
|||
|
||||
return <TimelineItem {...getDefaultProps()} item={item} i18n={i18n} />;
|
||||
})
|
||||
// tslint:disable-next-line max-func-body-length
|
||||
.add('Notification', () => {
|
||||
const item = {
|
||||
type: 'timerNotification',
|
||||
data: {
|
||||
type: 'fromOther',
|
||||
phoneNumber: '(202) 555-0000',
|
||||
timespan: '1 hour',
|
||||
const items = [
|
||||
{
|
||||
type: 'timerNotification',
|
||||
data: {
|
||||
type: 'fromOther',
|
||||
phoneNumber: '(202) 555-0000',
|
||||
timespan: '1 hour',
|
||||
},
|
||||
},
|
||||
} as TimelineItemProps['item'];
|
||||
{
|
||||
type: 'callHistory',
|
||||
data: {
|
||||
callHistoryDetails: {
|
||||
// declined incoming audio
|
||||
wasDeclined: true,
|
||||
wasIncoming: true,
|
||||
wasVideoCall: false,
|
||||
endedTime: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'callHistory',
|
||||
data: {
|
||||
callHistoryDetails: {
|
||||
// declined incoming video
|
||||
wasDeclined: true,
|
||||
wasIncoming: true,
|
||||
wasVideoCall: true,
|
||||
endedTime: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'callHistory',
|
||||
data: {
|
||||
callHistoryDetails: {
|
||||
// accepted incoming audio
|
||||
acceptedTime: Date.now() - 300,
|
||||
wasDeclined: false,
|
||||
wasIncoming: true,
|
||||
wasVideoCall: false,
|
||||
endedTime: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'callHistory',
|
||||
data: {
|
||||
callHistoryDetails: {
|
||||
// accepted incoming video
|
||||
acceptedTime: Date.now() - 400,
|
||||
wasDeclined: false,
|
||||
wasIncoming: true,
|
||||
wasVideoCall: true,
|
||||
endedTime: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'callHistory',
|
||||
data: {
|
||||
callHistoryDetails: {
|
||||
// missed (neither accepted nor declined) incoming audio
|
||||
wasDeclined: false,
|
||||
wasIncoming: true,
|
||||
wasVideoCall: false,
|
||||
endedTime: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'callHistory',
|
||||
data: {
|
||||
callHistoryDetails: {
|
||||
// missed (neither accepted nor declined) incoming video
|
||||
wasDeclined: false,
|
||||
wasIncoming: true,
|
||||
wasVideoCall: true,
|
||||
endedTime: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'callHistory',
|
||||
data: {
|
||||
callHistoryDetails: {
|
||||
// accepted outgoing audio
|
||||
acceptedTime: Date.now() - 200,
|
||||
wasDeclined: false,
|
||||
wasIncoming: false,
|
||||
wasVideoCall: true,
|
||||
endedTime: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'callHistory',
|
||||
data: {
|
||||
callHistoryDetails: {
|
||||
// accepted outgoing video
|
||||
acceptedTime: Date.now() - 200,
|
||||
wasDeclined: false,
|
||||
wasIncoming: false,
|
||||
wasVideoCall: true,
|
||||
endedTime: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'callHistory',
|
||||
callHistoryDetails: {
|
||||
data: {
|
||||
// declined outgoing audio
|
||||
wasDeclined: true,
|
||||
wasIncoming: false,
|
||||
wasVideoCall: false,
|
||||
endedTime: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'callHistory',
|
||||
data: {
|
||||
callHistoryDetails: {
|
||||
// declined outgoing video
|
||||
wasDeclined: true,
|
||||
wasIncoming: false,
|
||||
wasVideoCall: true,
|
||||
endedTime: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'callHistory',
|
||||
data: {
|
||||
callHistoryDetails: {
|
||||
// missed (neither accepted nor declined) outgoing audio
|
||||
wasDeclined: false,
|
||||
wasIncoming: false,
|
||||
wasVideoCall: false,
|
||||
endedTime: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'callHistory',
|
||||
data: {
|
||||
callHistoryDetails: {
|
||||
// missed (neither accepted nor declined) outgoing video
|
||||
wasDeclined: false,
|
||||
wasIncoming: false,
|
||||
wasVideoCall: true,
|
||||
endedTime: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return <TimelineItem {...getDefaultProps()} item={item} i18n={i18n} />;
|
||||
return (
|
||||
<>
|
||||
{items.map(item => (
|
||||
<>
|
||||
<TimelineItem
|
||||
{...getDefaultProps()}
|
||||
item={item as TimelineItemProps['item']}
|
||||
i18n={i18n}
|
||||
/>
|
||||
<hr />
|
||||
</>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
})
|
||||
.add('Unknown Type', () => {
|
||||
// @ts-ignore: intentional
|
||||
|
|
|
@ -8,6 +8,10 @@ import {
|
|||
PropsData as MessageProps,
|
||||
} from './Message';
|
||||
|
||||
import {
|
||||
CallingNotification,
|
||||
PropsData as CallingNotificationProps,
|
||||
} from './CallingNotification';
|
||||
import { InlineNotificationWrapper } from './InlineNotificationWrapper';
|
||||
import {
|
||||
PropsActions as UnsupportedMessageActionsType,
|
||||
|
@ -33,6 +37,10 @@ import {
|
|||
} from './GroupNotification';
|
||||
import { ResetSessionNotification } from './ResetSessionNotification';
|
||||
|
||||
type CallHistoryType = {
|
||||
type: 'callHistory';
|
||||
data: CallingNotificationProps;
|
||||
};
|
||||
type LinkNotificationType = {
|
||||
type: 'linkNotification';
|
||||
data: null;
|
||||
|
@ -66,6 +74,7 @@ type ResetSessionNotificationType = {
|
|||
data: null;
|
||||
};
|
||||
export type TimelineItemType =
|
||||
| CallHistoryType
|
||||
| LinkNotificationType
|
||||
| MessageType
|
||||
| ResetSessionNotificationType
|
||||
|
@ -121,6 +130,8 @@ export class TimelineItem extends React.PureComponent<PropsType> {
|
|||
notification = (
|
||||
<UnsupportedMessage {...this.props} {...item.data} i18n={i18n} />
|
||||
);
|
||||
} else if (item.type === 'callHistory') {
|
||||
notification = <CallingNotification i18n={i18n} {...item.data} />;
|
||||
} else if (item.type === 'linkNotification') {
|
||||
notification = (
|
||||
<div className="module-message-unsynced">
|
||||
|
|
34
ts/services/bounce.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { app, BrowserWindow, ipcMain } from 'electron';
|
||||
|
||||
let bounceId = -1;
|
||||
|
||||
export function init(win: BrowserWindow) {
|
||||
ipcMain.on('bounce-app-icon-start', (_, isCritical = false) => {
|
||||
if (app.dock) {
|
||||
const type = isCritical ? 'critical' : 'informational';
|
||||
bounceId = app.dock.bounce(type);
|
||||
|
||||
if (bounceId < 0) {
|
||||
return;
|
||||
}
|
||||
} else if (win && win.flashFrame) {
|
||||
win.once('focus', () => {
|
||||
win.flashFrame(false);
|
||||
});
|
||||
win.flashFrame(true);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on('bounce-app-icon-stop', () => {
|
||||
if (app.dock) {
|
||||
if (bounceId < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
app.dock.cancelBounce(bounceId);
|
||||
bounceId = -1;
|
||||
} else if (win && win.flashFrame) {
|
||||
win.flashFrame(false);
|
||||
}
|
||||
});
|
||||
}
|
464
ts/services/calling.ts
Normal file
|
@ -0,0 +1,464 @@
|
|||
import {
|
||||
Call,
|
||||
CallEndedReason,
|
||||
CallId,
|
||||
CallLogLevel,
|
||||
CallSettings,
|
||||
CallState,
|
||||
DeviceId,
|
||||
RingRTC,
|
||||
UserId,
|
||||
VideoCapturer,
|
||||
VideoRenderer,
|
||||
} from 'ringrtc';
|
||||
import {
|
||||
ActionsType as UxActionsType,
|
||||
CallDetailsType,
|
||||
} from '../state/ducks/calling';
|
||||
import { CallingMessageClass, EnvelopeClass } from '../textsecure.d';
|
||||
import { ConversationType } from '../window.d';
|
||||
import is from '@sindresorhus/is';
|
||||
|
||||
export {
|
||||
CallState,
|
||||
CanvasVideoRenderer,
|
||||
GumVideoCapturer,
|
||||
VideoCapturer,
|
||||
VideoRenderer,
|
||||
} from 'ringrtc';
|
||||
|
||||
export type CallHistoryDetailsType = {
|
||||
wasIncoming: boolean;
|
||||
wasVideoCall: boolean;
|
||||
wasDeclined: boolean;
|
||||
acceptedTime?: number;
|
||||
endedTime: number;
|
||||
};
|
||||
|
||||
export class CallingClass {
|
||||
private uxActions?: UxActionsType;
|
||||
|
||||
initialize(uxActions: UxActionsType): void {
|
||||
this.uxActions = uxActions;
|
||||
if (!uxActions) {
|
||||
throw new Error('CallingClass.initialize: Invalid uxActions.');
|
||||
}
|
||||
if (!is.function_(uxActions.incomingCall)) {
|
||||
throw new Error(
|
||||
'CallingClass.initialize: Invalid uxActions.incomingCall'
|
||||
);
|
||||
}
|
||||
if (!is.function_(uxActions.outgoingCall)) {
|
||||
throw new Error(
|
||||
'CallingClass.initialize: Invalid uxActions.outgoingCall'
|
||||
);
|
||||
}
|
||||
if (!is.function_(uxActions.callStateChange)) {
|
||||
throw new Error(
|
||||
'CallingClass.initialize: Invalid uxActions.callStateChange'
|
||||
);
|
||||
}
|
||||
if (!is.function_(uxActions.remoteVideoChange)) {
|
||||
throw new Error(
|
||||
'CallingClass.initialize: Invalid uxActions.remoteVideoChange'
|
||||
);
|
||||
}
|
||||
RingRTC.handleOutgoingSignaling = this.handleOutgoingSignaling.bind(this);
|
||||
RingRTC.handleIncomingCall = this.handleIncomingCall.bind(this);
|
||||
RingRTC.handleAutoEndedIncomingCallRequest = this.handleAutoEndedIncomingCallRequest.bind(
|
||||
this
|
||||
);
|
||||
RingRTC.handleLogMessage = this.handleLogMessage.bind(this);
|
||||
}
|
||||
|
||||
async startOutgoingCall(
|
||||
conversation: ConversationType,
|
||||
isVideoCall: boolean
|
||||
) {
|
||||
if (!this.uxActions) {
|
||||
window.log.error('Missing uxActions, new call not allowed.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (RingRTC.call && RingRTC.call.state !== CallState.Ended) {
|
||||
window.log.info('Call already in progress, new call not allowed.');
|
||||
return;
|
||||
}
|
||||
|
||||
const remoteUserId = this.getRemoteUserIdFromConversation(conversation);
|
||||
if (!remoteUserId || !this.localDeviceId) {
|
||||
window.log.error('Missing identifier, new call not allowed.');
|
||||
return;
|
||||
}
|
||||
|
||||
const haveMediaPermissions = await this.requestPermissions(isVideoCall);
|
||||
if (!haveMediaPermissions) {
|
||||
window.log.info('Permissions were denied, new call not allowed.');
|
||||
return;
|
||||
}
|
||||
|
||||
// We could make this faster by getting the call object
|
||||
// from the RingRTC before we lookup the ICE servers.
|
||||
const call = RingRTC.startOutgoingCall(
|
||||
remoteUserId,
|
||||
isVideoCall,
|
||||
this.localDeviceId,
|
||||
await this.getCallSettings(conversation)
|
||||
);
|
||||
|
||||
this.attachToCall(conversation, call);
|
||||
|
||||
this.uxActions.outgoingCall({
|
||||
callDetails: this.getUxCallDetails(conversation, call),
|
||||
});
|
||||
}
|
||||
|
||||
async accept(callId: CallId, asVideoCall: boolean) {
|
||||
const haveMediaPermissions = await this.requestPermissions(asVideoCall);
|
||||
if (haveMediaPermissions) {
|
||||
RingRTC.accept(callId, asVideoCall);
|
||||
} else {
|
||||
window.log.info('Permissions were denied, call not allowed, hanging up.');
|
||||
RingRTC.hangup(callId);
|
||||
}
|
||||
}
|
||||
|
||||
decline(callId: CallId) {
|
||||
RingRTC.decline(callId);
|
||||
}
|
||||
|
||||
hangup(callId: CallId) {
|
||||
RingRTC.hangup(callId);
|
||||
}
|
||||
|
||||
setOutgoingAudio(callId: CallId, enabled: boolean) {
|
||||
RingRTC.setOutgoingAudio(callId, enabled);
|
||||
}
|
||||
|
||||
setOutgoingVideo(callId: CallId, enabled: boolean) {
|
||||
RingRTC.setOutgoingVideo(callId, enabled);
|
||||
}
|
||||
|
||||
setVideoCapturer(callId: CallId, capturer: VideoCapturer | null) {
|
||||
RingRTC.setVideoCapturer(callId, capturer);
|
||||
}
|
||||
|
||||
setVideoRenderer(callId: CallId, renderer: VideoRenderer | null) {
|
||||
RingRTC.setVideoRenderer(callId, renderer);
|
||||
}
|
||||
|
||||
async handleCallingMessage(
|
||||
envelope: EnvelopeClass,
|
||||
callingMessage: CallingMessageClass
|
||||
) {
|
||||
const enableIncomingCalls = await window.getIncomingCallNotification();
|
||||
if (callingMessage.offer && !enableIncomingCalls) {
|
||||
// Drop offers silently if incoming call notifications are disabled.
|
||||
window.log.info('Incoming calls are disabled, ignoring call offer.');
|
||||
return;
|
||||
}
|
||||
|
||||
const remoteUserId = envelope.source || envelope.sourceUuid;
|
||||
const remoteDeviceId = this.parseDeviceId(envelope.sourceDevice);
|
||||
if (!remoteUserId || !remoteDeviceId || !this.localDeviceId) {
|
||||
window.log.error('Missing identifier, ignoring call message.');
|
||||
return;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const serverTimestamp = envelope.serverTimestamp
|
||||
? envelope.serverTimestamp
|
||||
: now.valueOf();
|
||||
const messageAgeSec = Math.floor((now.valueOf() - serverTimestamp) / 1000);
|
||||
|
||||
RingRTC.handleCallingMessage(
|
||||
remoteUserId,
|
||||
remoteDeviceId,
|
||||
this.localDeviceId,
|
||||
messageAgeSec,
|
||||
callingMessage
|
||||
);
|
||||
}
|
||||
|
||||
private async requestCameraPermissions(): Promise<boolean> {
|
||||
const cameraPermission = await window.getMediaCameraPermissions();
|
||||
if (!cameraPermission) {
|
||||
await window.showCallingPermissionsPopup(true);
|
||||
|
||||
// Check the setting again (from the source of truth).
|
||||
return window.getMediaCameraPermissions();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async requestMicrophonePermissions(): Promise<boolean> {
|
||||
const microphonePermission = await window.getMediaPermissions();
|
||||
if (!microphonePermission) {
|
||||
await window.showCallingPermissionsPopup(false);
|
||||
|
||||
// Check the setting again (from the source of truth).
|
||||
return window.getMediaPermissions();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async requestPermissions(isVideoCall: boolean): Promise<boolean> {
|
||||
const microphonePermission = await this.requestMicrophonePermissions();
|
||||
if (microphonePermission) {
|
||||
if (isVideoCall) {
|
||||
return this.requestCameraPermissions();
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async handleOutgoingSignaling(
|
||||
remoteUserId: UserId,
|
||||
message: CallingMessageClass
|
||||
): Promise<void> {
|
||||
const conversation = window.ConversationController.get(remoteUserId);
|
||||
const sendOptions = conversation ? conversation.getSendOptions() : {};
|
||||
|
||||
try {
|
||||
await window.textsecure.messaging.sendCallingMessage(
|
||||
remoteUserId,
|
||||
message,
|
||||
sendOptions
|
||||
);
|
||||
|
||||
window.log.info('handleOutgoingSignaling() completed successfully');
|
||||
} catch (err) {
|
||||
if (err && err.errors && err.errors.length > 0) {
|
||||
window.log.error(
|
||||
`handleOutgoingSignaling() failed: ${err.errors[0].reason}`
|
||||
);
|
||||
} else {
|
||||
window.log.error('handleOutgoingSignaling() failed');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async handleIncomingCall(call: Call): Promise<CallSettings | null> {
|
||||
if (!this.uxActions || !this.localDeviceId) {
|
||||
window.log.error('Missing required objects, ignoring incoming call.');
|
||||
return null;
|
||||
}
|
||||
|
||||
const conversation = window.ConversationController.get(call.remoteUserId);
|
||||
if (!conversation) {
|
||||
window.log.error('Missing conversation, ignoring incoming call.');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// The peer must be 'trusted' before accepting a call from them.
|
||||
// This is mostly the safety number check, unverified meaning that they were
|
||||
// verified before but now they are not.
|
||||
const verifiedEnum = await conversation.safeGetVerified();
|
||||
if (
|
||||
verifiedEnum ===
|
||||
window.textsecure.storage.protocol.VerifiedStatus.UNVERIFIED
|
||||
) {
|
||||
window.log.info('Peer is not trusted, ignoring incoming call.');
|
||||
this.addCallHistoryForFailedIncomingCall(conversation, call);
|
||||
return null;
|
||||
}
|
||||
|
||||
this.attachToCall(conversation, call);
|
||||
|
||||
this.uxActions.incomingCall({
|
||||
callDetails: this.getUxCallDetails(conversation, call),
|
||||
});
|
||||
|
||||
return await this.getCallSettings(conversation);
|
||||
} catch (err) {
|
||||
window.log.error(`Ignoring incoming call: ${err.stack}`);
|
||||
this.addCallHistoryForFailedIncomingCall(conversation, call);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private handleAutoEndedIncomingCallRequest(
|
||||
remoteUserId: UserId,
|
||||
reason: CallEndedReason
|
||||
) {
|
||||
const conversation = window.ConversationController.get(remoteUserId);
|
||||
if (!conversation) {
|
||||
return;
|
||||
}
|
||||
this.addCallHistoryForAutoEndedIncomingCall(conversation, reason);
|
||||
}
|
||||
|
||||
private attachToCall(conversation: ConversationType, call: Call): void {
|
||||
const { uxActions } = this;
|
||||
if (!uxActions) {
|
||||
return;
|
||||
}
|
||||
|
||||
let acceptedTime: number | undefined;
|
||||
|
||||
call.handleStateChanged = () => {
|
||||
if (call.state === CallState.Accepted) {
|
||||
acceptedTime = Date.now();
|
||||
} else if (call.state === CallState.Ended) {
|
||||
this.addCallHistoryForEndedCall(conversation, call, acceptedTime);
|
||||
}
|
||||
uxActions.callStateChange({
|
||||
callState: call.state,
|
||||
callDetails: this.getUxCallDetails(conversation, call),
|
||||
});
|
||||
};
|
||||
|
||||
call.handleRemoteVideoEnabled = () => {
|
||||
uxActions.remoteVideoChange({
|
||||
remoteVideoEnabled: call.remoteVideoEnabled,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
private async handleLogMessage(
|
||||
level: CallLogLevel,
|
||||
fileName: string,
|
||||
line: number,
|
||||
message: string
|
||||
) {
|
||||
// info/warn/error are only needed to be logged for now.
|
||||
// tslint:disable-next-line switch-default
|
||||
switch (level) {
|
||||
case CallLogLevel.Info:
|
||||
window.log.info(`${fileName}:${line} ${message}`);
|
||||
break;
|
||||
case CallLogLevel.Warn:
|
||||
window.log.warn(`${fileName}:${line} ${message}`);
|
||||
break;
|
||||
case CallLogLevel.Error:
|
||||
window.log.error(`${fileName}:${line} ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private getRemoteUserIdFromConversation(
|
||||
conversation: ConversationType
|
||||
): UserId | undefined {
|
||||
const recipients = conversation.getRecipients();
|
||||
if (recipients.length !== 1) {
|
||||
return undefined;
|
||||
}
|
||||
return recipients[0];
|
||||
}
|
||||
|
||||
private get localDeviceId(): DeviceId | null {
|
||||
return this.parseDeviceId(window.textsecure.storage.user.getDeviceId());
|
||||
}
|
||||
|
||||
private parseDeviceId(
|
||||
deviceId: number | string | undefined
|
||||
): DeviceId | null {
|
||||
if (typeof deviceId === 'string') {
|
||||
return parseInt(deviceId, 10);
|
||||
}
|
||||
if (typeof deviceId === 'number') {
|
||||
return deviceId;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async getCallSettings(
|
||||
conversation: ConversationType
|
||||
): Promise<CallSettings> {
|
||||
const iceServerJson = await window.textsecure.messaging.server.getIceServers();
|
||||
|
||||
const shouldRelayCalls = Boolean(await window.getAlwaysRelayCalls());
|
||||
|
||||
// If the peer is 'unknown', i.e. not in the contact list, force IP hiding.
|
||||
const isContactUnknown = !conversation.getIsAddedByContact();
|
||||
|
||||
return {
|
||||
iceServer: JSON.parse(iceServerJson),
|
||||
hideIp: shouldRelayCalls || isContactUnknown,
|
||||
};
|
||||
}
|
||||
|
||||
private getUxCallDetails(
|
||||
conversation: ConversationType,
|
||||
call: Call
|
||||
): CallDetailsType {
|
||||
return {
|
||||
avatarPath: conversation.getAvatarPath(),
|
||||
callId: call.callId,
|
||||
contactColor: conversation.getColor(),
|
||||
isIncoming: call.isIncoming,
|
||||
isVideoCall: call.isVideoCall,
|
||||
name: conversation.getName(),
|
||||
phoneNumber: conversation.getNumber(),
|
||||
profileName: conversation.getProfileName(),
|
||||
};
|
||||
}
|
||||
|
||||
private addCallHistoryForEndedCall(
|
||||
conversation: ConversationType,
|
||||
call: Call,
|
||||
acceptedTime: number | undefined
|
||||
) {
|
||||
const { endedReason, isIncoming } = call;
|
||||
const wasAccepted = Boolean(acceptedTime);
|
||||
const isOutgoing = !isIncoming;
|
||||
const wasDeclined =
|
||||
!wasAccepted &&
|
||||
(endedReason === CallEndedReason.Declined ||
|
||||
endedReason === CallEndedReason.DeclinedOnAnotherDevice ||
|
||||
(isIncoming && endedReason === CallEndedReason.LocalHangup) ||
|
||||
(isOutgoing && endedReason === CallEndedReason.RemoteHangup));
|
||||
if (call.endedReason === CallEndedReason.AcceptedOnAnotherDevice) {
|
||||
// tslint:disable-next-line no-parameter-reassignment
|
||||
acceptedTime = Date.now();
|
||||
}
|
||||
|
||||
const callHistoryDetails: CallHistoryDetailsType = {
|
||||
wasIncoming: call.isIncoming,
|
||||
wasVideoCall: call.isVideoCall,
|
||||
wasDeclined,
|
||||
acceptedTime,
|
||||
endedTime: Date.now(),
|
||||
};
|
||||
conversation.addCallHistory(callHistoryDetails);
|
||||
}
|
||||
|
||||
private addCallHistoryForFailedIncomingCall(
|
||||
conversation: ConversationType,
|
||||
call: Call
|
||||
) {
|
||||
const callHistoryDetails: CallHistoryDetailsType = {
|
||||
wasIncoming: true,
|
||||
wasVideoCall: call.isVideoCall,
|
||||
// Since the user didn't decline, make sure it shows up as a missed call instead
|
||||
wasDeclined: false,
|
||||
acceptedTime: undefined,
|
||||
endedTime: Date.now(),
|
||||
};
|
||||
conversation.addCallHistory(callHistoryDetails);
|
||||
}
|
||||
|
||||
private addCallHistoryForAutoEndedIncomingCall(
|
||||
conversation: ConversationType,
|
||||
_reason: CallEndedReason
|
||||
) {
|
||||
const callHistoryDetails: CallHistoryDetailsType = {
|
||||
wasIncoming: true,
|
||||
// We don't actually know, but it doesn't seem that important in this case,
|
||||
// but we could maybe plumb this info through RingRTC
|
||||
wasVideoCall: false,
|
||||
// Since the user didn't decline, make sure it shows up as a missed call instead
|
||||
wasDeclined: false,
|
||||
acceptedTime: undefined,
|
||||
endedTime: Date.now(),
|
||||
};
|
||||
conversation.addCallHistory(callHistoryDetails);
|
||||
}
|
||||
}
|
||||
|
||||
export const calling = new CallingClass();
|
34
ts/services/notify.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
function filter(text: string) {
|
||||
return (text || '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
|
||||
type NotificationType = {
|
||||
platform: string;
|
||||
icon: string;
|
||||
message: string;
|
||||
onNotificationClick: () => void;
|
||||
silent: boolean;
|
||||
title: string;
|
||||
};
|
||||
|
||||
export function notify({
|
||||
platform,
|
||||
icon,
|
||||
message,
|
||||
onNotificationClick,
|
||||
silent,
|
||||
title,
|
||||
}: NotificationType): Notification {
|
||||
const notification = new window.Notification(title, {
|
||||
body: platform === 'linux' ? filter(message) : message,
|
||||
icon,
|
||||
silent,
|
||||
});
|
||||
notification.onclick = onNotificationClick;
|
||||
return notification;
|
||||
}
|
9
ts/shims/bounceAppIcon.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { ipcRenderer } from 'electron';
|
||||
|
||||
export function bounceAppIconStart(isCritical = false) {
|
||||
ipcRenderer.send('bounce-app-icon-start', isCritical);
|
||||
}
|
||||
|
||||
export function bounceAppIconStop() {
|
||||
ipcRenderer.send('bounce-app-icon-stop');
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
import { actions as calling } from './ducks/calling';
|
||||
import { actions as conversations } from './ducks/conversations';
|
||||
import { actions as emojis } from './ducks/emojis';
|
||||
import { actions as expiration } from './ducks/expiration';
|
||||
|
@ -9,6 +10,7 @@ import { actions as updates } from './ducks/updates';
|
|||
import { actions as user } from './ducks/user';
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
...calling,
|
||||
...conversations,
|
||||
...emojis,
|
||||
...expiration,
|
||||
|
|
417
ts/state/ducks/calling.ts
Normal file
|
@ -0,0 +1,417 @@
|
|||
import { notify } from '../../services/notify';
|
||||
import { calling, VideoCapturer, VideoRenderer } from '../../services/calling';
|
||||
import { CallState } from '../../types/Calling';
|
||||
import { CanvasVideoRenderer, GumVideoCapturer } from '../../window.d';
|
||||
import { ColorType } from '../../types/Util';
|
||||
import { NoopActionType } from './noop';
|
||||
import { callingTones } from '../../util/callingTones';
|
||||
import { requestCameraPermissions } from '../../util/callingPermissions';
|
||||
import {
|
||||
bounceAppIconStart,
|
||||
bounceAppIconStop,
|
||||
} from '../../shims/bounceAppIcon';
|
||||
|
||||
// State
|
||||
|
||||
export type CallId = any;
|
||||
|
||||
export type CallDetailsType = {
|
||||
avatarPath?: string;
|
||||
callId: CallId;
|
||||
contactColor?: ColorType;
|
||||
isIncoming: boolean;
|
||||
isVideoCall: boolean;
|
||||
name?: string;
|
||||
phoneNumber: string;
|
||||
profileName?: string;
|
||||
};
|
||||
|
||||
export type CallingStateType = {
|
||||
callDetails?: CallDetailsType;
|
||||
callState?: CallState;
|
||||
hasLocalAudio: boolean;
|
||||
hasLocalVideo: boolean;
|
||||
hasRemoteVideo: boolean;
|
||||
};
|
||||
|
||||
export type AcceptCallType = {
|
||||
callId: CallId;
|
||||
asVideoCall: boolean;
|
||||
};
|
||||
|
||||
export type CallStateChangeType = {
|
||||
callState: CallState;
|
||||
callDetails: CallDetailsType;
|
||||
};
|
||||
|
||||
export type DeclineCallType = {
|
||||
callId: CallId;
|
||||
};
|
||||
|
||||
export type HangUpType = {
|
||||
callId: CallId;
|
||||
};
|
||||
|
||||
export type IncomingCallType = {
|
||||
callDetails: CallDetailsType;
|
||||
};
|
||||
|
||||
export type OutgoingCallType = {
|
||||
callDetails: CallDetailsType;
|
||||
};
|
||||
|
||||
export type RemoteVideoChangeType = {
|
||||
remoteVideoEnabled: boolean;
|
||||
};
|
||||
|
||||
export type SetLocalAudioType = {
|
||||
callId: CallId;
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
export type SetLocalVideoType = {
|
||||
callId: CallId;
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
export type SetVideoCapturerType = {
|
||||
callId: CallId;
|
||||
capturer: CanvasVideoRenderer | null;
|
||||
};
|
||||
|
||||
export type SetVideoRendererType = {
|
||||
callId: CallId;
|
||||
renderer: GumVideoCapturer | null;
|
||||
};
|
||||
|
||||
// Actions
|
||||
|
||||
const ACCEPT_CALL = 'calling/ACCEPT_CALL';
|
||||
const CALL_STATE_CHANGE = 'calling/CALL_STATE_CHANGE';
|
||||
const DECLINE_CALL = 'calling/DECLINE_CALL';
|
||||
const HANG_UP = 'calling/HANG_UP';
|
||||
const INCOMING_CALL = 'calling/INCOMING_CALL';
|
||||
const OUTGOING_CALL = 'calling/OUTGOING_CALL';
|
||||
const REMOTE_VIDEO_CHANGE = 'calling/REMOTE_VIDEO_CHANGE';
|
||||
const SET_LOCAL_AUDIO = 'calling/SET_LOCAL_AUDIO';
|
||||
const SET_LOCAL_VIDEO = 'calling/SET_LOCAL_VIDEO';
|
||||
const SET_LOCAL_VIDEO_FULFILLED = 'calling/SET_LOCAL_VIDEO_FULFILLED';
|
||||
|
||||
type AcceptCallActionType = {
|
||||
type: 'calling/ACCEPT_CALL';
|
||||
payload: AcceptCallType;
|
||||
};
|
||||
|
||||
type CallStateChangeActionType = {
|
||||
type: 'calling/CALL_STATE_CHANGE';
|
||||
payload: CallStateChangeType;
|
||||
};
|
||||
|
||||
type DeclineCallActionType = {
|
||||
type: 'calling/DECLINE_CALL';
|
||||
payload: DeclineCallType;
|
||||
};
|
||||
|
||||
type HangUpActionType = {
|
||||
type: 'calling/HANG_UP';
|
||||
payload: HangUpType;
|
||||
};
|
||||
|
||||
type IncomingCallActionType = {
|
||||
type: 'calling/INCOMING_CALL';
|
||||
payload: IncomingCallType;
|
||||
};
|
||||
|
||||
type OutgoingCallActionType = {
|
||||
type: 'calling/OUTGOING_CALL';
|
||||
payload: OutgoingCallType;
|
||||
};
|
||||
|
||||
type RemoteVideoChangeActionType = {
|
||||
type: 'calling/REMOTE_VIDEO_CHANGE';
|
||||
payload: RemoteVideoChangeType;
|
||||
};
|
||||
|
||||
type SetLocalAudioActionType = {
|
||||
type: 'calling/SET_LOCAL_AUDIO';
|
||||
payload: SetLocalAudioType;
|
||||
};
|
||||
|
||||
type SetLocalVideoActionType = {
|
||||
type: 'calling/SET_LOCAL_VIDEO';
|
||||
payload: Promise<SetLocalVideoType>;
|
||||
};
|
||||
|
||||
type SetLocalVideoFulfilledActionType = {
|
||||
type: 'calling/SET_LOCAL_VIDEO_FULFILLED';
|
||||
payload: SetLocalVideoType;
|
||||
};
|
||||
|
||||
export type CallingActionType =
|
||||
| AcceptCallActionType
|
||||
| CallStateChangeActionType
|
||||
| DeclineCallActionType
|
||||
| HangUpActionType
|
||||
| IncomingCallActionType
|
||||
| OutgoingCallActionType
|
||||
| RemoteVideoChangeActionType
|
||||
| SetLocalAudioActionType
|
||||
| SetLocalVideoActionType
|
||||
| SetLocalVideoFulfilledActionType;
|
||||
|
||||
// Action Creators
|
||||
|
||||
function acceptCall(
|
||||
payload: AcceptCallType
|
||||
): AcceptCallActionType | NoopActionType {
|
||||
// tslint:disable-next-line no-floating-promises
|
||||
(async () => {
|
||||
try {
|
||||
await calling.accept(payload.callId, payload.asVideoCall);
|
||||
} catch (err) {
|
||||
window.log.error(`Failed to acceptCall: ${err.stack}`);
|
||||
}
|
||||
})();
|
||||
|
||||
return {
|
||||
type: ACCEPT_CALL,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
function callStateChange(
|
||||
payload: CallStateChangeType
|
||||
): CallStateChangeActionType {
|
||||
const { callDetails, callState } = payload;
|
||||
const { isIncoming } = callDetails;
|
||||
if (callState === CallState.Ringing && isIncoming) {
|
||||
// tslint:disable-next-line no-floating-promises
|
||||
callingTones.playRingtone();
|
||||
// tslint:disable-next-line no-floating-promises
|
||||
showCallNotification(callDetails);
|
||||
bounceAppIconStart();
|
||||
}
|
||||
if (callState !== CallState.Ringing) {
|
||||
callingTones.stopRingtone();
|
||||
bounceAppIconStop();
|
||||
}
|
||||
if (callState === CallState.Ended) {
|
||||
// tslint:disable-next-line no-floating-promises
|
||||
callingTones.playEndCall();
|
||||
}
|
||||
|
||||
return {
|
||||
type: CALL_STATE_CHANGE,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
async function showCallNotification(callDetails: CallDetailsType) {
|
||||
const canNotify = await window.getCallSystemNotification();
|
||||
if (!canNotify) {
|
||||
return;
|
||||
}
|
||||
const { name, phoneNumber, profileName, isVideoCall } = callDetails;
|
||||
notify({
|
||||
platform: window.platform,
|
||||
title: `${name || phoneNumber} ${profileName || ''}`,
|
||||
icon: isVideoCall
|
||||
? 'images/icons/v2/video-solid-24.svg'
|
||||
: 'images/icons/v2/phone-right-solid-24.svg',
|
||||
message: window.i18n(
|
||||
isVideoCall ? 'incomingVideoCall' : 'incomingAudioCall'
|
||||
),
|
||||
onNotificationClick: () => {
|
||||
window.showWindow();
|
||||
},
|
||||
silent: false,
|
||||
});
|
||||
}
|
||||
|
||||
function declineCall(payload: DeclineCallType): DeclineCallActionType {
|
||||
calling.decline(payload.callId);
|
||||
|
||||
return {
|
||||
type: DECLINE_CALL,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
function hangUp(payload: HangUpType): HangUpActionType {
|
||||
calling.hangup(payload.callId);
|
||||
|
||||
return {
|
||||
type: HANG_UP,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
function incomingCall(payload: IncomingCallType): IncomingCallActionType {
|
||||
return {
|
||||
type: INCOMING_CALL,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
function outgoingCall(payload: OutgoingCallType): OutgoingCallActionType {
|
||||
// tslint:disable-next-line no-floating-promises
|
||||
callingTones.playRingtone();
|
||||
|
||||
return {
|
||||
type: OUTGOING_CALL,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
function remoteVideoChange(
|
||||
payload: RemoteVideoChangeType
|
||||
): RemoteVideoChangeActionType {
|
||||
return {
|
||||
type: REMOTE_VIDEO_CHANGE,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
function setVideoCapturer(payload: SetVideoCapturerType): NoopActionType {
|
||||
calling.setVideoCapturer(payload.callId, payload.capturer as VideoCapturer);
|
||||
|
||||
return {
|
||||
type: 'NOOP',
|
||||
payload: null,
|
||||
};
|
||||
}
|
||||
|
||||
function setVideoRenderer(payload: SetVideoRendererType): NoopActionType {
|
||||
calling.setVideoRenderer(payload.callId, payload.renderer as VideoRenderer);
|
||||
|
||||
return {
|
||||
type: 'NOOP',
|
||||
payload: null,
|
||||
};
|
||||
}
|
||||
|
||||
function setLocalAudio(payload: SetLocalAudioType): SetLocalAudioActionType {
|
||||
calling.setOutgoingAudio(payload.callId, payload.enabled);
|
||||
|
||||
return {
|
||||
type: SET_LOCAL_AUDIO,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
function setLocalVideo(payload: SetLocalVideoType): SetLocalVideoActionType {
|
||||
return {
|
||||
type: SET_LOCAL_VIDEO,
|
||||
payload: doSetLocalVideo(payload),
|
||||
};
|
||||
}
|
||||
|
||||
async function doSetLocalVideo(
|
||||
payload: SetLocalVideoType
|
||||
): Promise<SetLocalVideoType> {
|
||||
if (await requestCameraPermissions()) {
|
||||
calling.setOutgoingVideo(payload.callId, payload.enabled);
|
||||
return payload;
|
||||
}
|
||||
|
||||
return {
|
||||
...payload,
|
||||
enabled: false,
|
||||
};
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
acceptCall,
|
||||
callStateChange,
|
||||
declineCall,
|
||||
hangUp,
|
||||
incomingCall,
|
||||
outgoingCall,
|
||||
remoteVideoChange,
|
||||
setVideoCapturer,
|
||||
setVideoRenderer,
|
||||
setLocalAudio,
|
||||
setLocalVideo,
|
||||
};
|
||||
|
||||
export type ActionsType = typeof actions;
|
||||
|
||||
// Reducer
|
||||
|
||||
function getEmptyState(): CallingStateType {
|
||||
return {
|
||||
callDetails: undefined,
|
||||
callState: undefined,
|
||||
hasLocalAudio: false,
|
||||
hasLocalVideo: false,
|
||||
hasRemoteVideo: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function reducer(
|
||||
state: CallingStateType = getEmptyState(),
|
||||
action: CallingActionType
|
||||
): CallingStateType {
|
||||
if (action.type === ACCEPT_CALL) {
|
||||
return {
|
||||
...state,
|
||||
hasLocalAudio: true,
|
||||
hasLocalVideo: action.payload.asVideoCall,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === DECLINE_CALL || action.type === HANG_UP) {
|
||||
return getEmptyState();
|
||||
}
|
||||
|
||||
if (action.type === INCOMING_CALL) {
|
||||
return {
|
||||
...state,
|
||||
callDetails: action.payload.callDetails,
|
||||
callState: CallState.Prering,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === OUTGOING_CALL) {
|
||||
return {
|
||||
...state,
|
||||
callDetails: action.payload.callDetails,
|
||||
callState: CallState.Prering,
|
||||
hasLocalAudio: true,
|
||||
hasLocalVideo: action.payload.callDetails.isVideoCall,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === CALL_STATE_CHANGE) {
|
||||
if (action.payload.callState === CallState.Ended) {
|
||||
return getEmptyState();
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
callState: action.payload.callState,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === REMOTE_VIDEO_CHANGE) {
|
||||
return {
|
||||
...state,
|
||||
hasRemoteVideo: action.payload.remoteVideoEnabled,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === SET_LOCAL_AUDIO) {
|
||||
return {
|
||||
...state,
|
||||
hasLocalAudio: action.payload.enabled,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === SET_LOCAL_VIDEO_FULFILLED) {
|
||||
return {
|
||||
...state,
|
||||
hasLocalVideo: action.payload.enabled,
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
|
@ -72,7 +72,8 @@ export type MessageType = {
|
|||
| 'group'
|
||||
| 'keychange'
|
||||
| 'verified-change'
|
||||
| 'message-history-unsynced';
|
||||
| 'message-history-unsynced'
|
||||
| 'call-history';
|
||||
quote?: { author: string };
|
||||
received_at: number;
|
||||
hasSignalAccount?: boolean;
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
import { combineReducers } from 'redux';
|
||||
|
||||
import {
|
||||
CallingActionType,
|
||||
CallingStateType,
|
||||
reducer as calling,
|
||||
} from './ducks/calling';
|
||||
import {
|
||||
ConversationActionType,
|
||||
ConversationsStateType,
|
||||
|
@ -43,6 +48,7 @@ import {
|
|||
import { reducer as user, UserStateType } from './ducks/user';
|
||||
|
||||
export type StateType = {
|
||||
calling: CallingStateType;
|
||||
conversations: ConversationsStateType;
|
||||
emojis: EmojisStateType;
|
||||
expiration: ExpirationStateType;
|
||||
|
@ -55,6 +61,7 @@ export type StateType = {
|
|||
};
|
||||
|
||||
export type ActionsType =
|
||||
| CallingActionType
|
||||
| EmojisActionType
|
||||
| ExpirationActionType
|
||||
| ConversationActionType
|
||||
|
@ -65,6 +72,7 @@ export type ActionsType =
|
|||
| UpdatesActionType;
|
||||
|
||||
export const reducers = {
|
||||
calling,
|
||||
conversations,
|
||||
emojis,
|
||||
expiration,
|
||||
|
|
16
ts/state/roots/createCallManager.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { Store } from 'redux';
|
||||
|
||||
import { SmartCallManager } from '../smart/CallManager';
|
||||
|
||||
// Workaround: A react component's required properties are filtering up through connect()
|
||||
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
|
||||
const FilteredCallManager = SmartCallManager as any;
|
||||
|
||||
export const createCallManager = (store: Store) => (
|
||||
<Provider store={store}>
|
||||
<FilteredCallManager />
|
||||
</Provider>
|
||||
);
|
23
ts/state/smart/CallManager.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { RefObject } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { CanvasVideoRenderer, GumVideoCapturer } from 'ringrtc';
|
||||
import { mapDispatchToProps } from '../actions';
|
||||
import { CallManager } from '../../components/CallManager';
|
||||
import { StateType } from '../reducer';
|
||||
|
||||
import { getIntl } from '../selectors/user';
|
||||
|
||||
const mapStateToProps = (state: StateType) => {
|
||||
return {
|
||||
...state.calling,
|
||||
i18n: getIntl(state),
|
||||
getVideoCapturer: (localVideoRef: RefObject<HTMLVideoElement>) =>
|
||||
new GumVideoCapturer(640, 480, 30, localVideoRef),
|
||||
getVideoRenderer: (remoteVideoRef: RefObject<HTMLCanvasElement>) =>
|
||||
new CanvasVideoRenderer(remoteVideoRef),
|
||||
};
|
||||
};
|
||||
|
||||
const smart = connect(mapStateToProps, mapDispatchToProps);
|
||||
|
||||
export const SmartCallManager = smart(CallManager);
|
69
ts/textsecure.d.ts
vendored
|
@ -8,6 +8,8 @@ import Crypto from './textsecure/Crypto';
|
|||
import MessageReceiver from './textsecure/MessageReceiver';
|
||||
import EventTarget from './textsecure/EventTarget';
|
||||
import { ByteBufferClass } from './window.d';
|
||||
import { SendOptionsType } from './textsecure/SendMessage';
|
||||
import { WebAPIType } from './textsecure/WebAPI';
|
||||
|
||||
type AttachmentType = any;
|
||||
|
||||
|
@ -76,6 +78,12 @@ export type TextSecureType = {
|
|||
}>,
|
||||
options: Object
|
||||
) => Promise<void>;
|
||||
sendCallingMessage: (
|
||||
recipientId: string,
|
||||
callingMessage: CallingMessageClass,
|
||||
sendOptions: SendOptionsType
|
||||
) => Promise<void>;
|
||||
server: WebAPIType;
|
||||
};
|
||||
protobuf: ProtobufCollectionType;
|
||||
|
||||
|
@ -229,7 +237,7 @@ export declare class ContentClass {
|
|||
|
||||
dataMessage?: DataMessageClass;
|
||||
syncMessage?: SyncMessageClass;
|
||||
callMessage?: any;
|
||||
callingMessage?: CallingMessageClass;
|
||||
nullMessage?: NullMessageClass;
|
||||
receiptMessage?: ReceiptMessageClass;
|
||||
typingMessage?: TypingMessageClass;
|
||||
|
@ -708,3 +716,62 @@ export declare class WebSocketResponseMessageClass {
|
|||
headers?: Array<string>;
|
||||
body?: ProtoBinaryType;
|
||||
}
|
||||
|
||||
// Everything from here down to HangupType (everything related to calling)
|
||||
// must be kept in sync with RingRTC (ringrtc-node).
|
||||
// Whenever you change this, make sure you change RingRTC as well.
|
||||
export type DeviceId = number;
|
||||
|
||||
export type CallId = any;
|
||||
|
||||
export class CallingMessageClass {
|
||||
offer?: OfferMessageClass;
|
||||
answer?: AnswerMessageClass;
|
||||
iceCandidates?: Array<IceCandidateMessageClass>;
|
||||
legacyHangup?: HangupMessageClass;
|
||||
busy?: BusyMessageClass;
|
||||
hangup?: HangupMessageClass;
|
||||
supportsMultiRing?: boolean;
|
||||
destinationDeviceId?: DeviceId;
|
||||
}
|
||||
|
||||
export class OfferMessageClass {
|
||||
callId?: CallId;
|
||||
type?: OfferType;
|
||||
sdp?: string;
|
||||
}
|
||||
|
||||
export enum OfferType {
|
||||
AudioCall = 0,
|
||||
VideoCall = 1,
|
||||
NeedsPermission = 2,
|
||||
}
|
||||
|
||||
export class AnswerMessageClass {
|
||||
callId?: CallId;
|
||||
sdp?: string;
|
||||
}
|
||||
|
||||
export class IceCandidateMessageClass {
|
||||
callId?: CallId;
|
||||
mid?: string;
|
||||
midIndex?: number;
|
||||
sdp?: string;
|
||||
}
|
||||
|
||||
export class BusyMessageClass {
|
||||
callId?: CallId;
|
||||
}
|
||||
|
||||
export class HangupMessageClass {
|
||||
callId?: CallId;
|
||||
type?: HangupType;
|
||||
deviceId?: DeviceId;
|
||||
}
|
||||
|
||||
export enum HangupType {
|
||||
Normal = 0,
|
||||
Accepted = 1,
|
||||
Declined = 2,
|
||||
Busy = 3,
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import { IncomingIdentityKeyError } from './Errors';
|
|||
|
||||
import {
|
||||
AttachmentPointerClass,
|
||||
CallingMessageClass,
|
||||
DataMessageClass,
|
||||
DownloadAttachmentType,
|
||||
EnvelopeClass,
|
||||
|
@ -1217,9 +1218,8 @@ class MessageReceiverInner extends EventTarget {
|
|||
} else if (content.nullMessage) {
|
||||
this.handleNullMessage(envelope);
|
||||
return;
|
||||
} else if (content.callMessage) {
|
||||
this.handleCallMessage(envelope);
|
||||
return;
|
||||
} else if (content.callingMessage) {
|
||||
return this.handleCallingMessage(envelope, content.callingMessage);
|
||||
} else if (content.receiptMessage) {
|
||||
return this.handleReceiptMessage(envelope, content.receiptMessage);
|
||||
} else if (content.typingMessage) {
|
||||
|
@ -1228,9 +1228,15 @@ class MessageReceiverInner extends EventTarget {
|
|||
this.removeFromCache(envelope);
|
||||
throw new Error('Unsupported content message');
|
||||
}
|
||||
handleCallMessage(envelope: EnvelopeClass) {
|
||||
window.log.info('call message from', this.getEnvelopeId(envelope));
|
||||
async handleCallingMessage(
|
||||
envelope: EnvelopeClass,
|
||||
callingMessage: CallingMessageClass
|
||||
) {
|
||||
this.removeFromCache(envelope);
|
||||
await window.Signal.Services.calling.handleCallingMessage(
|
||||
envelope,
|
||||
callingMessage
|
||||
);
|
||||
}
|
||||
async handleReceiptMessage(
|
||||
envelope: EnvelopeClass,
|
||||
|
|
|
@ -9,6 +9,7 @@ import OutgoingMessage from './OutgoingMessage';
|
|||
import Crypto from './Crypto';
|
||||
import {
|
||||
AttachmentPointerClass,
|
||||
CallingMessageClass,
|
||||
ContentClass,
|
||||
DataMessageClass,
|
||||
} from '../textsecure.d';
|
||||
|
@ -892,6 +893,28 @@ export default class MessageSender {
|
|||
);
|
||||
}
|
||||
|
||||
async sendCallingMessage(
|
||||
recipientId: string,
|
||||
callingMessage: CallingMessageClass,
|
||||
sendOptions: SendOptionsType
|
||||
) {
|
||||
const recipients = [recipientId];
|
||||
const finalTimestamp = Date.now();
|
||||
|
||||
const contentMessage = new window.textsecure.protobuf.Content();
|
||||
contentMessage.callingMessage = callingMessage;
|
||||
|
||||
const silent = true;
|
||||
|
||||
await this.sendMessageProtoAndWait(
|
||||
finalTimestamp,
|
||||
recipients,
|
||||
contentMessage,
|
||||
silent,
|
||||
sendOptions
|
||||
);
|
||||
}
|
||||
|
||||
async sendDeliveryReceipt(
|
||||
recipientE164: string,
|
||||
recipientUuid: string,
|
||||
|
|
|
@ -478,6 +478,7 @@ const URL_CALLS = {
|
|||
accounts: 'v1/accounts',
|
||||
updateDeviceName: 'v1/accounts/name',
|
||||
removeSignalingKey: 'v1/accounts/signaling_key',
|
||||
getIceServers: 'v1/accounts/turn',
|
||||
attachmentId: 'v2/attachments/form/upload',
|
||||
deliveryCert: 'v1/certificate/delivery',
|
||||
supportUnauthenticatedDelivery: 'v1/devices/unauthenticated_delivery',
|
||||
|
@ -541,6 +542,7 @@ export type WebAPIType = {
|
|||
getAttachment: (cdnKey: string, cdnNumber: number) => Promise<any>;
|
||||
getAvatar: (path: string) => Promise<any>;
|
||||
getDevices: () => Promise<any>;
|
||||
getIceServers: () => Promise<any>;
|
||||
getKeysForIdentifier: (
|
||||
identifier: string,
|
||||
deviceId?: number
|
||||
|
@ -702,6 +704,7 @@ export function initialize({
|
|||
getAttachment,
|
||||
getAvatar,
|
||||
getDevices,
|
||||
getIceServers,
|
||||
getKeysForIdentifier,
|
||||
getKeysForIdentifierUnauth,
|
||||
getMessageSocket,
|
||||
|
@ -983,6 +986,13 @@ export function initialize({
|
|||
});
|
||||
}
|
||||
|
||||
async function getIceServers() {
|
||||
return _ajax({
|
||||
call: 'getIceServers',
|
||||
httpType: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
async function removeSignalingKey() {
|
||||
return _ajax({
|
||||
call: 'removeSignalingKey',
|
||||
|
|
8
ts/types/Calling.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
// This must be kept in sync with RingRTC.CallState.
|
||||
export enum CallState {
|
||||
Prering = 'init',
|
||||
Ringing = 'ringing',
|
||||
Accepted = 'connected',
|
||||
Reconnecting = 'connecting',
|
||||
Ended = 'ended',
|
||||
}
|
75
ts/util/Sound.ts
Normal file
|
@ -0,0 +1,75 @@
|
|||
export type SoundOpts = {
|
||||
loop?: boolean;
|
||||
src: string;
|
||||
};
|
||||
|
||||
export class Sound {
|
||||
static sounds = new Map();
|
||||
|
||||
private readonly context = new AudioContext();
|
||||
private readonly loop: boolean;
|
||||
private node?: AudioBufferSourceNode;
|
||||
private readonly src: string;
|
||||
|
||||
constructor(options: SoundOpts) {
|
||||
this.loop = Boolean(options.loop);
|
||||
this.src = options.src;
|
||||
}
|
||||
|
||||
async play() {
|
||||
if (!Sound.sounds.has(this.src)) {
|
||||
try {
|
||||
const buffer = await Sound.loadSoundFile(this.src);
|
||||
const decodedBuffer = await this.context.decodeAudioData(buffer);
|
||||
Sound.sounds.set(this.src, decodedBuffer);
|
||||
} catch (err) {
|
||||
window.log.error(`Sound error: ${err}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const soundBuffer = Sound.sounds.get(this.src);
|
||||
|
||||
const soundNode = this.context.createBufferSource();
|
||||
soundNode.buffer = soundBuffer;
|
||||
|
||||
const volumeNode = this.context.createGain();
|
||||
soundNode.connect(volumeNode);
|
||||
volumeNode.connect(this.context.destination);
|
||||
|
||||
soundNode.loop = this.loop;
|
||||
|
||||
soundNode.start(0, 0);
|
||||
|
||||
this.node = soundNode;
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.node) {
|
||||
this.node.stop(0);
|
||||
this.node = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
static async loadSoundFile(src: string): Promise<ArrayBuffer> {
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.open('GET', src, true);
|
||||
xhr.responseType = 'arraybuffer';
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
xhr.onload = () => {
|
||||
if (xhr.status === 200) {
|
||||
resolve(xhr.response);
|
||||
return;
|
||||
}
|
||||
|
||||
reject(new Error(`Request failed: ${xhr.statusText}`));
|
||||
};
|
||||
xhr.onerror = () => {
|
||||
reject(new Error(`Request failed, most likely file not found: ${src}`));
|
||||
};
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
}
|
10
ts/util/callingPermissions.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
export async function requestCameraPermissions(): Promise<boolean> {
|
||||
if (!(await window.getMediaCameraPermissions())) {
|
||||
await window.showCallingPermissionsPopup(true);
|
||||
|
||||
// Check the setting again (from the source of truth).
|
||||
return window.getMediaCameraPermissions();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
44
ts/util/callingTones.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
import { Sound, SoundOpts } from './Sound';
|
||||
|
||||
async function playSound(howlProps: SoundOpts): Promise<Sound | undefined> {
|
||||
const canPlayTone = await window.getCallRingtoneNotification();
|
||||
|
||||
if (!canPlayTone) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tone = new Sound(howlProps);
|
||||
await tone.play();
|
||||
|
||||
return tone;
|
||||
}
|
||||
|
||||
class CallingTones {
|
||||
private ringtone?: Sound;
|
||||
|
||||
async playEndCall() {
|
||||
await playSound({
|
||||
src: 'sounds/navigation-cancel.ogg',
|
||||
});
|
||||
}
|
||||
|
||||
async playRingtone() {
|
||||
if (this.ringtone) {
|
||||
this.stopRingtone();
|
||||
}
|
||||
|
||||
this.ringtone = await playSound({
|
||||
loop: true,
|
||||
src: 'sounds/ringtone_minimal.ogg',
|
||||
});
|
||||
}
|
||||
|
||||
stopRingtone() {
|
||||
if (this.ringtone) {
|
||||
this.ringtone.stop();
|
||||
this.ringtone = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const callingTones = new CallingTones();
|
|
@ -289,9 +289,9 @@
|
|||
"rule": "jQuery-appendTo(",
|
||||
"path": "js/permissions_popup_start.js",
|
||||
"line": "window.view.$el.appendTo($body);",
|
||||
"lineNumber": 42,
|
||||
"lineNumber": 57,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"updated": "2020-06-02T21:51:34.813Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
},
|
||||
{
|
||||
|
@ -316,9 +316,9 @@
|
|||
"rule": "jQuery-appendTo(",
|
||||
"path": "js/settings_start.js",
|
||||
"line": " window.view.$el.appendTo($body);",
|
||||
"lineNumber": 57,
|
||||
"lineNumber": 63,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"updated": "2020-06-02T21:51:34.813Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
},
|
||||
{
|
||||
|
@ -513,81 +513,99 @@
|
|||
"rule": "jQuery-appendTo(",
|
||||
"path": "js/views/inbox_view.js",
|
||||
"line": " toast.$el.appendTo(this.$el);",
|
||||
"lineNumber": 98,
|
||||
"lineNumber": 99,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2019-10-21T22:30:15.622Z",
|
||||
"updated": "2020-05-28T17:42:35.329Z",
|
||||
"reasonDetail": "Known DOM elements"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/inbox_view.js",
|
||||
"line": " this.$('.left-pane-placeholder').append(this.leftPaneView.el);",
|
||||
"lineNumber": 118,
|
||||
"line": " this.$('.call-manager-placeholder').append(this.callManagerView.el);",
|
||||
"lineNumber": 121,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2019-10-21T22:30:15.622Z",
|
||||
"updated": "2020-05-28T17:42:35.329Z",
|
||||
"reasonDetail": "<optional>"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-append(",
|
||||
"path": "js/views/inbox_view.js",
|
||||
"line": " this.$('.call-manager-placeholder').append(this.callManagerView.el);",
|
||||
"lineNumber": 121,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-05-28T17:42:35.329Z",
|
||||
"reasonDetail": "<optional>"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/inbox_view.js",
|
||||
"line": " this.$('.left-pane-placeholder').append(this.leftPaneView.el);",
|
||||
"lineNumber": 132,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-05-28T17:42:35.329Z",
|
||||
"reasonDetail": "Known DOM elements"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-append(",
|
||||
"path": "js/views/inbox_view.js",
|
||||
"line": " this.$('.left-pane-placeholder').append(this.leftPaneView.el);",
|
||||
"lineNumber": 118,
|
||||
"lineNumber": 132,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2019-10-21T22:30:15.622Z",
|
||||
"updated": "2020-05-28T17:42:35.329Z",
|
||||
"reasonDetail": "Known DOM elements"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/inbox_view.js",
|
||||
"line": " if (e && this.$(e.target).closest('.placeholder').length) {",
|
||||
"lineNumber": 168,
|
||||
"lineNumber": 183,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2019-10-21T22:30:15.622Z",
|
||||
"updated": "2020-05-28T17:42:35.329Z",
|
||||
"reasonDetail": "Known DOM elements"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/inbox_view.js",
|
||||
"line": " this.$('#header, .gutter').addClass('inactive');",
|
||||
"lineNumber": 172,
|
||||
"lineNumber": 187,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2019-10-21T22:30:15.622Z",
|
||||
"updated": "2020-05-28T17:42:35.329Z",
|
||||
"reasonDetail": "Hardcoded selector"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/inbox_view.js",
|
||||
"line": " this.$('.conversation-stack').addClass('inactive');",
|
||||
"lineNumber": 176,
|
||||
"lineNumber": 191,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2019-10-21T22:30:15.622Z",
|
||||
"updated": "2020-05-28T17:42:35.329Z",
|
||||
"reasonDetail": "Hardcoded selector"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/inbox_view.js",
|
||||
"line": " this.$('.conversation:first .menu').trigger('close');",
|
||||
"lineNumber": 178,
|
||||
"lineNumber": 193,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2019-10-21T22:30:15.622Z",
|
||||
"updated": "2020-05-28T17:42:35.329Z",
|
||||
"reasonDetail": "Hardcoded selector"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/inbox_view.js",
|
||||
"line": " if (e && this.$(e.target).closest('.capture-audio').length > 0) {",
|
||||
"lineNumber": 198,
|
||||
"lineNumber": 213,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-05-20T20:10:43.540Z",
|
||||
"updated": "2020-05-29T18:29:18.234Z",
|
||||
"reasonDetail": "Known DOM elements"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/inbox_view.js",
|
||||
"line": " this.$('.conversation:first .recorder').trigger('close');",
|
||||
"lineNumber": 201,
|
||||
"lineNumber": 216,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-05-20T20:10:43.540Z",
|
||||
"updated": "2020-05-29T18:29:18.234Z",
|
||||
"reasonDetail": "Hardcoded selector"
|
||||
},
|
||||
{
|
||||
|
@ -838,150 +856,200 @@
|
|||
"line": " this.$('input').prop('checked', Boolean(this.value));",
|
||||
"lineNumber": 49,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"updated": "2020-06-02T22:20:33.618Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/settings_view.js",
|
||||
"line": " this.$('input').prop('checked', Boolean(this.value));",
|
||||
"lineNumber": 68,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-06-02T21:51:34.813Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/settings_view.js",
|
||||
"line": " const value = this.$(e.target).val();",
|
||||
"lineNumber": 64,
|
||||
"lineNumber": 83,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"updated": "2020-06-02T21:51:34.813Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/settings_view.js",
|
||||
"line": " this.$(`#${this.name}-${this.value}`).attr('checked', 'checked');",
|
||||
"lineNumber": 69,
|
||||
"lineNumber": 88,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"updated": "2020-06-02T21:51:34.813Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/settings_view.js",
|
||||
"line": " el: this.$('.notification-settings'),",
|
||||
"lineNumber": 78,
|
||||
"lineNumber": 97,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"updated": "2020-06-02T21:51:34.813Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/settings_view.js",
|
||||
"line": " el: this.$('.theme-settings'),",
|
||||
"lineNumber": 84,
|
||||
"lineNumber": 103,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"updated": "2020-06-02T21:51:34.813Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/settings_view.js",
|
||||
"line": " $(document.body)",
|
||||
"lineNumber": 88,
|
||||
"lineNumber": 107,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"updated": "2020-06-02T21:51:34.813Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/settings_view.js",
|
||||
"line": " el: this.$('.audio-notification-setting'),",
|
||||
"lineNumber": 99,
|
||||
"lineNumber": 118,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"updated": "2020-06-02T21:51:34.813Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/settings_view.js",
|
||||
"line": " el: this.$('.spell-check-setting'),",
|
||||
"lineNumber": 106,
|
||||
"lineNumber": 125,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"updated": "2020-06-02T21:51:34.813Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/settings_view.js",
|
||||
"line": " const $msg = this.$('.spell-check-setting-message');",
|
||||
"lineNumber": 110,
|
||||
"lineNumber": 129,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-03-19T16:06:32.598Z"
|
||||
"updated": "2020-06-02T21:51:34.813Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/settings_view.js",
|
||||
"line": " el: this.$('.menu-bar-setting'),",
|
||||
"lineNumber": 123,
|
||||
"lineNumber": 142,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-03-20T16:47:14.450Z",
|
||||
"updated": "2020-06-02T21:51:34.813Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/settings_view.js",
|
||||
"line": " el: this.$('.media-permissions'),",
|
||||
"lineNumber": 130,
|
||||
"line": " el: this.$('.always-relay-calls-setting'),",
|
||||
"lineNumber": 149,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-03-20T16:47:14.450Z",
|
||||
"updated": "2020-06-02T21:51:34.813Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/settings_view.js",
|
||||
"line": " el: this.$('.call-ringtone-notification-setting'),",
|
||||
"lineNumber": 155,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-06-02T21:51:34.813Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/settings_view.js",
|
||||
"line": " el: this.$('.call-system-notification-setting'),",
|
||||
"lineNumber": 161,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-06-02T21:51:34.813Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/settings_view.js",
|
||||
"line": " el: this.$('.incoming-call-notification-setting'),",
|
||||
"lineNumber": 167,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-06-02T21:51:34.813Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/settings_view.js",
|
||||
"line": " el: this.$('.media-permissions'),",
|
||||
"lineNumber": 173,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-06-02T21:51:34.813Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/settings_view.js",
|
||||
"line": " el: this.$('.media-camera-permissions'),",
|
||||
"lineNumber": 178,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-06-02T21:51:34.813Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/settings_view.js",
|
||||
"line": " this.$('.sync-setting').append(syncView.el);",
|
||||
"lineNumber": 136,
|
||||
"lineNumber": 184,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-03-20T16:47:14.450Z",
|
||||
"updated": "2020-06-02T21:51:34.813Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-append(",
|
||||
"path": "js/views/settings_view.js",
|
||||
"line": " this.$('.sync-setting').append(syncView.el);",
|
||||
"lineNumber": 136,
|
||||
"lineNumber": 184,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-03-20T16:47:14.450Z",
|
||||
"updated": "2020-06-02T21:51:34.813Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/settings_view.js",
|
||||
"line": " this.$('.sync').text(i18n('syncNow'));",
|
||||
"lineNumber": 200,
|
||||
"lineNumber": 263,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-03-25T13:52:04.149Z",
|
||||
"updated": "2020-06-02T21:51:34.813Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/settings_view.js",
|
||||
"line": " this.$('.sync').attr('disabled', 'disabled');",
|
||||
"lineNumber": 204,
|
||||
"lineNumber": 267,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-03-25T13:52:04.149Z",
|
||||
"updated": "2020-06-02T21:51:34.813Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/settings_view.js",
|
||||
"line": " this.$('.synced_at').hide();",
|
||||
"lineNumber": 216,
|
||||
"lineNumber": 279,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-03-25T13:52:04.149Z",
|
||||
"updated": "2020-06-02T21:51:34.813Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/settings_view.js",
|
||||
"line": " this.$('.sync_failed').hide();",
|
||||
"lineNumber": 221,
|
||||
"lineNumber": 284,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-03-25T13:52:04.149Z",
|
||||
"updated": "2020-06-02T21:51:34.813Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
|
@ -11412,6 +11480,24 @@
|
|||
"updated": "2018-09-17T20:50:40.689Z",
|
||||
"reasonDetail": "Hard-coded value"
|
||||
},
|
||||
{
|
||||
"rule": "React-createRef",
|
||||
"path": "ts/components/CallScreen.js",
|
||||
"line": " this.localVideoRef = react_1.default.createRef();",
|
||||
"lineNumber": 97,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-05-28T17:22:06.472Z",
|
||||
"reasonDetail": "Used to render local preview video"
|
||||
},
|
||||
{
|
||||
"rule": "React-createRef",
|
||||
"path": "ts/components/CallScreen.tsx",
|
||||
"line": " this.localVideoRef = React.createRef();",
|
||||
"lineNumber": 80,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-06-02T21:51:34.813Z",
|
||||
"reasonDetail": "Used to render local preview video"
|
||||
},
|
||||
{
|
||||
"rule": "React-createRef",
|
||||
"path": "ts/components/CaptionEditor.js",
|
||||
|
@ -11515,7 +11601,7 @@
|
|||
"rule": "React-createRef",
|
||||
"path": "ts/components/conversation/ConversationHeader.tsx",
|
||||
"line": " this.menuTriggerRef = React.createRef();",
|
||||
"lineNumber": 68,
|
||||
"lineNumber": 70,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-05-20T20:10:43.540Z",
|
||||
"reasonDetail": "Used to reference popup menu"
|
||||
|
@ -11716,17 +11802,17 @@
|
|||
"rule": "jQuery-wrap(",
|
||||
"path": "ts/textsecure/SendMessage.ts",
|
||||
"line": " return window.dcodeIO.ByteBuffer.wrap(string, 'hex').toArrayBuffer();",
|
||||
"lineNumber": 29,
|
||||
"lineNumber": 30,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2020-04-05T23:45:16.746Z"
|
||||
"updated": "2020-05-28T18:08:02.658Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-wrap(",
|
||||
"path": "ts/textsecure/SendMessage.ts",
|
||||
"line": " return window.dcodeIO.ByteBuffer.wrap(string, 'base64').toArrayBuffer();",
|
||||
"lineNumber": 32,
|
||||
"lineNumber": 33,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2020-04-05T23:45:16.746Z"
|
||||
"updated": "2020-05-28T18:08:02.658Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-wrap(",
|
||||
|
|
50
ts/window.d.ts
vendored
|
@ -1,5 +1,6 @@
|
|||
// Captures the globals put in place by preload.js, background.js and others
|
||||
|
||||
import { Ref } from 'react';
|
||||
import {
|
||||
LibSignalType,
|
||||
SignalProtocolAddressClass,
|
||||
|
@ -7,7 +8,10 @@ import {
|
|||
} from './libsignal.d';
|
||||
import { TextSecureType } from './textsecure.d';
|
||||
import { WebAPIConnectType } from './textsecure/WebAPI';
|
||||
import { CallingClass, CallHistoryDetailsType } from './services/calling';
|
||||
import * as Crypto from './Crypto';
|
||||
import { ColorType, LocalizerType } from './types/Util';
|
||||
import { SendOptionsType } from './textsecure/SendMessage';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
@ -15,6 +19,14 @@ declare global {
|
|||
getExpiration: () => string;
|
||||
getEnvironment: () => string;
|
||||
getSocketStatus: () => number;
|
||||
getAlwaysRelayCalls: () => Promise<boolean>;
|
||||
getIncomingCallNotification: () => Promise<boolean>;
|
||||
getCallRingtoneNotification: () => Promise<boolean>;
|
||||
getCallSystemNotification: () => Promise<boolean>;
|
||||
getMediaPermissions: () => Promise<boolean>;
|
||||
getMediaCameraPermissions: () => Promise<boolean>;
|
||||
showCallingPermissionsPopup: (forCamera: boolean) => Promise<void>;
|
||||
i18n: LocalizerType;
|
||||
libphonenumber: {
|
||||
util: {
|
||||
getRegionCodeForNumber: (number: string) => string;
|
||||
|
@ -27,7 +39,9 @@ declare global {
|
|||
error: LoggerType;
|
||||
};
|
||||
normalizeUuids: (obj: any, paths: Array<string>, context: string) => any;
|
||||
platform: string;
|
||||
restart: () => void;
|
||||
showWindow: () => void;
|
||||
storage: {
|
||||
put: (key: string, value: any) => void;
|
||||
remove: (key: string) => void;
|
||||
|
@ -43,10 +57,16 @@ declare global {
|
|||
trustRoot: ArrayBuffer
|
||||
) => CertificateValidatorType;
|
||||
};
|
||||
Services: {
|
||||
calling: CallingClass;
|
||||
};
|
||||
};
|
||||
ConversationController: ConversationControllerType;
|
||||
WebAPI: WebAPIConnectType;
|
||||
Whisper: WhisperType;
|
||||
|
||||
// Flags
|
||||
CALLING: boolean;
|
||||
}
|
||||
|
||||
interface Error {
|
||||
|
@ -58,6 +78,17 @@ export type ConversationType = {
|
|||
updateE164: (e164?: string) => void;
|
||||
updateUuid: (uuid?: string) => void;
|
||||
id: string;
|
||||
get: (key: string) => any;
|
||||
getAvatarPath(): string | undefined;
|
||||
getColor(): ColorType | undefined;
|
||||
getName(): string | undefined;
|
||||
getNumber(): string;
|
||||
getProfileName(): string | undefined;
|
||||
getRecipients: () => Array<string>;
|
||||
getSendOptions(): SendOptionsType;
|
||||
safeGetVerified(): Promise<number>;
|
||||
getIsAddedByContact(): boolean;
|
||||
addCallHistory(details: CallHistoryDetailsType): void;
|
||||
};
|
||||
|
||||
export type ConversationControllerType = {
|
||||
|
@ -73,11 +104,7 @@ export type ConversationControllerType = {
|
|||
wrap: (promise: Promise<any>) => Promise<void>;
|
||||
sendOptions: Object;
|
||||
};
|
||||
get: (
|
||||
identifier: string
|
||||
) => null | {
|
||||
get: (key: string) => any;
|
||||
};
|
||||
get: (identifier: string) => null | ConversationType;
|
||||
};
|
||||
|
||||
export type DCodeIOType = {
|
||||
|
@ -130,6 +157,19 @@ export class ByteBufferClass {
|
|||
skip: (length: number) => void;
|
||||
}
|
||||
|
||||
export class GumVideoCapturer {
|
||||
constructor(
|
||||
maxWidth: number,
|
||||
maxHeight: number,
|
||||
maxFramerate: number,
|
||||
localPreview: Ref<HTMLVideoElement>
|
||||
);
|
||||
}
|
||||
|
||||
export class CanvasVideoRenderer {
|
||||
constructor(canvas: Ref<HTMLCanvasElement>);
|
||||
}
|
||||
|
||||
export type LoggerType = (...args: Array<any>) => void;
|
||||
|
||||
export type WhisperType = {
|
||||
|
|
|
@ -122,6 +122,9 @@
|
|||
]
|
||||
],
|
||||
|
||||
// So things like "autoplay" on <video> work
|
||||
"jsx-boolean-value": false,
|
||||
|
||||
// Maybe will turn on:
|
||||
|
||||
// We're not trying to be comprehensive with JSDoc right now. We have the style guide.
|
||||
|
|
11
yarn.lock
|
@ -14063,6 +14063,13 @@ react-textarea-autosize@^7.1.0:
|
|||
"@babel/runtime" "^7.1.2"
|
||||
prop-types "^15.6.0"
|
||||
|
||||
react-tooltip-lite@1.12.0:
|
||||
version "1.12.0"
|
||||
resolved "https://registry.yarnpkg.com/react-tooltip-lite/-/react-tooltip-lite-1.12.0.tgz#f6cd1323cdd9f5f80dd0e71a30ef59f401dee9ba"
|
||||
integrity sha512-QjDnmDmjtLNKvLY6bzUOG8W6ZDBTiE4UXugGzClOQEGvMvbkJn2GvZvLwRaxsN/GCx7589RgbGaESMiJAm+zWg==
|
||||
dependencies:
|
||||
prop-types "^15.5.8"
|
||||
|
||||
react-transition-group@^2.2.1:
|
||||
version "2.9.0"
|
||||
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.9.0.tgz#df9cdb025796211151a436c69a8f3b97b5b07c8d"
|
||||
|
@ -14924,6 +14931,10 @@ rimraf@~2.4.0:
|
|||
dependencies:
|
||||
glob "^6.0.1"
|
||||
|
||||
"ringrtc@https://github.com/signalapp/signal-ringrtc-node.git#95afcf7effb0b34e2cdcf3df40bc9519324db8cd":
|
||||
version "2.1.0"
|
||||
resolved "https://github.com/signalapp/signal-ringrtc-node.git#95afcf7effb0b34e2cdcf3df40bc9519324db8cd"
|
||||
|
||||
ripemd160@^2.0.0, ripemd160@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.1.tgz#0f4584295c53a3628af7e6d79aca21ce57d1c6e7"
|
||||
|
|