Basic call link join support
This commit is contained in:
parent
2bfb6e7481
commit
96b3413feb
75 changed files with 2438 additions and 509 deletions
|
@ -1776,6 +1776,54 @@
|
||||||
"messageformat": "Call is full",
|
"messageformat": "Call is full",
|
||||||
"description": "Text in the call lobby when you can't join because the call is full"
|
"description": "Text in the call lobby when you can't join because the call is full"
|
||||||
},
|
},
|
||||||
|
"icu:calling__cant-join": {
|
||||||
|
"messageformat": "Can't join call",
|
||||||
|
"description": "Modal dialog title when you can't join a call"
|
||||||
|
},
|
||||||
|
"icu:calling__dialog-already-in-call": {
|
||||||
|
"messageformat": "You are already in a call.",
|
||||||
|
"description": "Error message when clicking a call link to join a call, but you're in a call already."
|
||||||
|
},
|
||||||
|
"icu:calling__call-link-connection-issues": {
|
||||||
|
"messageformat": "Could not fetch call link information. Please check your network connection and try again.",
|
||||||
|
"description": "Error message when unable to fetch call link info from server."
|
||||||
|
},
|
||||||
|
"icu:calling__call-link-copied": {
|
||||||
|
"messageformat": "Call link copied.",
|
||||||
|
"description": "Toast shown when a call link is copied to the clipboard."
|
||||||
|
},
|
||||||
|
"icu:calling__call-link-no-longer-valid": {
|
||||||
|
"messageformat": "This call link is no longer valid.",
|
||||||
|
"description": "Error message when a call link has been revoked or expires."
|
||||||
|
},
|
||||||
|
"icu:calling__call-link-default-title": {
|
||||||
|
"messageformat": "Signal Call",
|
||||||
|
"description": "Default title for Signal call links."
|
||||||
|
},
|
||||||
|
"icu:calling__join-request-denied": {
|
||||||
|
"messageformat": "Your request to join this call has been denied.",
|
||||||
|
"description": "Error message when a request to join a call link call was rejected by a call admin."
|
||||||
|
},
|
||||||
|
"icu:calling__join-request-denied-title": {
|
||||||
|
"messageformat": "Join request denied",
|
||||||
|
"description": "Title of error when a request to join a call link call was rejected by a call admin."
|
||||||
|
},
|
||||||
|
"icu:calling__removed-from-call": {
|
||||||
|
"messageformat": "Someone has removed you from the call.",
|
||||||
|
"description": "Error message when in a call then a call admin removes you."
|
||||||
|
},
|
||||||
|
"icu:calling__removed-from-call-title": {
|
||||||
|
"messageformat": "Removed from call",
|
||||||
|
"description": "Title of error when in a call then a call admin removes you."
|
||||||
|
},
|
||||||
|
"icu:CallingLobby__CallLinkNotice": {
|
||||||
|
"messageformat": "Anyone who joins this call via the link will see your name and photo.",
|
||||||
|
"description": "Toast shown in call lobby before joining a call link call to inform user that profile info will be shared."
|
||||||
|
},
|
||||||
|
"icu:CallingLobby__CallLinkNotice--phone-sharing": {
|
||||||
|
"messageformat": "Anyone who joins this call via the link will see your name, photo, and phone number.",
|
||||||
|
"description": "Toast shown in call lobby before joining a call link call to inform user that profile info will be shared, when the phone number sharing account setting is enabled."
|
||||||
|
},
|
||||||
"icu:CallingLobbyJoinButton--join": {
|
"icu:CallingLobbyJoinButton--join": {
|
||||||
"messageformat": "Join",
|
"messageformat": "Join",
|
||||||
"description": "Button label in the call lobby for joining a call"
|
"description": "Button label in the call lobby for joining a call"
|
||||||
|
@ -2691,6 +2739,10 @@
|
||||||
"messageformat": "Some attachments are too large to display.",
|
"messageformat": "Some attachments are too large to display.",
|
||||||
"description": "Shown in a message bubble if any attachments are left on message when too-large attachments are dropped"
|
"description": "Shown in a message bubble if any attachments are left on message when too-large attachments are dropped"
|
||||||
},
|
},
|
||||||
|
"icu:message--call-link-description": {
|
||||||
|
"messageformat": "Use this link to join a Signal call",
|
||||||
|
"description": "Shown in message previews for Signal call links."
|
||||||
|
},
|
||||||
"icu:donation--missing": {
|
"icu:donation--missing": {
|
||||||
"messageformat": "Unable to fetch donation details",
|
"messageformat": "Unable to fetch donation details",
|
||||||
"description": "Aria label for donation when we can't fetch the details."
|
"description": "Aria label for donation when we can't fetch the details."
|
||||||
|
@ -3663,6 +3715,14 @@
|
||||||
"messageformat": "Audio call",
|
"messageformat": "Audio call",
|
||||||
"description": "Shown in the call lobby for a direct 1:1 call when the caller's video is disabled, to specify that an audio call will be placed when clicking the Start button."
|
"description": "Shown in the call lobby for a direct 1:1 call when the caller's video is disabled, to specify that an audio call will be placed when clicking the Start button."
|
||||||
},
|
},
|
||||||
|
"icu:CallControls__InfoDisplay--adhoc-call": {
|
||||||
|
"messageformat": "Call link",
|
||||||
|
"description": "Shown in the call lobby for a call link."
|
||||||
|
},
|
||||||
|
"icu:CallControls__InfoDisplay--adhoc-join-request-pending": {
|
||||||
|
"messageformat": "Waiting to be let in",
|
||||||
|
"description": "Shown in the call lobby for call link calls when requesting to join a call which requires admin approval."
|
||||||
|
},
|
||||||
"icu:CallControls__JoinLeaveButton--hangup-1-1": {
|
"icu:CallControls__JoinLeaveButton--hangup-1-1": {
|
||||||
"messageformat": "End",
|
"messageformat": "End",
|
||||||
"description": "Title for the hangup button for a direct 1:1 call with only 2 participants."
|
"description": "Title for the hangup button for a direct 1:1 call with only 2 participants."
|
||||||
|
@ -3803,6 +3863,10 @@
|
||||||
"messageformat": "A window",
|
"messageformat": "A window",
|
||||||
"description": "Title for the select your screen sharing sources modal"
|
"description": "Title for the select your screen sharing sources modal"
|
||||||
},
|
},
|
||||||
|
"icu:CallingAdhocCallInfo__CopyLink": {
|
||||||
|
"messageformat": "Copy link",
|
||||||
|
"description": "Menu item in the in-call info popup for call link calls. The action is to add the call link to the clipboard."
|
||||||
|
},
|
||||||
"icu:callingDeviceSelection__label--video": {
|
"icu:callingDeviceSelection__label--video": {
|
||||||
"messageformat": "Video",
|
"messageformat": "Video",
|
||||||
"description": "Label for video input selector"
|
"description": "Label for video input selector"
|
||||||
|
|
|
@ -2469,6 +2469,7 @@ ipc.on('get-config', async event => {
|
||||||
registrationChallengeUrl: config.get<string>('registrationChallengeUrl'),
|
registrationChallengeUrl: config.get<string>('registrationChallengeUrl'),
|
||||||
serverPublicParams: config.get<string>('serverPublicParams'),
|
serverPublicParams: config.get<string>('serverPublicParams'),
|
||||||
serverTrustRoot: config.get<string>('serverTrustRoot'),
|
serverTrustRoot: config.get<string>('serverTrustRoot'),
|
||||||
|
genericServerPublicParams: config.get<string>('genericServerPublicParams'),
|
||||||
theme,
|
theme,
|
||||||
appStartInitialSpellcheckSetting,
|
appStartInitialSpellcheckSetting,
|
||||||
|
|
||||||
|
@ -2619,6 +2620,10 @@ function handleSignalRoute(route: ParsedSignalRoute) {
|
||||||
mainWindow.webContents.send('start-call-lobby', {
|
mainWindow.webContents.send('start-call-lobby', {
|
||||||
conversationId: route.args.conversationId,
|
conversationId: route.args.conversationId,
|
||||||
});
|
});
|
||||||
|
} else if (route.key === 'linkCall') {
|
||||||
|
mainWindow.webContents.send('start-call-link', {
|
||||||
|
key: route.args.key,
|
||||||
|
});
|
||||||
} else if (route.key === 'showWindow') {
|
} else if (route.key === 'showWindow') {
|
||||||
mainWindow.webContents.send('show-window');
|
mainWindow.webContents.send('show-window');
|
||||||
} else if (route.key === 'setIsPresenting') {
|
} else if (route.key === 'setIsPresenting') {
|
||||||
|
|
|
@ -24,5 +24,6 @@
|
||||||
"buildExpiration": 0,
|
"buildExpiration": 0,
|
||||||
"certificateAuthority": "-----BEGIN CERTIFICATE-----\nMIIF2zCCA8OgAwIBAgIUAMHz4g60cIDBpPr1gyZ/JDaaPpcwDQYJKoZIhvcNAQEL\nBQAwdTELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcT\nDU1vdW50YWluIFZpZXcxHjAcBgNVBAoTFVNpZ25hbCBNZXNzZW5nZXIsIExMQzEZ\nMBcGA1UEAxMQU2lnbmFsIE1lc3NlbmdlcjAeFw0yMjAxMjYwMDQ1NTFaFw0zMjAx\nMjQwMDQ1NTBaMHUxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYw\nFAYDVQQHEw1Nb3VudGFpbiBWaWV3MR4wHAYDVQQKExVTaWduYWwgTWVzc2VuZ2Vy\nLCBMTEMxGTAXBgNVBAMTEFNpZ25hbCBNZXNzZW5nZXIwggIiMA0GCSqGSIb3DQEB\nAQUAA4ICDwAwggIKAoICAQDEecifxMHHlDhxbERVdErOhGsLO08PUdNkATjZ1kT5\n1uPf5JPiRbus9F4J/GgBQ4ANSAjIDZuFY0WOvG/i0qvxthpW70ocp8IjkiWTNiA8\n1zQNQdCiWbGDU4B1sLi2o4JgJMweSkQFiyDynqWgHpw+KmvytCzRWnvrrptIfE4G\nPxNOsAtXFbVH++8JO42IaKRVlbfpe/lUHbjiYmIpQroZPGPY4Oql8KM3o39ObPnT\no1WoM4moyOOZpU3lV1awftvWBx1sbTBL02sQWfHRxgNVF+Pj0fdDMMFdFJobArrL\nVfK2Ua+dYN4pV5XIxzVarSRW73CXqQ+2qloPW/ynpa3gRtYeGWV4jl7eD0PmeHpK\nOY78idP4H1jfAv0TAVeKpuB5ZFZ2szcySxrQa8d7FIf0kNJe9gIRjbQ+XrvnN+ZZ\nvj6d+8uBJq8LfQaFhlVfI0/aIdggScapR7w8oLpvdflUWqcTLeXVNLVrg15cEDwd\nlV8PVscT/KT0bfNzKI80qBq8LyRmauAqP0CDjayYGb2UAabnhefgmRY6aBE5mXxd\nbyAEzzCS3vDxjeTD8v8nbDq+SD6lJi0i7jgwEfNDhe9XK50baK15Udc8Cr/ZlhGM\njNmWqBd0jIpaZm1rzWA0k4VwXtDwpBXSz8oBFshiXs3FD6jHY2IhOR3ppbyd4qRU\npwIDAQABo2MwYTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV\nHQ4EFgQUtfNLxuXWS9DlgGuMUMNnW7yx83EwHwYDVR0jBBgwFoAUtfNLxuXWS9Dl\ngGuMUMNnW7yx83EwDQYJKoZIhvcNAQELBQADggIBABUeiryS0qjykBN75aoHO9bV\nPrrX+DSJIB9V2YzkFVyh/io65QJMG8naWVGOSpVRwUwhZVKh3JVp/miPgzTGAo7z\nhrDIoXc+ih7orAMb19qol/2Ha8OZLa75LojJNRbZoCR5C+gM8C+spMLjFf9k3JVx\ndajhtRUcR0zYhwsBS7qZ5Me0d6gRXD0ZiSbadMMxSw6KfKk3ePmPb9gX+MRTS63c\n8mLzVYB/3fe/bkpq4RUwzUHvoZf+SUD7NzSQRQQMfvAHlxk11TVNxScYPtxXDyiy\n3Cssl9gWrrWqQ/omuHipoH62J7h8KAYbr6oEIq+Czuenc3eCIBGBBfvCpuFOgckA\nXXE4MlBasEU0MO66GrTCgMt9bAmSw3TrRP12+ZUFxYNtqWluRU8JWQ4FCCPcz9pg\nMRBOgn4lTxDZG+I47OKNuSRjFEP94cdgxd3H/5BK7WHUz1tAGQ4BgepSXgmjzifF\nT5FVTDTl3ZnWUVBXiHYtbOBgLiSIkbqGMCLtrBtFIeQ7RRTb3L+IE9R0UB0cJB3A\nXbf1lVkOcmrdu2h8A32aCwtr5S1fBF1unlG7imPmqJfpOMWa8yIF/KWVm29JAPq8\nLrsybb0z5gg8w7ZblEuB9zOW9M3l60DXuJO6l7g+deV6P96rv2unHS8UlvWiVWDy\n9qfgAJizyy3kqM4lOwBH\n-----END CERTIFICATE-----\n",
|
"certificateAuthority": "-----BEGIN CERTIFICATE-----\nMIIF2zCCA8OgAwIBAgIUAMHz4g60cIDBpPr1gyZ/JDaaPpcwDQYJKoZIhvcNAQEL\nBQAwdTELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcT\nDU1vdW50YWluIFZpZXcxHjAcBgNVBAoTFVNpZ25hbCBNZXNzZW5nZXIsIExMQzEZ\nMBcGA1UEAxMQU2lnbmFsIE1lc3NlbmdlcjAeFw0yMjAxMjYwMDQ1NTFaFw0zMjAx\nMjQwMDQ1NTBaMHUxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYw\nFAYDVQQHEw1Nb3VudGFpbiBWaWV3MR4wHAYDVQQKExVTaWduYWwgTWVzc2VuZ2Vy\nLCBMTEMxGTAXBgNVBAMTEFNpZ25hbCBNZXNzZW5nZXIwggIiMA0GCSqGSIb3DQEB\nAQUAA4ICDwAwggIKAoICAQDEecifxMHHlDhxbERVdErOhGsLO08PUdNkATjZ1kT5\n1uPf5JPiRbus9F4J/GgBQ4ANSAjIDZuFY0WOvG/i0qvxthpW70ocp8IjkiWTNiA8\n1zQNQdCiWbGDU4B1sLi2o4JgJMweSkQFiyDynqWgHpw+KmvytCzRWnvrrptIfE4G\nPxNOsAtXFbVH++8JO42IaKRVlbfpe/lUHbjiYmIpQroZPGPY4Oql8KM3o39ObPnT\no1WoM4moyOOZpU3lV1awftvWBx1sbTBL02sQWfHRxgNVF+Pj0fdDMMFdFJobArrL\nVfK2Ua+dYN4pV5XIxzVarSRW73CXqQ+2qloPW/ynpa3gRtYeGWV4jl7eD0PmeHpK\nOY78idP4H1jfAv0TAVeKpuB5ZFZ2szcySxrQa8d7FIf0kNJe9gIRjbQ+XrvnN+ZZ\nvj6d+8uBJq8LfQaFhlVfI0/aIdggScapR7w8oLpvdflUWqcTLeXVNLVrg15cEDwd\nlV8PVscT/KT0bfNzKI80qBq8LyRmauAqP0CDjayYGb2UAabnhefgmRY6aBE5mXxd\nbyAEzzCS3vDxjeTD8v8nbDq+SD6lJi0i7jgwEfNDhe9XK50baK15Udc8Cr/ZlhGM\njNmWqBd0jIpaZm1rzWA0k4VwXtDwpBXSz8oBFshiXs3FD6jHY2IhOR3ppbyd4qRU\npwIDAQABo2MwYTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV\nHQ4EFgQUtfNLxuXWS9DlgGuMUMNnW7yx83EwHwYDVR0jBBgwFoAUtfNLxuXWS9Dl\ngGuMUMNnW7yx83EwDQYJKoZIhvcNAQELBQADggIBABUeiryS0qjykBN75aoHO9bV\nPrrX+DSJIB9V2YzkFVyh/io65QJMG8naWVGOSpVRwUwhZVKh3JVp/miPgzTGAo7z\nhrDIoXc+ih7orAMb19qol/2Ha8OZLa75LojJNRbZoCR5C+gM8C+spMLjFf9k3JVx\ndajhtRUcR0zYhwsBS7qZ5Me0d6gRXD0ZiSbadMMxSw6KfKk3ePmPb9gX+MRTS63c\n8mLzVYB/3fe/bkpq4RUwzUHvoZf+SUD7NzSQRQQMfvAHlxk11TVNxScYPtxXDyiy\n3Cssl9gWrrWqQ/omuHipoH62J7h8KAYbr6oEIq+Czuenc3eCIBGBBfvCpuFOgckA\nXXE4MlBasEU0MO66GrTCgMt9bAmSw3TrRP12+ZUFxYNtqWluRU8JWQ4FCCPcz9pg\nMRBOgn4lTxDZG+I47OKNuSRjFEP94cdgxd3H/5BK7WHUz1tAGQ4BgepSXgmjzifF\nT5FVTDTl3ZnWUVBXiHYtbOBgLiSIkbqGMCLtrBtFIeQ7RRTb3L+IE9R0UB0cJB3A\nXbf1lVkOcmrdu2h8A32aCwtr5S1fBF1unlG7imPmqJfpOMWa8yIF/KWVm29JAPq8\nLrsybb0z5gg8w7ZblEuB9zOW9M3l60DXuJO6l7g+deV6P96rv2unHS8UlvWiVWDy\n9qfgAJizyy3kqM4lOwBH\n-----END CERTIFICATE-----\n",
|
||||||
"serverPublicParams": "ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUjlENAErBme1YHmOSpU6tr6doJ66dPzVAWIanmO/5mgjNEDeK7DDqQdB1xd03HT2Qs2TxY3kCK8aAb/0iM0HQiXjxZ9HIgYhbtvGEnDKW5ILSUydqH/KBhW4Pb0jZWnqN/YgbWDKeJxnDbYcUob5ZY5Lt5ZCMKuaGUvCJRrCtuugSMaqjowCGRempsDdJEt+cMaalhZ6gczklJB/IbdwENW9KeVFPoFNFzhxWUIS5ML9riVYhAtE6JE5jX0xiHNVIIPthb458cfA8daR0nYfYAUKogQArm0iBezOO+mPk5vCM=",
|
"serverPublicParams": "ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUjlENAErBme1YHmOSpU6tr6doJ66dPzVAWIanmO/5mgjNEDeK7DDqQdB1xd03HT2Qs2TxY3kCK8aAb/0iM0HQiXjxZ9HIgYhbtvGEnDKW5ILSUydqH/KBhW4Pb0jZWnqN/YgbWDKeJxnDbYcUob5ZY5Lt5ZCMKuaGUvCJRrCtuugSMaqjowCGRempsDdJEt+cMaalhZ6gczklJB/IbdwENW9KeVFPoFNFzhxWUIS5ML9riVYhAtE6JE5jX0xiHNVIIPthb458cfA8daR0nYfYAUKogQArm0iBezOO+mPk5vCM=",
|
||||||
"serverTrustRoot": "BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx"
|
"serverTrustRoot": "BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx",
|
||||||
|
"genericServerPublicParams": "AHILOIrFPXX9laLbalbA9+L1CXpSbM/bTJXZGZiuyK1JaI6dK5FHHWL6tWxmHKYAZTSYmElmJ5z2A5YcirjO/yfoemE03FItyaf8W1fE4p14hzb5qnrmfXUSiAIVrhaXVwIwSzH6RL/+EO8jFIjJ/YfExfJ8aBl48CKHgu1+A6kWynhttonvWWx6h7924mIzW0Czj2ROuh4LwQyZypex4GuOPW8sgIT21KNZaafgg+KbV7XM1x1tF3XA17B4uGUaDbDw2O+nR1+U5p6qHPzmJ7ggFjSN6Utu+35dS1sS0P9N"
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,5 +14,6 @@
|
||||||
"registrationChallengeUrl": "https://signalcaptchas.org/registration/generate.html",
|
"registrationChallengeUrl": "https://signalcaptchas.org/registration/generate.html",
|
||||||
"serverPublicParams": "AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXTLfN0/vLt98KDPnxwAQL9j5V1jGOY8jQl6MLxEs56cwXN0dqCnImzVH3TZT1cJ8SW1BRX6qIVxEzjsSGx3yxF3suAilPMqGRp4ffyopjMD1JXiKR2RwLKzizUe5e8XyGOy9fplzhw3jVzTRyUZTRSZKkMLWcQ/gv0E4aONNqs4P+NameAZYOD12qRkxosQQP5uux6B2nRyZ7sAV54DgFyLiRcq1FvwKw2EPQdk4HDoePrO/RNUbyNddnM/mMgj4FW65xCoT1LmjrIjsv/Ggdlx46ueczhMgtBunx1/w8k8V+l8LVZ8gAT6wkU5J+DPQalQguMg12Jzug3q4TbdHiGCmD9EunCwOmsLuLJkz6EcSYXtrlDEnAM+hicw7iergYLLlMXpfTdGxJCWJmP4zqUFeTTmsmhsjGBt7NiEB/9pFFEB3pSbf4iiUukw63Eo8Aqnf4iwob6X1QviCWuc8t0I=",
|
"serverPublicParams": "AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXTLfN0/vLt98KDPnxwAQL9j5V1jGOY8jQl6MLxEs56cwXN0dqCnImzVH3TZT1cJ8SW1BRX6qIVxEzjsSGx3yxF3suAilPMqGRp4ffyopjMD1JXiKR2RwLKzizUe5e8XyGOy9fplzhw3jVzTRyUZTRSZKkMLWcQ/gv0E4aONNqs4P+NameAZYOD12qRkxosQQP5uux6B2nRyZ7sAV54DgFyLiRcq1FvwKw2EPQdk4HDoePrO/RNUbyNddnM/mMgj4FW65xCoT1LmjrIjsv/Ggdlx46ueczhMgtBunx1/w8k8V+l8LVZ8gAT6wkU5J+DPQalQguMg12Jzug3q4TbdHiGCmD9EunCwOmsLuLJkz6EcSYXtrlDEnAM+hicw7iergYLLlMXpfTdGxJCWJmP4zqUFeTTmsmhsjGBt7NiEB/9pFFEB3pSbf4iiUukw63Eo8Aqnf4iwob6X1QviCWuc8t0I=",
|
||||||
"serverTrustRoot": "BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF",
|
"serverTrustRoot": "BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF",
|
||||||
|
"genericServerPublicParams": "AByD873dTilmOSG0TjKrvpeaKEsUmIO8Vx9BeMmftwUs9v7ikPwM8P3OHyT0+X3EUMZrSe9VUp26Wai51Q9I8mdk0hX/yo7CeFGJyzoOqn8e/i4Ygbn5HoAyXJx5eXfIbqpc0bIxzju4H/HOQeOpt6h742qii5u/cbwOhFZCsMIbElZTaeU+BWMBQiZHIGHT5IE0qCordQKZ5iPZom0HeFa8Yq0ShuEyAl0WINBiY6xE3H/9WnvzXBbMuuk//eRxXgzO8ieCeK8FwQNxbfXqZm6Ro1cMhCOF3u7xoX83QhpN",
|
||||||
"updatesEnabled": true
|
"updatesEnabled": true
|
||||||
}
|
}
|
||||||
|
|
1
images/icons/v3/video/video-display-bold.svg
Normal file
1
images/icons/v3/video/video-display-bold.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" fill="none"><path fill="#5151F6" fill-rule="evenodd" d="M10.302 5.625c-1.22 0-2.203 0-3 .065-.82.067-1.54.209-2.206.548a5.625 5.625 0 0 0-2.458 2.458c-.34.667-.481 1.387-.548 2.207-.065.796-.065 1.78-.065 2.999v8.196c0 1.22 0 2.203.065 3 .067.82.208 1.54.548 2.206a5.625 5.625 0 0 0 2.458 2.458c.667.34 1.387.481 2.207.548.796.065 1.78.065 2.999.065h7.296c1.22 0 2.203 0 3-.065.82-.067 1.54-.209 2.206-.548a5.625 5.625 0 0 0 2.458-2.458c.34-.667.48-1.387.548-2.207.065-.796.065-1.78.065-2.999v-.032l4.775 4.775c1.559 1.56 4.225.455 4.225-1.75V10.909c0-2.205-2.666-3.31-4.225-1.75l-4.775 4.775v-.032c0-1.22 0-2.203-.065-3-.067-.82-.209-1.54-.548-2.206a5.625 5.625 0 0 0-2.458-2.458c-.667-.34-1.387-.481-2.207-.548-.796-.065-1.78-.065-2.999-.065h-7.296Zm13.323 8.325c0-1.279-.001-2.17-.058-2.864-.055-.68-.159-1.072-.31-1.368a3.374 3.374 0 0 0-1.475-1.475c-.296-.151-.687-.255-1.368-.31-.694-.057-1.585-.058-2.864-.058h-7.2c-1.279 0-2.17 0-2.864.058-.68.055-1.072.159-1.368.31a3.375 3.375 0 0 0-1.475 1.475c-.151.296-.255.687-.31 1.368-.057.694-.058 1.585-.058 2.864v8.1c0 1.279 0 2.17.057 2.864.056.68.16 1.072.31 1.368.324.635.84 1.152 1.476 1.475.296.151.687.255 1.368.31.694.057 1.585.058 2.864.058h7.2c1.279 0 2.17 0 2.864-.058.68-.055 1.072-.159 1.368-.31a3.374 3.374 0 0 0 1.475-1.475c.151-.296.255-.687.31-1.368.057-.694.058-1.585.058-2.864v-8.1Zm2.25 4.05c0 .566.225 1.109.625 1.51l5.74 5.74a.21.21 0 0 0 .116.066.24.24 0 0 0 .13-.017.239.239 0 0 0 .104-.08.21.21 0 0 0 .035-.128V10.909a.21.21 0 0 0-.035-.128.238.238 0 0 0-.104-.08.24.24 0 0 0-.13-.017.21.21 0 0 0-.115.066L26.5 16.49c-.4.401-.625.944-.625 1.51Z" clip-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 1.7 KiB |
|
@ -201,7 +201,7 @@
|
||||||
"@electron/notarize": "2.1.0",
|
"@electron/notarize": "2.1.0",
|
||||||
"@formatjs/intl": "2.6.7",
|
"@formatjs/intl": "2.6.7",
|
||||||
"@mixer/parallel-prettier": "2.0.3",
|
"@mixer/parallel-prettier": "2.0.3",
|
||||||
"@signalapp/mock-server": "5.0.1",
|
"@signalapp/mock-server": "5.1.0",
|
||||||
"@storybook/addon-a11y": "7.4.5",
|
"@storybook/addon-a11y": "7.4.5",
|
||||||
"@storybook/addon-actions": "7.4.5",
|
"@storybook/addon-actions": "7.4.5",
|
||||||
"@storybook/addon-controls": "7.4.5",
|
"@storybook/addon-controls": "7.4.5",
|
||||||
|
|
|
@ -606,6 +606,11 @@ message SyncMessage {
|
||||||
optional Event event = 6;
|
optional Event event = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message CallLinkUpdate {
|
||||||
|
optional bytes rootKey = 1;
|
||||||
|
optional bytes adminPasskey = 2;
|
||||||
|
}
|
||||||
|
|
||||||
message CallLogEvent {
|
message CallLogEvent {
|
||||||
enum Type {
|
enum Type {
|
||||||
CLEAR = 0;
|
CLEAR = 0;
|
||||||
|
@ -634,7 +639,7 @@ message SyncMessage {
|
||||||
reserved 17; // pniIdentity
|
reserved 17; // pniIdentity
|
||||||
optional PniChangeNumber pniChangeNumber = 18;
|
optional PniChangeNumber pniChangeNumber = 18;
|
||||||
optional CallEvent callEvent = 19;
|
optional CallEvent callEvent = 19;
|
||||||
reserved 20; // callLinkUpdate
|
optional CallLinkUpdate callLinkUpdate = 20;
|
||||||
optional CallLogEvent callLogEvent = 21;
|
optional CallLogEvent callLogEvent = 21;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7528,3 +7528,45 @@ button.module-image__border-overlay:focus {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module-message__action {
|
||||||
|
@include button-reset();
|
||||||
|
margin-top: 5px;
|
||||||
|
padding: 10px;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
@include font-body-1-bold();
|
||||||
|
border-top: 1px solid;
|
||||||
|
}
|
||||||
|
.module-message__action--outgoing {
|
||||||
|
border-top-color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
.module-message__action--incoming {
|
||||||
|
@include light-theme {
|
||||||
|
border-top-color: rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
@include dark-theme {
|
||||||
|
border-top-color: rgba(255, 255, 255, 0.25);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-message__link-preview__call-link-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
margin-inline-end: 12px;
|
||||||
|
background: #e5e5fe;
|
||||||
|
@include rounded-corners();
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
@include color-svg(
|
||||||
|
'../images/icons/v3/video/video-display-bold.svg',
|
||||||
|
#5151f6
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -91,6 +91,10 @@
|
||||||
background-color: WindowText;
|
background-color: WindowText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--callLink {
|
||||||
|
-webkit-mask-image: url('../images/icons/v3/video/video-display-bold.svg');
|
||||||
|
}
|
||||||
|
|
||||||
&--direct {
|
&--direct {
|
||||||
-webkit-mask-image: url('../images/icons/v3/person/person.svg');
|
-webkit-mask-image: url('../images/icons/v3/person/person.svg');
|
||||||
}
|
}
|
||||||
|
|
83
stylesheets/components/CallingAdhocCallInfo.scss
Normal file
83
stylesheets/components/CallingAdhocCallInfo.scss
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
// Copyright 2024 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
.CallingAdhocCallInfo {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
margin-block-end: auto;
|
||||||
|
padding-block-end: 16px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CallingAdhocCallInfo__width-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 360px;
|
||||||
|
height: auto;
|
||||||
|
padding-block: 1px;
|
||||||
|
padding-inline: 1px;
|
||||||
|
margin-block-end: 102px;
|
||||||
|
margin-inline-start: 90px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CallingAdhocCallInfo__overlay {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CallingAdhocCallInfo__overlay-container {
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CallingAdhocCallInfo__Overlay {
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CallingAdhocCallInfo__MenuItem {
|
||||||
|
@include button-reset;
|
||||||
|
@include font-body-2;
|
||||||
|
display: flex;
|
||||||
|
padding-block: 8px;
|
||||||
|
padding-inline: 10px 2px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CallingAdhocCallInfo__MenuItemIcon {
|
||||||
|
background: $color-gray-65;
|
||||||
|
display: flex;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
margin-inline-end: 8px;
|
||||||
|
border-radius: 32px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CallingAdhocCallInfo__MenuItemIcon:before {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
height: 18px;
|
||||||
|
width: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CallingAdhocCallInfo__MenuItemIcon--copy-link:before {
|
||||||
|
@include color-svg('../images/icons/v3/copy/copy.svg', $color-gray-15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.CallingAdhocCallInfo__MenuItemIcon--share-via-signal:before {
|
||||||
|
@include color-svg('../images/icons/v3/forward/forward.svg', $color-gray-15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.CallingAdhocCallInfo__MenuItemText {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CallingAdhocCallInfo__Divider {
|
||||||
|
display: flex;
|
||||||
|
margin-block: 16px;
|
||||||
|
margin-inline: 10px;
|
||||||
|
border: 1px solid $color-gray-65;
|
||||||
|
}
|
|
@ -36,6 +36,19 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.CallingLobby__CallLinkNotice {
|
||||||
|
@include font-caption;
|
||||||
|
display: flex;
|
||||||
|
padding-block: 12px;
|
||||||
|
padding-inline: 18px;
|
||||||
|
margin-block-end: 32px;
|
||||||
|
width: 340px;
|
||||||
|
background: $color-black-alpha-60;
|
||||||
|
color: $color-white;
|
||||||
|
border-radius: 10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.CallingLobby__Footer {
|
.CallingLobby__Footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -38,6 +38,7 @@
|
||||||
@import './components/BetterAvatarBubble.scss';
|
@import './components/BetterAvatarBubble.scss';
|
||||||
@import './components/Button.scss';
|
@import './components/Button.scss';
|
||||||
@import './components/CallsTab.scss';
|
@import './components/CallsTab.scss';
|
||||||
|
@import './components/CallingAdhocCallInfo.scss';
|
||||||
@import './components/CallingAudioIndicator.scss';
|
@import './components/CallingAudioIndicator.scss';
|
||||||
@import './components/CallingStatusIndicator.scss';
|
@import './components/CallingStatusIndicator.scss';
|
||||||
@import './components/CallingButton.scss';
|
@import './components/CallingButton.scss';
|
||||||
|
|
|
@ -15,6 +15,7 @@ import { HashType } from './types/Crypto';
|
||||||
import { getCountryCode } from './types/PhoneNumber';
|
import { getCountryCode } from './types/PhoneNumber';
|
||||||
|
|
||||||
export type ConfigKeyType =
|
export type ConfigKeyType =
|
||||||
|
| 'desktop.calling.adhoc'
|
||||||
| 'desktop.clientExpiration'
|
| 'desktop.clientExpiration'
|
||||||
| 'desktop.groupMultiTypingIndicators'
|
| 'desktop.groupMultiTypingIndicators'
|
||||||
| 'desktop.internalUser'
|
| 'desktop.internalUser'
|
||||||
|
|
|
@ -57,7 +57,7 @@ export type Props = {
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
|
|
||||||
acceptedMessageRequest: boolean;
|
acceptedMessageRequest: boolean;
|
||||||
conversationType: 'group' | 'direct';
|
conversationType: 'group' | 'direct' | 'callLink';
|
||||||
isMe: boolean;
|
isMe: boolean;
|
||||||
noteToSelf?: boolean;
|
noteToSelf?: boolean;
|
||||||
phoneNumber?: string;
|
phoneNumber?: string;
|
||||||
|
|
|
@ -74,6 +74,7 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
hangUpActiveCall: action('hang-up-active-call'),
|
hangUpActiveCall: action('hang-up-active-call'),
|
||||||
i18n,
|
i18n,
|
||||||
incomingCall: null,
|
incomingCall: null,
|
||||||
|
callLink: undefined,
|
||||||
isGroupCallRaiseHandEnabled: true,
|
isGroupCallRaiseHandEnabled: true,
|
||||||
isGroupCallReactionsEnabled: true,
|
isGroupCallReactionsEnabled: true,
|
||||||
keyChangeOk: action('key-change-ok'),
|
keyChangeOk: action('key-change-ok'),
|
||||||
|
@ -101,6 +102,7 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
setPresenting: action('toggle-presenting'),
|
setPresenting: action('toggle-presenting'),
|
||||||
setRendererCanvas: action('set-renderer-canvas'),
|
setRendererCanvas: action('set-renderer-canvas'),
|
||||||
setOutgoingRing: action('set-outgoing-ring'),
|
setOutgoingRing: action('set-outgoing-ring'),
|
||||||
|
showToast: action('show-toast'),
|
||||||
startCall: action('start-call'),
|
startCall: action('start-call'),
|
||||||
stopRingtone: action('stop-ringtone'),
|
stopRingtone: action('stop-ringtone'),
|
||||||
switchToPresentationView: action('switch-to-presentation-view'),
|
switchToPresentationView: action('switch-to-presentation-view'),
|
||||||
|
|
|
@ -15,6 +15,7 @@ import type { SafetyNumberProps } from './SafetyNumberChangeDialog';
|
||||||
import { SafetyNumberChangeDialog } from './SafetyNumberChangeDialog';
|
import { SafetyNumberChangeDialog } from './SafetyNumberChangeDialog';
|
||||||
import type {
|
import type {
|
||||||
ActiveCallType,
|
ActiveCallType,
|
||||||
|
CallingConversationType,
|
||||||
CallViewMode,
|
CallViewMode,
|
||||||
GroupCallVideoRequest,
|
GroupCallVideoRequest,
|
||||||
PresentedSource,
|
PresentedSource,
|
||||||
|
@ -43,12 +44,19 @@ import type {
|
||||||
SetRendererCanvasType,
|
SetRendererCanvasType,
|
||||||
StartCallType,
|
StartCallType,
|
||||||
} from '../state/ducks/calling';
|
} from '../state/ducks/calling';
|
||||||
|
import { CallLinkRestrictions } from '../types/CallLink';
|
||||||
|
import type { CallLinkType } from '../types/CallLink';
|
||||||
import type { LocalizerType, ThemeType } from '../types/Util';
|
import type { LocalizerType, ThemeType } from '../types/Util';
|
||||||
import { missingCaseError } from '../util/missingCaseError';
|
import { missingCaseError } from '../util/missingCaseError';
|
||||||
import { CallingToastProvider } from './CallingToast';
|
import { CallingToastProvider } from './CallingToast';
|
||||||
import type { SmartReactionPicker } from '../state/smart/ReactionPicker';
|
import type { SmartReactionPicker } from '../state/smart/ReactionPicker';
|
||||||
import type { Props as ReactionPickerProps } from './conversation/ReactionPicker';
|
import type { Props as ReactionPickerProps } from './conversation/ReactionPicker';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
|
import { isGroupOrAdhocActiveCall } from '../util/isGroupOrAdhocCall';
|
||||||
|
import { CallingAdhocCallInfo } from './CallingAdhocCallInfo';
|
||||||
|
import { callLinkRootKeyToUrl } from '../util/callLinkRootKeyToUrl';
|
||||||
|
import { ToastType } from '../types/Toast';
|
||||||
|
import type { ShowToastAction } from '../state/ducks/toast';
|
||||||
|
|
||||||
const GROUP_CALL_RING_DURATION = 60 * 1000;
|
const GROUP_CALL_RING_DURATION = 60 * 1000;
|
||||||
|
|
||||||
|
@ -73,6 +81,7 @@ export type GroupIncomingCall = Readonly<{
|
||||||
export type PropsType = {
|
export type PropsType = {
|
||||||
activeCall?: ActiveCallType;
|
activeCall?: ActiveCallType;
|
||||||
availableCameras: Array<MediaDeviceInfo>;
|
availableCameras: Array<MediaDeviceInfo>;
|
||||||
|
callLink: CallLinkType | undefined;
|
||||||
cancelCall: (_: CancelCallType) => void;
|
cancelCall: (_: CancelCallType) => void;
|
||||||
changeCallView: (mode: CallViewMode) => void;
|
changeCallView: (mode: CallViewMode) => void;
|
||||||
closeNeedPermissionScreen: () => void;
|
closeNeedPermissionScreen: () => void;
|
||||||
|
@ -116,6 +125,7 @@ export type PropsType = {
|
||||||
setOutgoingRing: (_: boolean) => void;
|
setOutgoingRing: (_: boolean) => void;
|
||||||
setPresenting: (_?: PresentedSource) => void;
|
setPresenting: (_?: PresentedSource) => void;
|
||||||
setRendererCanvas: (_: SetRendererCanvasType) => void;
|
setRendererCanvas: (_: SetRendererCanvasType) => void;
|
||||||
|
showToast: ShowToastAction;
|
||||||
stopRingtone: () => unknown;
|
stopRingtone: () => unknown;
|
||||||
switchToPresentationView: () => void;
|
switchToPresentationView: () => void;
|
||||||
switchFromPresentationView: () => void;
|
switchFromPresentationView: () => void;
|
||||||
|
@ -135,6 +145,7 @@ type ActiveCallManagerPropsType = PropsType & {
|
||||||
function ActiveCallManager({
|
function ActiveCallManager({
|
||||||
activeCall,
|
activeCall,
|
||||||
availableCameras,
|
availableCameras,
|
||||||
|
callLink,
|
||||||
cancelCall,
|
cancelCall,
|
||||||
changeCallView,
|
changeCallView,
|
||||||
closeNeedPermissionScreen,
|
closeNeedPermissionScreen,
|
||||||
|
@ -161,6 +172,7 @@ function ActiveCallManager({
|
||||||
setPresenting,
|
setPresenting,
|
||||||
setRendererCanvas,
|
setRendererCanvas,
|
||||||
setOutgoingRing,
|
setOutgoingRing,
|
||||||
|
showToast,
|
||||||
startCall,
|
startCall,
|
||||||
switchToPresentationView,
|
switchToPresentationView,
|
||||||
switchFromPresentationView,
|
switchFromPresentationView,
|
||||||
|
@ -224,6 +236,18 @@ function ActiveCallManager({
|
||||||
[setGroupCallVideoRequest, conversation.id]
|
[setGroupCallVideoRequest, conversation.id]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onCopyCallLink = useCallback(async () => {
|
||||||
|
if (!callLink) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const link = callLinkRootKeyToUrl(callLink.rootKey);
|
||||||
|
if (link) {
|
||||||
|
await window.navigator.clipboard.writeText(link);
|
||||||
|
showToast({ toastType: ToastType.CopiedCallLink });
|
||||||
|
}
|
||||||
|
}, [callLink, showToast]);
|
||||||
|
|
||||||
const onSafetyNumberDialogCancel = useCallback(() => {
|
const onSafetyNumberDialogCancel = useCallback(() => {
|
||||||
hangUpActiveCall('safety number dialog cancel');
|
hangUpActiveCall('safety number dialog cancel');
|
||||||
}, [hangUpActiveCall]);
|
}, [hangUpActiveCall]);
|
||||||
|
@ -234,6 +258,7 @@ function ActiveCallManager({
|
||||||
| undefined
|
| undefined
|
||||||
| Array<Pick<ConversationType, 'id' | 'firstName' | 'title'>>;
|
| Array<Pick<ConversationType, 'id' | 'firstName' | 'title'>>;
|
||||||
let isConvoTooBigToRing = false;
|
let isConvoTooBigToRing = false;
|
||||||
|
let isAdhocJoinRequestPending = false;
|
||||||
|
|
||||||
switch (activeCall.callMode) {
|
switch (activeCall.callMode) {
|
||||||
case CallMode.Direct: {
|
case CallMode.Direct: {
|
||||||
|
@ -256,11 +281,15 @@ function ActiveCallManager({
|
||||||
groupMembers = undefined;
|
groupMembers = undefined;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case CallMode.Group: {
|
case CallMode.Group:
|
||||||
|
case CallMode.Adhoc: {
|
||||||
showCallLobby = activeCall.joinState !== GroupCallJoinState.Joined;
|
showCallLobby = activeCall.joinState !== GroupCallJoinState.Joined;
|
||||||
isCallFull = activeCall.deviceCount >= activeCall.maxDevices;
|
isCallFull = activeCall.deviceCount >= activeCall.maxDevices;
|
||||||
isConvoTooBigToRing = activeCall.isConversationTooBigToRing;
|
isConvoTooBigToRing = activeCall.isConversationTooBigToRing;
|
||||||
({ groupMembers } = activeCall);
|
({ groupMembers } = activeCall);
|
||||||
|
isAdhocJoinRequestPending =
|
||||||
|
callLink?.restrictions === CallLinkRestrictions.AdminApproval &&
|
||||||
|
activeCall.joinState === GroupCallJoinState.Pending;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
@ -272,12 +301,13 @@ function ActiveCallManager({
|
||||||
<>
|
<>
|
||||||
<CallingLobby
|
<CallingLobby
|
||||||
availableCameras={availableCameras}
|
availableCameras={availableCameras}
|
||||||
|
callMode={activeCall.callMode}
|
||||||
conversation={conversation}
|
conversation={conversation}
|
||||||
groupMembers={groupMembers}
|
groupMembers={groupMembers}
|
||||||
hasLocalAudio={hasLocalAudio}
|
hasLocalAudio={hasLocalAudio}
|
||||||
hasLocalVideo={hasLocalVideo}
|
hasLocalVideo={hasLocalVideo}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
isGroupCall={activeCall.callMode === CallMode.Group}
|
isAdhocJoinRequestPending={isAdhocJoinRequestPending}
|
||||||
isCallFull={isCallFull}
|
isCallFull={isCallFull}
|
||||||
isConversationTooBigToRing={isConvoTooBigToRing}
|
isConversationTooBigToRing={isConvoTooBigToRing}
|
||||||
me={me}
|
me={me}
|
||||||
|
@ -294,14 +324,24 @@ function ActiveCallManager({
|
||||||
toggleSettings={toggleSettings}
|
toggleSettings={toggleSettings}
|
||||||
/>
|
/>
|
||||||
{settingsDialogOpen && renderDeviceSelection()}
|
{settingsDialogOpen && renderDeviceSelection()}
|
||||||
{showParticipantsList && activeCall.callMode === CallMode.Group ? (
|
{showParticipantsList &&
|
||||||
|
(activeCall.callMode === CallMode.Adhoc && callLink ? (
|
||||||
|
<CallingAdhocCallInfo
|
||||||
|
callLink={callLink}
|
||||||
|
i18n={i18n}
|
||||||
|
ourServiceId={me.serviceId}
|
||||||
|
participants={peekedParticipants}
|
||||||
|
onClose={toggleParticipants}
|
||||||
|
onCopyCallLink={onCopyCallLink}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<CallingParticipantsList
|
<CallingParticipantsList
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
onClose={toggleParticipants}
|
onClose={toggleParticipants}
|
||||||
ourServiceId={me.serviceId}
|
ourServiceId={me.serviceId}
|
||||||
participants={peekedParticipants}
|
participants={peekedParticipants}
|
||||||
/>
|
/>
|
||||||
) : null}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -325,22 +365,18 @@ function ActiveCallManager({
|
||||||
}
|
}
|
||||||
|
|
||||||
let isHandRaised = false;
|
let isHandRaised = false;
|
||||||
if (activeCall.callMode === CallMode.Group) {
|
if (isGroupOrAdhocActiveCall(activeCall)) {
|
||||||
const { raisedHands, localDemuxId } = activeCall;
|
const { raisedHands, localDemuxId } = activeCall;
|
||||||
if (localDemuxId) {
|
if (localDemuxId) {
|
||||||
isHandRaised = raisedHands.has(localDemuxId);
|
isHandRaised = raisedHands.has(localDemuxId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const groupCallParticipantsForParticipantsList =
|
const groupCallParticipantsForParticipantsList = isGroupOrAdhocActiveCall(
|
||||||
activeCall.callMode === CallMode.Group
|
activeCall
|
||||||
|
)
|
||||||
? [
|
? [
|
||||||
...activeCall.remoteParticipants.map(participant => ({
|
...activeCall.remoteParticipants,
|
||||||
...participant,
|
|
||||||
hasRemoteAudio: participant.hasRemoteAudio,
|
|
||||||
hasRemoteVideo: participant.hasRemoteVideo,
|
|
||||||
presenting: participant.presenting,
|
|
||||||
})),
|
|
||||||
{
|
{
|
||||||
...me,
|
...me,
|
||||||
hasRemoteAudio: hasLocalAudio,
|
hasRemoteAudio: hasLocalAudio,
|
||||||
|
@ -393,15 +429,25 @@ function ActiveCallManager({
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{settingsDialogOpen && renderDeviceSelection()}
|
{settingsDialogOpen && renderDeviceSelection()}
|
||||||
{showParticipantsList && activeCall.callMode === CallMode.Group ? (
|
{showParticipantsList &&
|
||||||
|
(activeCall.callMode === CallMode.Adhoc && callLink ? (
|
||||||
|
<CallingAdhocCallInfo
|
||||||
|
callLink={callLink}
|
||||||
|
i18n={i18n}
|
||||||
|
ourServiceId={me.serviceId}
|
||||||
|
participants={groupCallParticipantsForParticipantsList}
|
||||||
|
onClose={toggleParticipants}
|
||||||
|
onCopyCallLink={onCopyCallLink}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<CallingParticipantsList
|
<CallingParticipantsList
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
onClose={toggleParticipants}
|
onClose={toggleParticipants}
|
||||||
ourServiceId={me.serviceId}
|
ourServiceId={me.serviceId}
|
||||||
participants={groupCallParticipantsForParticipantsList}
|
participants={groupCallParticipantsForParticipantsList}
|
||||||
/>
|
/>
|
||||||
) : null}
|
))}
|
||||||
{activeCall.callMode === CallMode.Group &&
|
{isGroupOrAdhocActiveCall(activeCall) &&
|
||||||
activeCall.conversationsWithSafetyNumberChanges.length ? (
|
activeCall.conversationsWithSafetyNumberChanges.length ? (
|
||||||
<SafetyNumberChangeDialog
|
<SafetyNumberChangeDialog
|
||||||
confirmText={i18n('icu:continueCall')}
|
confirmText={i18n('icu:continueCall')}
|
||||||
|
@ -462,7 +508,7 @@ export function CallManager(props: PropsType): JSX.Element | null {
|
||||||
}, [shouldRing, playRingtone, stopRingtone]);
|
}, [shouldRing, playRingtone, stopRingtone]);
|
||||||
|
|
||||||
const mightBeRingingOutgoingGroupCall =
|
const mightBeRingingOutgoingGroupCall =
|
||||||
activeCall?.callMode === CallMode.Group &&
|
isGroupOrAdhocActiveCall(activeCall) &&
|
||||||
activeCall.outgoingRing &&
|
activeCall.outgoingRing &&
|
||||||
activeCall.joinState !== GroupCallJoinState.NotJoined;
|
activeCall.joinState !== GroupCallJoinState.NotJoined;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -527,7 +573,7 @@ function hasRemoteParticipants(
|
||||||
return remoteParticipants.length > 0;
|
return remoteParticipants.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isLonelyGroup(conversation: ConversationType): boolean {
|
function isLonelyGroup(conversation: CallingConversationType): boolean {
|
||||||
return (conversation.sortedGroupMembers?.length ?? 0) < 2;
|
return (conversation.sortedGroupMembers?.length ?? 0) < 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -563,18 +609,20 @@ function getShouldRing({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Adhoc calls can't be incoming.
|
||||||
|
|
||||||
throw missingCaseError(incomingCall);
|
throw missingCaseError(incomingCall);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activeCall != null) {
|
if (activeCall != null) {
|
||||||
if (activeCall.callMode === CallMode.Direct) {
|
switch (activeCall.callMode) {
|
||||||
|
case CallMode.Direct:
|
||||||
return (
|
return (
|
||||||
activeCall.callState === CallState.Prering ||
|
activeCall.callState === CallState.Prering ||
|
||||||
activeCall.callState === CallState.Ringing
|
activeCall.callState === CallState.Ringing
|
||||||
);
|
);
|
||||||
}
|
case CallMode.Group:
|
||||||
|
case CallMode.Adhoc:
|
||||||
if (activeCall.callMode === CallMode.Group) {
|
|
||||||
return (
|
return (
|
||||||
activeCall.outgoingRing &&
|
activeCall.outgoingRing &&
|
||||||
isConnected(activeCall.connectionState) &&
|
isConnected(activeCall.connectionState) &&
|
||||||
|
@ -582,10 +630,10 @@ function getShouldRing({
|
||||||
!hasRemoteParticipants(activeCall.remoteParticipants) &&
|
!hasRemoteParticipants(activeCall.remoteParticipants) &&
|
||||||
!isLonelyGroup(activeCall.conversation)
|
!isLonelyGroup(activeCall.conversation)
|
||||||
);
|
);
|
||||||
}
|
default:
|
||||||
|
|
||||||
throw missingCaseError(activeCall);
|
throw missingCaseError(activeCall);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,27 +3,46 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { LocalizerType } from '../types/Util';
|
import type { LocalizerType } from '../types/Util';
|
||||||
|
import { CallMode } from '../types/Calling';
|
||||||
|
|
||||||
export type PropsType = {
|
export type PropsType = {
|
||||||
|
callMode: CallMode;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
isAdhocJoinRequestPending?: boolean;
|
||||||
groupMemberCount?: number;
|
groupMemberCount?: number;
|
||||||
participantCount: number;
|
participantCount: number;
|
||||||
toggleParticipants: () => void;
|
toggleParticipants: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function CallParticipantCount({
|
export function CallParticipantCount({
|
||||||
|
callMode,
|
||||||
i18n,
|
i18n,
|
||||||
|
isAdhocJoinRequestPending,
|
||||||
groupMemberCount,
|
groupMemberCount,
|
||||||
participantCount,
|
participantCount,
|
||||||
toggleParticipants,
|
toggleParticipants,
|
||||||
}: PropsType): JSX.Element {
|
}: PropsType): JSX.Element {
|
||||||
|
const isToggleVisible =
|
||||||
|
Boolean(participantCount) || callMode === CallMode.Adhoc;
|
||||||
const count = participantCount || groupMemberCount || 1;
|
const count = participantCount || groupMemberCount || 1;
|
||||||
const innerText = i18n('icu:CallControls__InfoDisplay--participants', {
|
let innerText: string | undefined;
|
||||||
|
if (callMode === CallMode.Adhoc) {
|
||||||
|
if (isAdhocJoinRequestPending) {
|
||||||
|
innerText = i18n(
|
||||||
|
'icu:CallControls__InfoDisplay--adhoc-join-request-pending'
|
||||||
|
);
|
||||||
|
} else if (!participantCount) {
|
||||||
|
innerText = i18n('icu:CallControls__InfoDisplay--adhoc-call');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!innerText) {
|
||||||
|
innerText = i18n('icu:CallControls__InfoDisplay--participants', {
|
||||||
count: String(count),
|
count: String(count),
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Call not started, can't click to show participants
|
// Call not started, can't click to show participants
|
||||||
if (!participantCount) {
|
if (!isToggleVisible) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
aria-label={i18n('icu:calling__participants', {
|
aria-label={i18n('icu:calling__participants', {
|
||||||
|
|
|
@ -85,6 +85,8 @@ import {
|
||||||
CallReactionBurstProvider,
|
CallReactionBurstProvider,
|
||||||
useCallReactionBursts,
|
useCallReactionBursts,
|
||||||
} from './CallReactionBurst';
|
} from './CallReactionBurst';
|
||||||
|
import { isGroupOrAdhocActiveCall } from '../util/isGroupOrAdhocCall';
|
||||||
|
import { assertDev } from '../util/assert';
|
||||||
|
|
||||||
export type PropsType = {
|
export type PropsType = {
|
||||||
activeCall: ActiveCallType;
|
activeCall: ActiveCallType;
|
||||||
|
@ -378,6 +380,7 @@ export function CallScreen({
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case CallMode.Group:
|
case CallMode.Group:
|
||||||
|
case CallMode.Adhoc:
|
||||||
isRinging =
|
isRinging =
|
||||||
activeCall.outgoingRing &&
|
activeCall.outgoingRing &&
|
||||||
!activeCall.remoteParticipants.length &&
|
!activeCall.remoteParticipants.length &&
|
||||||
|
@ -475,7 +478,7 @@ export function CallScreen({
|
||||||
'module-ongoing-call__controls--fadeOut': controlsFadedOut,
|
'module-ongoing-call__controls--fadeOut': controlsFadedOut,
|
||||||
});
|
});
|
||||||
|
|
||||||
const isGroupCall = activeCall.callMode === CallMode.Group;
|
const isGroupCall = isGroupOrAdhocActiveCall(activeCall);
|
||||||
|
|
||||||
let presentingButtonType: CallingButtonType;
|
let presentingButtonType: CallingButtonType;
|
||||||
if (presentingSource) {
|
if (presentingSource) {
|
||||||
|
@ -486,8 +489,9 @@ export function CallScreen({
|
||||||
presentingButtonType = CallingButtonType.PRESENTING_OFF;
|
presentingButtonType = CallingButtonType.PRESENTING_OFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
const raisedHands =
|
const raisedHands = isGroupOrAdhocActiveCall(activeCall)
|
||||||
activeCall.callMode === CallMode.Group ? activeCall.raisedHands : undefined;
|
? activeCall.raisedHands
|
||||||
|
: undefined;
|
||||||
|
|
||||||
// This is the value of our hand raised as seen by remote clients. We should prefer
|
// This is the value of our hand raised as seen by remote clients. We should prefer
|
||||||
// to use it in UI so the user understands what remote clients see.
|
// to use it in UI so the user understands what remote clients see.
|
||||||
|
@ -614,6 +618,7 @@ export function CallScreen({
|
||||||
if (isGroupCall) {
|
if (isGroupCall) {
|
||||||
return (
|
return (
|
||||||
<CallParticipantCount
|
<CallParticipantCount
|
||||||
|
callMode={activeCall.callMode}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
participantCount={participantCount}
|
participantCount={participantCount}
|
||||||
toggleParticipants={toggleParticipants}
|
toggleParticipants={toggleParticipants}
|
||||||
|
@ -635,6 +640,7 @@ export function CallScreen({
|
||||||
i18n,
|
i18n,
|
||||||
isRinging,
|
isRinging,
|
||||||
isConnected,
|
isConnected,
|
||||||
|
activeCall.callMode,
|
||||||
activeCall.joinedAt,
|
activeCall.joinedAt,
|
||||||
isReconnecting,
|
isReconnecting,
|
||||||
isGroupCall,
|
isGroupCall,
|
||||||
|
@ -647,6 +653,10 @@ export function CallScreen({
|
||||||
let remoteParticipantsElement: ReactNode;
|
let remoteParticipantsElement: ReactNode;
|
||||||
switch (activeCall.callMode) {
|
switch (activeCall.callMode) {
|
||||||
case CallMode.Direct: {
|
case CallMode.Direct: {
|
||||||
|
assertDev(
|
||||||
|
conversation.type === 'direct',
|
||||||
|
'direct call must have direct conversation'
|
||||||
|
);
|
||||||
remoteParticipantsElement = hasCallStarted ? (
|
remoteParticipantsElement = hasCallStarted ? (
|
||||||
<DirectCallRemoteParticipant
|
<DirectCallRemoteParticipant
|
||||||
conversation={conversation}
|
conversation={conversation}
|
||||||
|
@ -661,6 +671,7 @@ export function CallScreen({
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case CallMode.Group:
|
case CallMode.Group:
|
||||||
|
case CallMode.Adhoc:
|
||||||
remoteParticipantsElement = (
|
remoteParticipantsElement = (
|
||||||
<GroupCallRemoteParticipants
|
<GroupCallRemoteParticipants
|
||||||
callViewMode={activeCall.viewMode}
|
callViewMode={activeCall.viewMode}
|
||||||
|
@ -846,6 +857,7 @@ export function CallScreen({
|
||||||
onPick: emoji => {
|
onPick: emoji => {
|
||||||
setShowReactionPicker(false);
|
setShowReactionPicker(false);
|
||||||
sendGroupCallReaction({
|
sendGroupCallReaction({
|
||||||
|
callMode: activeCall.callMode,
|
||||||
conversationId: conversation.id,
|
conversationId: conversation.id,
|
||||||
value: emoji,
|
value: emoji,
|
||||||
});
|
});
|
||||||
|
@ -932,12 +944,13 @@ export function CallScreen({
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCallModeClassSuffix(
|
function getCallModeClassSuffix(
|
||||||
callMode: CallMode.Direct | CallMode.Group
|
callMode: CallMode.Direct | CallMode.Group | CallMode.Adhoc
|
||||||
): string {
|
): string {
|
||||||
switch (callMode) {
|
switch (callMode) {
|
||||||
case CallMode.Direct:
|
case CallMode.Direct:
|
||||||
return 'direct';
|
return 'direct';
|
||||||
case CallMode.Group:
|
case CallMode.Group:
|
||||||
|
case CallMode.Adhoc:
|
||||||
return 'group';
|
return 'group';
|
||||||
default:
|
default:
|
||||||
throw missingCaseError(callMode);
|
throw missingCaseError(callMode);
|
||||||
|
|
136
ts/components/CallingAdhocCallInfo.stories.tsx
Normal file
136
ts/components/CallingAdhocCallInfo.stories.tsx
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
// Copyright 2024 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import { sample } from 'lodash';
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
|
||||||
|
import type { Meta } from '@storybook/react';
|
||||||
|
import type { PropsType } from './CallingAdhocCallInfo';
|
||||||
|
import { CallingAdhocCallInfo } from './CallingAdhocCallInfo';
|
||||||
|
import { AvatarColors } from '../types/Colors';
|
||||||
|
import type { GroupCallRemoteParticipantType } from '../types/Calling';
|
||||||
|
import { generateAci } from '../types/ServiceId';
|
||||||
|
import { getDefaultConversationWithServiceId } from '../test-both/helpers/getDefaultConversation';
|
||||||
|
import { setupI18n } from '../util/setupI18n';
|
||||||
|
import enMessages from '../../_locales/en/messages.json';
|
||||||
|
import type { CallLinkType } from '../types/CallLink';
|
||||||
|
import { CallLinkRestrictions } from '../types/CallLink';
|
||||||
|
|
||||||
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
function createParticipant(
|
||||||
|
participantProps: Partial<GroupCallRemoteParticipantType>
|
||||||
|
): GroupCallRemoteParticipantType {
|
||||||
|
return {
|
||||||
|
aci: generateAci(),
|
||||||
|
demuxId: 2,
|
||||||
|
hasRemoteAudio: Boolean(participantProps.hasRemoteAudio),
|
||||||
|
hasRemoteVideo: Boolean(participantProps.hasRemoteVideo),
|
||||||
|
isHandRaised: Boolean(participantProps.isHandRaised),
|
||||||
|
mediaKeysReceived: Boolean(participantProps.mediaKeysReceived),
|
||||||
|
presenting: Boolean(participantProps.presenting),
|
||||||
|
sharingScreen: Boolean(participantProps.sharingScreen),
|
||||||
|
videoAspectRatio: 1.3,
|
||||||
|
...getDefaultConversationWithServiceId({
|
||||||
|
avatarPath: participantProps.avatarPath,
|
||||||
|
color: sample(AvatarColors),
|
||||||
|
isBlocked: Boolean(participantProps.isBlocked),
|
||||||
|
name: participantProps.name,
|
||||||
|
profileName: participantProps.title,
|
||||||
|
title: String(participantProps.title),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCallLink(overrideProps: Partial<CallLinkType> = {}): CallLinkType {
|
||||||
|
// Normally, roomId would be derived from rootKey however we don't want to import
|
||||||
|
// ringrtc in storybook
|
||||||
|
return {
|
||||||
|
roomId: 'abcd1234abcd1234abcd1234abcd1234abcd1234',
|
||||||
|
rootKey: 'abcd-abcd-abcd-abcd-abcd-abcd-abcd-abcd',
|
||||||
|
name: 'Axolotl Discuss',
|
||||||
|
restrictions: CallLinkRestrictions.None,
|
||||||
|
expiration: Date.now() + 30 * 24 * 60 * 60 * 1000,
|
||||||
|
...overrideProps,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
|
callLink: getCallLink(overrideProps.callLink || {}),
|
||||||
|
i18n,
|
||||||
|
ourServiceId: generateAci(),
|
||||||
|
participants: overrideProps.participants || [],
|
||||||
|
onClose: action('on-close'),
|
||||||
|
onCopyCallLink: action('on-copy-call-link'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Components/CallingAdhocCallInfo',
|
||||||
|
} satisfies Meta<PropsType>;
|
||||||
|
|
||||||
|
export function NoOne(): JSX.Element {
|
||||||
|
const props = createProps();
|
||||||
|
return <CallingAdhocCallInfo {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SoloCall(): JSX.Element {
|
||||||
|
const props = createProps({
|
||||||
|
participants: [
|
||||||
|
createParticipant({
|
||||||
|
title: 'Bardock',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return <CallingAdhocCallInfo {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ManyParticipants(): JSX.Element {
|
||||||
|
const props = createProps({
|
||||||
|
participants: [
|
||||||
|
createParticipant({
|
||||||
|
title: 'Son Goku',
|
||||||
|
}),
|
||||||
|
createParticipant({
|
||||||
|
hasRemoteAudio: true,
|
||||||
|
hasRemoteVideo: true,
|
||||||
|
presenting: true,
|
||||||
|
name: 'Rage Trunks',
|
||||||
|
title: 'Rage Trunks',
|
||||||
|
}),
|
||||||
|
createParticipant({
|
||||||
|
hasRemoteAudio: true,
|
||||||
|
title: 'Prince Vegeta',
|
||||||
|
}),
|
||||||
|
createParticipant({
|
||||||
|
hasRemoteAudio: true,
|
||||||
|
hasRemoteVideo: true,
|
||||||
|
name: 'Goku Black',
|
||||||
|
title: 'Goku Black',
|
||||||
|
}),
|
||||||
|
createParticipant({
|
||||||
|
isHandRaised: true,
|
||||||
|
title: 'Supreme Kai Zamasu',
|
||||||
|
}),
|
||||||
|
createParticipant({
|
||||||
|
hasRemoteAudio: false,
|
||||||
|
hasRemoteVideo: true,
|
||||||
|
isHandRaised: true,
|
||||||
|
title: 'Chi Chi',
|
||||||
|
}),
|
||||||
|
createParticipant({
|
||||||
|
title: 'Someone With A Really Long Name',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return <CallingAdhocCallInfo {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Overflow(): JSX.Element {
|
||||||
|
const props = createProps({
|
||||||
|
participants: Array(50)
|
||||||
|
.fill(null)
|
||||||
|
.map(() => createParticipant({ title: 'Kirby' })),
|
||||||
|
});
|
||||||
|
return <CallingAdhocCallInfo {...props} />;
|
||||||
|
}
|
162
ts/components/CallingAdhocCallInfo.tsx
Normal file
162
ts/components/CallingAdhocCallInfo.tsx
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
// Copyright 2024 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
/* eslint-disable react/no-array-index-key */
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import { Avatar, AvatarSize } from './Avatar';
|
||||||
|
import { ContactName } from './conversation/ContactName';
|
||||||
|
import { InContactsIcon } from './InContactsIcon';
|
||||||
|
import type { CallLinkType } from '../types/CallLink';
|
||||||
|
import type { LocalizerType } from '../types/Util';
|
||||||
|
import type { ServiceIdString } from '../types/ServiceId';
|
||||||
|
import { sortByTitle } from '../util/sortByTitle';
|
||||||
|
import type { ConversationType } from '../state/ducks/conversations';
|
||||||
|
import { ModalHost } from './ModalHost';
|
||||||
|
import { isInSystemContacts } from '../util/isInSystemContacts';
|
||||||
|
|
||||||
|
type ParticipantType = ConversationType & {
|
||||||
|
hasRemoteAudio?: boolean;
|
||||||
|
hasRemoteVideo?: boolean;
|
||||||
|
isHandRaised?: boolean;
|
||||||
|
presenting?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PropsType = {
|
||||||
|
readonly callLink: CallLinkType;
|
||||||
|
readonly i18n: LocalizerType;
|
||||||
|
readonly ourServiceId: ServiceIdString | undefined;
|
||||||
|
readonly participants: Array<ParticipantType>;
|
||||||
|
readonly onClose: () => void;
|
||||||
|
readonly onCopyCallLink: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function CallingAdhocCallInfo({
|
||||||
|
i18n,
|
||||||
|
ourServiceId,
|
||||||
|
participants,
|
||||||
|
onClose,
|
||||||
|
onCopyCallLink,
|
||||||
|
}: PropsType): JSX.Element | null {
|
||||||
|
const sortedParticipants = React.useMemo<Array<ParticipantType>>(
|
||||||
|
() => sortByTitle(participants),
|
||||||
|
[participants]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalHost
|
||||||
|
modalName="CallingAdhocCallInfo"
|
||||||
|
moduleClassName="CallingAdhocCallInfo"
|
||||||
|
onClose={onClose}
|
||||||
|
>
|
||||||
|
<div className="CallingAdhocCallInfo module-calling-participants-list">
|
||||||
|
<div className="module-calling-participants-list__header">
|
||||||
|
<div className="module-calling-participants-list__title">
|
||||||
|
{!participants.length && i18n('icu:calling__in-this-call--zero')}
|
||||||
|
{participants.length === 1 &&
|
||||||
|
i18n('icu:calling__in-this-call--one')}
|
||||||
|
{participants.length > 1 &&
|
||||||
|
i18n('icu:calling__in-this-call--many', {
|
||||||
|
people: String(participants.length),
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="module-calling-participants-list__close"
|
||||||
|
onClick={onClose}
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label={i18n('icu:close')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ul className="module-calling-participants-list__list">
|
||||||
|
{sortedParticipants.map(
|
||||||
|
(participant: ParticipantType, index: number) => (
|
||||||
|
<li
|
||||||
|
className="module-calling-participants-list__contact"
|
||||||
|
// It's tempting to use `participant.serviceId` as the `key`
|
||||||
|
// here, but that can result in duplicate keys for
|
||||||
|
// participants who have joined on multiple devices.
|
||||||
|
key={index}
|
||||||
|
>
|
||||||
|
<div className="module-calling-participants-list__avatar-and-name">
|
||||||
|
<Avatar
|
||||||
|
acceptedMessageRequest={participant.acceptedMessageRequest}
|
||||||
|
avatarPath={participant.avatarPath}
|
||||||
|
badge={undefined}
|
||||||
|
color={participant.color}
|
||||||
|
conversationType="direct"
|
||||||
|
i18n={i18n}
|
||||||
|
isMe={participant.isMe}
|
||||||
|
profileName={participant.profileName}
|
||||||
|
title={participant.title}
|
||||||
|
sharedGroupNames={participant.sharedGroupNames}
|
||||||
|
size={AvatarSize.THIRTY_TWO}
|
||||||
|
/>
|
||||||
|
{ourServiceId && participant.serviceId === ourServiceId ? (
|
||||||
|
<span className="module-calling-participants-list__name">
|
||||||
|
{i18n('icu:you')}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ContactName
|
||||||
|
module="module-calling-participants-list__name"
|
||||||
|
title={participant.title}
|
||||||
|
/>
|
||||||
|
{isInSystemContacts(participant) ? (
|
||||||
|
<span>
|
||||||
|
{' '}
|
||||||
|
<InContactsIcon
|
||||||
|
className="module-calling-participants-list__contact-icon"
|
||||||
|
i18n={i18n}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={classNames(
|
||||||
|
'module-calling-participants-list__status-icon',
|
||||||
|
participant.isHandRaised &&
|
||||||
|
'module-calling-participants-list__hand-raised'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className={classNames(
|
||||||
|
'module-calling-participants-list__status-icon',
|
||||||
|
participant.presenting &&
|
||||||
|
'module-calling-participants-list__presenting',
|
||||||
|
!participant.hasRemoteVideo &&
|
||||||
|
'module-calling-participants-list__muted--video'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className={classNames(
|
||||||
|
'module-calling-participants-list__status-icon',
|
||||||
|
!participant.hasRemoteAudio &&
|
||||||
|
'module-calling-participants-list__muted--audio'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
<div className="CallingAdhocCallInfo__Divider" />
|
||||||
|
<div className="CallingAdhocCallInfo__CallLinkInfo">
|
||||||
|
<button
|
||||||
|
className="CallingAdhocCallInfo__MenuItem"
|
||||||
|
onClick={onCopyCallLink}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span className="CallingAdhocCallInfo__MenuItemIcon CallingAdhocCallInfo__MenuItemIcon--copy-link" />
|
||||||
|
<span className="CallingAdhocCallInfo__MenuItemText">
|
||||||
|
{i18n('icu:CallingAdhocCallInfo__CopyLink')}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalHost>
|
||||||
|
);
|
||||||
|
}
|
|
@ -19,6 +19,7 @@ import {
|
||||||
getDefaultConversationWithServiceId,
|
getDefaultConversationWithServiceId,
|
||||||
} from '../test-both/helpers/getDefaultConversation';
|
} from '../test-both/helpers/getDefaultConversation';
|
||||||
import { CallingToastProvider } from './CallingToast';
|
import { CallingToastProvider } from './CallingToast';
|
||||||
|
import { CallMode } from '../types/Calling';
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
@ -33,8 +34,9 @@ const camera = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => {
|
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => {
|
||||||
const isGroupCall = overrideProps.isGroupCall ?? false;
|
const callMode = overrideProps.callMode ?? CallMode.Direct;
|
||||||
const conversation = isGroupCall
|
const conversation =
|
||||||
|
callMode === CallMode.Group
|
||||||
? getDefaultConversation({
|
? getDefaultConversation({
|
||||||
title: 'Tahoe Trip',
|
title: 'Tahoe Trip',
|
||||||
type: 'group',
|
type: 'group',
|
||||||
|
@ -43,14 +45,17 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
availableCameras: overrideProps.availableCameras || [camera],
|
availableCameras: overrideProps.availableCameras || [camera],
|
||||||
|
callMode,
|
||||||
conversation,
|
conversation,
|
||||||
groupMembers:
|
groupMembers:
|
||||||
overrideProps.groupMembers ||
|
overrideProps.groupMembers ||
|
||||||
(isGroupCall ? times(3, () => getDefaultConversation()) : undefined),
|
(callMode === CallMode.Group
|
||||||
|
? times(3, () => getDefaultConversation())
|
||||||
|
: undefined),
|
||||||
hasLocalAudio: overrideProps.hasLocalAudio ?? true,
|
hasLocalAudio: overrideProps.hasLocalAudio ?? true,
|
||||||
hasLocalVideo: overrideProps.hasLocalVideo ?? false,
|
hasLocalVideo: overrideProps.hasLocalVideo ?? false,
|
||||||
i18n,
|
i18n,
|
||||||
isGroupCall,
|
isAdhocJoinRequestPending: false,
|
||||||
isConversationTooBigToRing: false,
|
isConversationTooBigToRing: false,
|
||||||
isCallFull: overrideProps.isCallFull ?? false,
|
isCallFull: overrideProps.isCallFull ?? false,
|
||||||
me:
|
me:
|
||||||
|
@ -133,13 +138,16 @@ export function InitiallyMuted(): JSX.Element {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GroupCallWithNoPeekedParticipants(): JSX.Element {
|
export function GroupCallWithNoPeekedParticipants(): JSX.Element {
|
||||||
const props = createProps({ isGroupCall: true, peekedParticipants: [] });
|
const props = createProps({
|
||||||
|
callMode: CallMode.Group,
|
||||||
|
peekedParticipants: [],
|
||||||
|
});
|
||||||
return <CallingLobby {...props} />;
|
return <CallingLobby {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GroupCallWith1PeekedParticipant(): JSX.Element {
|
export function GroupCallWith1PeekedParticipant(): JSX.Element {
|
||||||
const props = createProps({
|
const props = createProps({
|
||||||
isGroupCall: true,
|
callMode: CallMode.Group,
|
||||||
peekedParticipants: [{ title: 'Sam' }].map(fakePeekedParticipant),
|
peekedParticipants: [{ title: 'Sam' }].map(fakePeekedParticipant),
|
||||||
});
|
});
|
||||||
return <CallingLobby {...props} />;
|
return <CallingLobby {...props} />;
|
||||||
|
@ -148,7 +156,7 @@ export function GroupCallWith1PeekedParticipant(): JSX.Element {
|
||||||
export function GroupCallWith1PeekedParticipantSelf(): JSX.Element {
|
export function GroupCallWith1PeekedParticipantSelf(): JSX.Element {
|
||||||
const serviceId = generateAci();
|
const serviceId = generateAci();
|
||||||
const props = createProps({
|
const props = createProps({
|
||||||
isGroupCall: true,
|
callMode: CallMode.Group,
|
||||||
me: getDefaultConversation({
|
me: getDefaultConversation({
|
||||||
id: generateUuid(),
|
id: generateUuid(),
|
||||||
serviceId,
|
serviceId,
|
||||||
|
@ -160,7 +168,7 @@ export function GroupCallWith1PeekedParticipantSelf(): JSX.Element {
|
||||||
|
|
||||||
export function GroupCallWith4PeekedParticipants(): JSX.Element {
|
export function GroupCallWith4PeekedParticipants(): JSX.Element {
|
||||||
const props = createProps({
|
const props = createProps({
|
||||||
isGroupCall: true,
|
callMode: CallMode.Group,
|
||||||
peekedParticipants: ['Sam', 'Cayce', 'April', 'Logan', 'Carl'].map(title =>
|
peekedParticipants: ['Sam', 'Cayce', 'April', 'Logan', 'Carl'].map(title =>
|
||||||
fakePeekedParticipant({ title })
|
fakePeekedParticipant({ title })
|
||||||
),
|
),
|
||||||
|
@ -170,7 +178,7 @@ export function GroupCallWith4PeekedParticipants(): JSX.Element {
|
||||||
|
|
||||||
export function GroupCallWith4PeekedParticipantsParticipantsList(): JSX.Element {
|
export function GroupCallWith4PeekedParticipantsParticipantsList(): JSX.Element {
|
||||||
const props = createProps({
|
const props = createProps({
|
||||||
isGroupCall: true,
|
callMode: CallMode.Group,
|
||||||
peekedParticipants: ['Sam', 'Cayce', 'April', 'Logan', 'Carl'].map(title =>
|
peekedParticipants: ['Sam', 'Cayce', 'April', 'Logan', 'Carl'].map(title =>
|
||||||
fakePeekedParticipant({ title })
|
fakePeekedParticipant({ title })
|
||||||
),
|
),
|
||||||
|
@ -181,7 +189,7 @@ export function GroupCallWith4PeekedParticipantsParticipantsList(): JSX.Element
|
||||||
|
|
||||||
export function GroupCallWithCallFull(): JSX.Element {
|
export function GroupCallWithCallFull(): JSX.Element {
|
||||||
const props = createProps({
|
const props = createProps({
|
||||||
isGroupCall: true,
|
callMode: CallMode.Group,
|
||||||
isCallFull: true,
|
isCallFull: true,
|
||||||
peekedParticipants: ['Sam', 'Cayce'].map(title =>
|
peekedParticipants: ['Sam', 'Cayce'].map(title =>
|
||||||
fakePeekedParticipant({ title })
|
fakePeekedParticipant({ title })
|
||||||
|
@ -192,7 +200,7 @@ export function GroupCallWithCallFull(): JSX.Element {
|
||||||
|
|
||||||
export function GroupCallWith0PeekedParticipantsBigGroup(): JSX.Element {
|
export function GroupCallWith0PeekedParticipantsBigGroup(): JSX.Element {
|
||||||
const props = createProps({
|
const props = createProps({
|
||||||
isGroupCall: true,
|
callMode: CallMode.Group,
|
||||||
groupMembers: times(100, () => getDefaultConversation()),
|
groupMembers: times(100, () => getDefaultConversation()),
|
||||||
});
|
});
|
||||||
return <CallingLobby {...props} />;
|
return <CallingLobby {...props} />;
|
||||||
|
|
|
@ -19,17 +19,22 @@ import {
|
||||||
CallingLobbyJoinButton,
|
CallingLobbyJoinButton,
|
||||||
CallingLobbyJoinButtonVariant,
|
CallingLobbyJoinButtonVariant,
|
||||||
} from './CallingLobbyJoinButton';
|
} from './CallingLobbyJoinButton';
|
||||||
|
import { CallMode } from '../types/Calling';
|
||||||
|
import type { CallingConversationType } from '../types/Calling';
|
||||||
import type { LocalizerType } from '../types/Util';
|
import type { LocalizerType } from '../types/Util';
|
||||||
import { useIsOnline } from '../hooks/useIsOnline';
|
import { useIsOnline } from '../hooks/useIsOnline';
|
||||||
import * as KeyboardLayout from '../services/keyboardLayout';
|
import * as KeyboardLayout from '../services/keyboardLayout';
|
||||||
import type { ConversationType } from '../state/ducks/conversations';
|
import type { ConversationType } from '../state/ducks/conversations';
|
||||||
import { useCallingToasts } from './CallingToast';
|
import { useCallingToasts } from './CallingToast';
|
||||||
import { CallingButtonToastsContainer } from './CallingToastManager';
|
import { CallingButtonToastsContainer } from './CallingToastManager';
|
||||||
|
import { isGroupOrAdhocCallMode } from '../util/isGroupOrAdhocCall';
|
||||||
|
import { isSharingPhoneNumberWithEverybody } from '../util/phoneNumberSharingMode';
|
||||||
|
|
||||||
export type PropsType = {
|
export type PropsType = {
|
||||||
availableCameras: Array<MediaDeviceInfo>;
|
availableCameras: Array<MediaDeviceInfo>;
|
||||||
|
callMode: CallMode;
|
||||||
conversation: Pick<
|
conversation: Pick<
|
||||||
ConversationType,
|
CallingConversationType,
|
||||||
| 'acceptedMessageRequest'
|
| 'acceptedMessageRequest'
|
||||||
| 'avatarPath'
|
| 'avatarPath'
|
||||||
| 'color'
|
| 'color'
|
||||||
|
@ -54,8 +59,8 @@ export type PropsType = {
|
||||||
hasLocalAudio: boolean;
|
hasLocalAudio: boolean;
|
||||||
hasLocalVideo: boolean;
|
hasLocalVideo: boolean;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
isAdhocJoinRequestPending: boolean;
|
||||||
isConversationTooBigToRing: boolean;
|
isConversationTooBigToRing: boolean;
|
||||||
isGroupCall: boolean;
|
|
||||||
isCallFull?: boolean;
|
isCallFull?: boolean;
|
||||||
me: Readonly<
|
me: Readonly<
|
||||||
Pick<ConversationType, 'avatarPath' | 'color' | 'id' | 'serviceId'>
|
Pick<ConversationType, 'avatarPath' | 'color' | 'id' | 'serviceId'>
|
||||||
|
@ -75,12 +80,13 @@ export type PropsType = {
|
||||||
|
|
||||||
export function CallingLobby({
|
export function CallingLobby({
|
||||||
availableCameras,
|
availableCameras,
|
||||||
|
callMode,
|
||||||
conversation,
|
conversation,
|
||||||
groupMembers,
|
groupMembers,
|
||||||
hasLocalAudio,
|
hasLocalAudio,
|
||||||
hasLocalVideo,
|
hasLocalVideo,
|
||||||
i18n,
|
i18n,
|
||||||
isGroupCall = false,
|
isAdhocJoinRequestPending,
|
||||||
isCallFull = false,
|
isCallFull = false,
|
||||||
isConversationTooBigToRing,
|
isConversationTooBigToRing,
|
||||||
me,
|
me,
|
||||||
|
@ -99,6 +105,8 @@ export function CallingLobby({
|
||||||
|
|
||||||
const shouldShowLocalVideo = hasLocalVideo && availableCameras.length > 0;
|
const shouldShowLocalVideo = hasLocalVideo && availableCameras.length > 0;
|
||||||
|
|
||||||
|
const isGroupOrAdhocCall = isGroupOrAdhocCallMode(callMode);
|
||||||
|
|
||||||
const toggleAudio = React.useCallback((): void => {
|
const toggleAudio = React.useCallback((): void => {
|
||||||
setLocalAudio({ enabled: !hasLocalAudio });
|
setLocalAudio({ enabled: !hasLocalAudio });
|
||||||
}, [hasLocalAudio, setLocalAudio]);
|
}, [hasLocalAudio, setLocalAudio]);
|
||||||
|
@ -161,12 +169,12 @@ export function CallingLobby({
|
||||||
: CallingButtonType.AUDIO_OFF;
|
: CallingButtonType.AUDIO_OFF;
|
||||||
|
|
||||||
const isRingButtonVisible: boolean =
|
const isRingButtonVisible: boolean =
|
||||||
isGroupCall &&
|
isGroupOrAdhocCall &&
|
||||||
peekedParticipants.length === 0 &&
|
peekedParticipants.length === 0 &&
|
||||||
(groupMembers || []).length > 1;
|
(groupMembers || []).length > 1;
|
||||||
|
|
||||||
let preCallInfoRingMode: RingMode;
|
let preCallInfoRingMode: RingMode;
|
||||||
if (isGroupCall) {
|
if (isGroupOrAdhocCall) {
|
||||||
preCallInfoRingMode =
|
preCallInfoRingMode =
|
||||||
outgoingRing && !isConversationTooBigToRing
|
outgoingRing && !isConversationTooBigToRing
|
||||||
? RingMode.WillRing
|
? RingMode.WillRing
|
||||||
|
@ -205,10 +213,12 @@ export function CallingLobby({
|
||||||
}
|
}
|
||||||
|
|
||||||
const callStatus = React.useMemo(() => {
|
const callStatus = React.useMemo(() => {
|
||||||
if (isGroupCall) {
|
if (isGroupOrAdhocCall) {
|
||||||
return (
|
return (
|
||||||
<CallParticipantCount
|
<CallParticipantCount
|
||||||
|
callMode={callMode}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
|
isAdhocJoinRequestPending={isAdhocJoinRequestPending}
|
||||||
groupMemberCount={groupMembers?.length ?? 0}
|
groupMemberCount={groupMembers?.length ?? 0}
|
||||||
participantCount={peekedParticipants.length}
|
participantCount={peekedParticipants.length}
|
||||||
toggleParticipants={toggleParticipants}
|
toggleParticipants={toggleParticipants}
|
||||||
|
@ -223,7 +233,9 @@ export function CallingLobby({
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, [
|
}, [
|
||||||
isGroupCall,
|
callMode,
|
||||||
|
isAdhocJoinRequestPending,
|
||||||
|
isGroupOrAdhocCall,
|
||||||
peekedParticipants.length,
|
peekedParticipants.length,
|
||||||
i18n,
|
i18n,
|
||||||
hasLocalVideo,
|
hasLocalVideo,
|
||||||
|
@ -252,7 +264,7 @@ export function CallingLobby({
|
||||||
|
|
||||||
<CallingHeader
|
<CallingHeader
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
isGroupCall={isGroupCall}
|
isGroupCall={isGroupOrAdhocCall}
|
||||||
participantCount={peekedParticipants.length}
|
participantCount={peekedParticipants.length}
|
||||||
toggleSettings={toggleSettings}
|
toggleSettings={toggleSettings}
|
||||||
onCancel={onCallCanceled}
|
onCancel={onCallCanceled}
|
||||||
|
@ -280,6 +292,14 @@ export function CallingLobby({
|
||||||
{i18n('icu:calling__your-video-is-off')}
|
{i18n('icu:calling__your-video-is-off')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{callMode === CallMode.Adhoc && (
|
||||||
|
<div className="CallingLobby__CallLinkNotice">
|
||||||
|
{isSharingPhoneNumberWithEverybody()
|
||||||
|
? i18n('icu:CallingLobby__CallLinkNotice--phone-sharing')
|
||||||
|
: i18n('icu:CallingLobby__CallLinkNotice')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<CallingButtonToastsContainer
|
<CallingButtonToastsContainer
|
||||||
hasLocalAudio={hasLocalAudio}
|
hasLocalAudio={hasLocalAudio}
|
||||||
outgoingRing={outgoingRing}
|
outgoingRing={outgoingRing}
|
||||||
|
|
|
@ -23,6 +23,8 @@ import { usePageVisibility } from '../hooks/usePageVisibility';
|
||||||
import { missingCaseError } from '../util/missingCaseError';
|
import { missingCaseError } from '../util/missingCaseError';
|
||||||
import { nonRenderedRemoteParticipant } from '../util/ringrtc/nonRenderedRemoteParticipant';
|
import { nonRenderedRemoteParticipant } from '../util/ringrtc/nonRenderedRemoteParticipant';
|
||||||
import { isReconnecting } from '../util/callingIsReconnecting';
|
import { isReconnecting } from '../util/callingIsReconnecting';
|
||||||
|
import { isGroupOrAdhocActiveCall } from '../util/isGroupOrAdhocCall';
|
||||||
|
import { assertDev } from '../util/assert';
|
||||||
|
|
||||||
// This value should be kept in sync with the hard-coded CSS height. It should also be
|
// This value should be kept in sync with the hard-coded CSS height. It should also be
|
||||||
// less than `MAX_FRAME_HEIGHT`.
|
// less than `MAX_FRAME_HEIGHT`.
|
||||||
|
@ -97,17 +99,17 @@ export function CallingPipRemoteVideo({
|
||||||
|
|
||||||
const activeGroupCallSpeaker: undefined | GroupCallRemoteParticipantType =
|
const activeGroupCallSpeaker: undefined | GroupCallRemoteParticipantType =
|
||||||
useMemo(() => {
|
useMemo(() => {
|
||||||
if (activeCall.callMode !== CallMode.Group) {
|
if (!isGroupOrAdhocActiveCall(activeCall)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return maxBy(activeCall.remoteParticipants, participant =>
|
return maxBy(activeCall.remoteParticipants, participant =>
|
||||||
participant.presenting ? Infinity : participant.speakerTime || -Infinity
|
participant.presenting ? Infinity : participant.speakerTime || -Infinity
|
||||||
);
|
);
|
||||||
}, [activeCall.callMode, activeCall.remoteParticipants]);
|
}, [activeCall]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeCall.callMode !== CallMode.Group) {
|
if (!isGroupOrAdhocActiveCall(activeCall)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,8 +138,7 @@ export function CallingPipRemoteVideo({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
activeCall.callMode,
|
activeCall,
|
||||||
activeCall.remoteParticipants,
|
|
||||||
activeGroupCallSpeaker,
|
activeGroupCallSpeaker,
|
||||||
isPageVisible,
|
isPageVisible,
|
||||||
setGroupCallVideoRequest,
|
setGroupCallVideoRequest,
|
||||||
|
@ -149,6 +150,10 @@ export function CallingPipRemoteVideo({
|
||||||
if (!hasRemoteVideo) {
|
if (!hasRemoteVideo) {
|
||||||
return <NoVideo activeCall={activeCall} i18n={i18n} />;
|
return <NoVideo activeCall={activeCall} i18n={i18n} />;
|
||||||
}
|
}
|
||||||
|
assertDev(
|
||||||
|
conversation.type === 'direct',
|
||||||
|
'CallingPipRemoteVideo for direct call must be associated with direct conversation'
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<div className="module-calling-pip__video--remote">
|
<div className="module-calling-pip__video--remote">
|
||||||
<DirectCallRemoteParticipant
|
<DirectCallRemoteParticipant
|
||||||
|
@ -162,6 +167,7 @@ export function CallingPipRemoteVideo({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
case CallMode.Group:
|
case CallMode.Group:
|
||||||
|
case CallMode.Adhoc:
|
||||||
if (!activeGroupCallSpeaker) {
|
if (!activeGroupCallSpeaker) {
|
||||||
return <NoVideo activeCall={activeCall} i18n={i18n} />;
|
return <NoVideo activeCall={activeCall} i18n={i18n} />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { ConversationType } from '../state/ducks/conversations';
|
import type { ConversationType } from '../state/ducks/conversations';
|
||||||
|
import type { CallingConversationType } from '../types/Calling';
|
||||||
import type { LocalizerType } from '../types/Util';
|
import type { LocalizerType } from '../types/Util';
|
||||||
import { Avatar, AvatarSize } from './Avatar';
|
import { Avatar, AvatarSize } from './Avatar';
|
||||||
import { getParticipantName } from '../util/callingGetParticipantName';
|
import { getParticipantName } from '../util/callingGetParticipantName';
|
||||||
|
@ -17,7 +18,7 @@ export enum RingMode {
|
||||||
|
|
||||||
export type PropsType = {
|
export type PropsType = {
|
||||||
conversation: Pick<
|
conversation: Pick<
|
||||||
ConversationType,
|
CallingConversationType,
|
||||||
| 'acceptedMessageRequest'
|
| 'acceptedMessageRequest'
|
||||||
| 'avatarPath'
|
| 'avatarPath'
|
||||||
| 'color'
|
| 'color'
|
||||||
|
@ -114,6 +115,7 @@ export function CallingPreCallInfo({
|
||||||
memberNames = [getParticipantName(conversation)];
|
memberNames = [getParticipantName(conversation)];
|
||||||
break;
|
break;
|
||||||
case 'group':
|
case 'group':
|
||||||
|
case 'callLink':
|
||||||
memberNames = groupMembers
|
memberNames = groupMembers
|
||||||
.filter(member => member.id !== me.id)
|
.filter(member => member.id !== me.id)
|
||||||
.map(getParticipantName);
|
.map(getParticipantName);
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { CallingToastProvider, useCallingToasts } from './CallingToast';
|
||||||
import { usePrevious } from '../hooks/usePrevious';
|
import { usePrevious } from '../hooks/usePrevious';
|
||||||
import { difference as setDifference } from '../util/setUtil';
|
import { difference as setDifference } from '../util/setUtil';
|
||||||
import { isMoreRecentThan } from '../util/timestamp';
|
import { isMoreRecentThan } from '../util/timestamp';
|
||||||
|
import { isGroupOrAdhocActiveCall } from '../util/isGroupOrAdhocCall';
|
||||||
|
|
||||||
type PropsType = {
|
type PropsType = {
|
||||||
activeCall: ActiveCallType;
|
activeCall: ActiveCallType;
|
||||||
|
@ -24,13 +25,16 @@ function getCurrentPresenter(
|
||||||
if (activeCall.presentingSource) {
|
if (activeCall.presentingSource) {
|
||||||
return { id: ME };
|
return { id: ME };
|
||||||
}
|
}
|
||||||
if (activeCall.callMode === CallMode.Direct) {
|
if (
|
||||||
|
activeCall.callMode === CallMode.Direct &&
|
||||||
|
activeCall.conversation.type === 'direct'
|
||||||
|
) {
|
||||||
const isOtherPersonPresenting = activeCall.remoteParticipants.some(
|
const isOtherPersonPresenting = activeCall.remoteParticipants.some(
|
||||||
participant => participant.presenting
|
participant => participant.presenting
|
||||||
);
|
);
|
||||||
return isOtherPersonPresenting ? activeCall.conversation : undefined;
|
return isOtherPersonPresenting ? activeCall.conversation : undefined;
|
||||||
}
|
}
|
||||||
if (activeCall.callMode === CallMode.Group) {
|
if (isGroupOrAdhocActiveCall(activeCall)) {
|
||||||
return activeCall.remoteParticipants.find(
|
return activeCall.remoteParticipants.find(
|
||||||
participant => participant.presenting
|
participant => participant.presenting
|
||||||
);
|
);
|
||||||
|
|
|
@ -10,12 +10,14 @@ import { ErrorModal } from './ErrorModal';
|
||||||
|
|
||||||
import { setupI18n } from '../util/setupI18n';
|
import { setupI18n } from '../util/setupI18n';
|
||||||
import enMessages from '../../_locales/en/messages.json';
|
import enMessages from '../../_locales/en/messages.json';
|
||||||
|
import { ButtonVariant } from './Button';
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
title: overrideProps.title ?? '',
|
buttonVariant: overrideProps.buttonVariant ?? undefined,
|
||||||
description: overrideProps.description ?? '',
|
description: overrideProps.description ?? '',
|
||||||
|
title: overrideProps.title ?? '',
|
||||||
i18n,
|
i18n,
|
||||||
onClose: action('onClick'),
|
onClose: action('onClick'),
|
||||||
});
|
});
|
||||||
|
@ -30,6 +32,12 @@ export function Normal(): JSX.Element {
|
||||||
return <ErrorModal {...createProps()} />;
|
return <ErrorModal {...createProps()} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function PrimaryButton(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<ErrorModal {...createProps({ buttonVariant: ButtonVariant.Primary })} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function CustomStrings(): JSX.Element {
|
export function CustomStrings(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<ErrorModal
|
<ErrorModal
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { Modal } from './Modal';
|
||||||
import { Button, ButtonVariant } from './Button';
|
import { Button, ButtonVariant } from './Button';
|
||||||
|
|
||||||
export type PropsType = {
|
export type PropsType = {
|
||||||
|
buttonVariant?: ButtonVariant;
|
||||||
description?: string;
|
description?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
|
||||||
|
@ -22,10 +23,14 @@ function focusRef(el: HTMLElement | null) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ErrorModal(props: PropsType): JSX.Element {
|
export function ErrorModal(props: PropsType): JSX.Element {
|
||||||
const { description, i18n, onClose, title } = props;
|
const { buttonVariant, description, i18n, onClose, title } = props;
|
||||||
|
|
||||||
const footer = (
|
const footer = (
|
||||||
<Button onClick={onClose} ref={focusRef} variant={ButtonVariant.Secondary}>
|
<Button
|
||||||
|
onClick={onClose}
|
||||||
|
ref={focusRef}
|
||||||
|
variant={buttonVariant || ButtonVariant.Secondary}
|
||||||
|
>
|
||||||
{i18n('icu:Confirmation--confirm')}
|
{i18n('icu:Confirmation--confirm')}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|
|
@ -136,6 +136,7 @@ export function LinkPreview(): JSX.Element {
|
||||||
contentType: IMAGE_JPEG,
|
contentType: IMAGE_JPEG,
|
||||||
}),
|
}),
|
||||||
isStickerPack: false,
|
isStickerPack: false,
|
||||||
|
isCallLink: false,
|
||||||
title: LONG_TITLE,
|
title: LONG_TITLE,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -452,6 +452,7 @@ function ForwardMessageEditor({
|
||||||
onClose={removeLinkPreview}
|
onClose={removeLinkPreview}
|
||||||
title={linkPreview.title}
|
title={linkPreview.title}
|
||||||
url={linkPreview.url}
|
url={linkPreview.url}
|
||||||
|
isCallLink={linkPreview.isCallLink}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
@ -40,8 +40,11 @@ export type PropsType = {
|
||||||
editHistoryMessages: EditHistoryMessagesType | undefined;
|
editHistoryMessages: EditHistoryMessagesType | undefined;
|
||||||
renderEditHistoryMessagesModal: () => JSX.Element;
|
renderEditHistoryMessagesModal: () => JSX.Element;
|
||||||
// ErrorModal
|
// ErrorModal
|
||||||
errorModalProps: { description?: string; title?: string } | undefined;
|
errorModalProps:
|
||||||
|
| { buttonVariant?: ButtonVariant; description?: string; title?: string }
|
||||||
|
| undefined;
|
||||||
renderErrorModal: (opts: {
|
renderErrorModal: (opts: {
|
||||||
|
buttonVariant?: ButtonVariant;
|
||||||
description?: string;
|
description?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
}) => JSX.Element;
|
}) => JSX.Element;
|
||||||
|
|
|
@ -71,6 +71,7 @@ LinkPreview.args = {
|
||||||
}),
|
}),
|
||||||
title: 'Cats & Kittens LOL',
|
title: 'Cats & Kittens LOL',
|
||||||
url: 'https://www.catsandkittens.lolcats/kittens/page/1',
|
url: 'https://www.catsandkittens.lolcats/kittens/page/1',
|
||||||
|
isCallLink: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -184,6 +184,7 @@ export function LinkPreview(): JSX.Element {
|
||||||
preview: {
|
preview: {
|
||||||
url: 'https://www.signal.org/workworkwork',
|
url: 'https://www.signal.org/workworkwork',
|
||||||
title: 'Signal >> Careers',
|
title: 'Signal >> Careers',
|
||||||
|
isCallLink: false,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -200,6 +201,7 @@ export function LinkPreviewThumbnail(): JSX.Element {
|
||||||
preview: {
|
preview: {
|
||||||
url: 'https://www.signal.org/workworkwork',
|
url: 'https://www.signal.org/workworkwork',
|
||||||
title: 'Signal >> Careers',
|
title: 'Signal >> Careers',
|
||||||
|
isCallLink: false,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -216,6 +218,7 @@ export function LinkPreviewLongTitle(): JSX.Element {
|
||||||
title:
|
title:
|
||||||
'2021 Etihad Airways Abu Dhabi Grand Prix Race Summary - F1 RaceCast Dec 10 to Dec 12 - ESPN',
|
'2021 Etihad Airways Abu Dhabi Grand Prix Race Summary - F1 RaceCast Dec 10 to Dec 12 - ESPN',
|
||||||
url: 'https://www.espn.com/f1/race/_/id/600001776',
|
url: 'https://www.espn.com/f1/race/_/id/600001776',
|
||||||
|
isCallLink: false,
|
||||||
},
|
},
|
||||||
text: 'Spoiler alert!',
|
text: 'Spoiler alert!',
|
||||||
textForegroundColor: 4294704123,
|
textForegroundColor: 4294704123,
|
||||||
|
@ -232,6 +235,7 @@ export function LinkPreviewJustUrl(): JSX.Element {
|
||||||
color: 4294951251,
|
color: 4294951251,
|
||||||
preview: {
|
preview: {
|
||||||
url: 'https://www.rolex.com/en-us/watches/day-date/m228236-0012.html',
|
url: 'https://www.rolex.com/en-us/watches/day-date/m228236-0012.html',
|
||||||
|
isCallLink: false,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -246,6 +250,7 @@ export function LinkPreviewJustUrlText(): JSX.Element {
|
||||||
color: 4294951251,
|
color: 4294951251,
|
||||||
preview: {
|
preview: {
|
||||||
url: 'https://www.rolex.com/en-us/watches/day-date/m228236-0012.html',
|
url: 'https://www.rolex.com/en-us/watches/day-date/m228236-0012.html',
|
||||||
|
isCallLink: false,
|
||||||
},
|
},
|
||||||
text: 'Check this out!',
|
text: 'Check this out!',
|
||||||
}}
|
}}
|
||||||
|
@ -261,6 +266,7 @@ export function LinkPreviewReallyLongDomain(): JSX.Element {
|
||||||
color: 4294951251,
|
color: 4294951251,
|
||||||
preview: {
|
preview: {
|
||||||
url: 'https://llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch.international/',
|
url: 'https://llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch.international/',
|
||||||
|
isCallLink: false,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -284,6 +290,7 @@ export function LinkPreviewWRJ(): JSX.Element {
|
||||||
preview: {
|
preview: {
|
||||||
title: 'Romeo and Juliet: Entire Play',
|
title: 'Romeo and Juliet: Entire Play',
|
||||||
url: 'http://shakespeare.mit.edu/romeo_juliet/full.html',
|
url: 'http://shakespeare.mit.edu/romeo_juliet/full.html',
|
||||||
|
isCallLink: false,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -307,6 +314,7 @@ export function TextBackgroundAndLinkPreview(): JSX.Element {
|
||||||
preview: {
|
preview: {
|
||||||
title: 'A really long title so that the we can test the margins',
|
title: 'A really long title so that the we can test the margins',
|
||||||
url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
|
url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
|
||||||
|
isCallLink: false,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -61,6 +61,8 @@ function getToast(toastType: ToastType): AnyToast {
|
||||||
};
|
};
|
||||||
case ToastType.ConversationUnarchived:
|
case ToastType.ConversationUnarchived:
|
||||||
return { toastType: ToastType.ConversationUnarchived };
|
return { toastType: ToastType.ConversationUnarchived };
|
||||||
|
case ToastType.CopiedCallLink:
|
||||||
|
return { toastType: ToastType.CopiedCallLink };
|
||||||
case ToastType.CopiedUsername:
|
case ToastType.CopiedUsername:
|
||||||
return { toastType: ToastType.CopiedUsername };
|
return { toastType: ToastType.CopiedUsername };
|
||||||
case ToastType.CopiedUsernameLink:
|
case ToastType.CopiedUsernameLink:
|
||||||
|
|
|
@ -188,6 +188,14 @@ export function renderToast({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (toastType === ToastType.CopiedCallLink) {
|
||||||
|
return (
|
||||||
|
<Toast onClose={hideToast} timeout={3 * SECOND}>
|
||||||
|
{i18n('icu:calling__call-link-copied')}
|
||||||
|
</Toast>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (toastType === ToastType.CopiedUsername) {
|
if (toastType === ToastType.CopiedUsername) {
|
||||||
return (
|
return (
|
||||||
<Toast onClose={hideToast} timeout={3 * SECOND}>
|
<Toast onClose={hideToast} timeout={3 * SECOND}>
|
||||||
|
|
|
@ -216,6 +216,9 @@ function renderCallingNotificationButton(
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case CallMode.Adhoc:
|
||||||
|
log.warn('CallingNotification for adhoc call, should never happen');
|
||||||
|
return null;
|
||||||
default:
|
default:
|
||||||
log.error(missingCaseError(props.callHistory.mode));
|
log.error(missingCaseError(props.callHistory.mode));
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -1229,6 +1229,9 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
{first.isCallLink && (
|
||||||
|
<div className="module-message__link-preview__call-link-icon" />
|
||||||
|
)}
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-message__link-preview__text',
|
'module-message__link-preview__text',
|
||||||
|
@ -1931,6 +1934,31 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderAction(): JSX.Element | null {
|
||||||
|
const { direction, i18n, previews } = this.props;
|
||||||
|
if (previews?.length !== 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onlyPreview = previews[0];
|
||||||
|
if (onlyPreview.isCallLink) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={classNames('module-message__action', {
|
||||||
|
'module-message__action--incoming': direction === 'incoming',
|
||||||
|
'module-message__action--outgoing': direction === 'outgoing',
|
||||||
|
})}
|
||||||
|
onClick={() => openLinkInWebBrowser(onlyPreview.url)}
|
||||||
|
>
|
||||||
|
{i18n('icu:calling__join')}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private renderError(): ReactNode {
|
private renderError(): ReactNode {
|
||||||
const { status, direction } = this.props;
|
const { status, direction } = this.props;
|
||||||
|
|
||||||
|
@ -2406,6 +2434,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
{this.renderPayment()}
|
{this.renderPayment()}
|
||||||
{this.renderEmbeddedContact()}
|
{this.renderEmbeddedContact()}
|
||||||
{this.renderText()}
|
{this.renderText()}
|
||||||
|
{this.renderAction()}
|
||||||
{this.renderMetadata()}
|
{this.renderMetadata()}
|
||||||
{this.renderSendMessageButton()}
|
{this.renderSendMessageButton()}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -32,6 +32,7 @@ const getDefaultProps = (): Props => ({
|
||||||
onClose: action('onClose'),
|
onClose: action('onClose'),
|
||||||
title: 'This is a super-sweet site',
|
title: 'This is a super-sweet site',
|
||||||
url: 'https://www.signal.org',
|
url: 'https://www.signal.org',
|
||||||
|
isCallLink: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line react/function-component-definition
|
// eslint-disable-next-line react/function-component-definition
|
||||||
|
|
|
@ -426,6 +426,7 @@ export function EmojiMessages(): JSX.Element {
|
||||||
width: 320,
|
width: 320,
|
||||||
}),
|
}),
|
||||||
isStickerPack: false,
|
isStickerPack: false,
|
||||||
|
isCallLink: false,
|
||||||
title: 'Signal',
|
title: 'Signal',
|
||||||
description:
|
description:
|
||||||
'Say "hello" to a different messaging experience. An unexpected focus on privacy, combined with all of the features you expect.',
|
'Say "hello" to a different messaging experience. An unexpected focus on privacy, combined with all of the features you expect.',
|
||||||
|
@ -894,6 +895,7 @@ LinkPreviewInGroup.args = {
|
||||||
width: 320,
|
width: 320,
|
||||||
}),
|
}),
|
||||||
isStickerPack: false,
|
isStickerPack: false,
|
||||||
|
isCallLink: false,
|
||||||
title: 'Signal',
|
title: 'Signal',
|
||||||
description:
|
description:
|
||||||
'Say "hello" to a different messaging experience. An unexpected focus on privacy, combined with all of the features you expect.',
|
'Say "hello" to a different messaging experience. An unexpected focus on privacy, combined with all of the features you expect.',
|
||||||
|
@ -931,6 +933,7 @@ LinkPreviewWithQuote.args = {
|
||||||
width: 320,
|
width: 320,
|
||||||
}),
|
}),
|
||||||
isStickerPack: false,
|
isStickerPack: false,
|
||||||
|
isCallLink: false,
|
||||||
title: 'Signal',
|
title: 'Signal',
|
||||||
description:
|
description:
|
||||||
'Say "hello" to a different messaging experience. An unexpected focus on privacy, combined with all of the features you expect.',
|
'Say "hello" to a different messaging experience. An unexpected focus on privacy, combined with all of the features you expect.',
|
||||||
|
@ -956,6 +959,7 @@ LinkPreviewWithSmallImage.args = {
|
||||||
width: 50,
|
width: 50,
|
||||||
}),
|
}),
|
||||||
isStickerPack: false,
|
isStickerPack: false,
|
||||||
|
isCallLink: false,
|
||||||
title: 'Signal',
|
title: 'Signal',
|
||||||
description:
|
description:
|
||||||
'Say "hello" to a different messaging experience. An unexpected focus on privacy, combined with all of the features you expect.',
|
'Say "hello" to a different messaging experience. An unexpected focus on privacy, combined with all of the features you expect.',
|
||||||
|
@ -973,6 +977,7 @@ LinkPreviewWithoutImage.args = {
|
||||||
{
|
{
|
||||||
domain: 'signal.org',
|
domain: 'signal.org',
|
||||||
isStickerPack: false,
|
isStickerPack: false,
|
||||||
|
isCallLink: false,
|
||||||
title: 'Signal',
|
title: 'Signal',
|
||||||
description:
|
description:
|
||||||
'Say "hello" to a different messaging experience. An unexpected focus on privacy, combined with all of the features you expect.',
|
'Say "hello" to a different messaging experience. An unexpected focus on privacy, combined with all of the features you expect.',
|
||||||
|
@ -990,6 +995,7 @@ LinkPreviewWithNoDescription.args = {
|
||||||
{
|
{
|
||||||
domain: 'signal.org',
|
domain: 'signal.org',
|
||||||
isStickerPack: false,
|
isStickerPack: false,
|
||||||
|
isCallLink: false,
|
||||||
title: 'Signal',
|
title: 'Signal',
|
||||||
url: 'https://www.signal.org',
|
url: 'https://www.signal.org',
|
||||||
date: Date.now(),
|
date: Date.now(),
|
||||||
|
@ -1005,6 +1011,7 @@ LinkPreviewWithLongDescription.args = {
|
||||||
{
|
{
|
||||||
domain: 'signal.org',
|
domain: 'signal.org',
|
||||||
isStickerPack: false,
|
isStickerPack: false,
|
||||||
|
isCallLink: false,
|
||||||
title: 'Signal',
|
title: 'Signal',
|
||||||
description: Array(10)
|
description: Array(10)
|
||||||
.fill(
|
.fill(
|
||||||
|
@ -1032,6 +1039,7 @@ LinkPreviewWithSmallImageLongDescription.args = {
|
||||||
width: 50,
|
width: 50,
|
||||||
}),
|
}),
|
||||||
isStickerPack: false,
|
isStickerPack: false,
|
||||||
|
isCallLink: false,
|
||||||
title: 'Signal',
|
title: 'Signal',
|
||||||
description: Array(10)
|
description: Array(10)
|
||||||
.fill(
|
.fill(
|
||||||
|
@ -1059,6 +1067,7 @@ LinkPreviewWithNoDate.args = {
|
||||||
width: 320,
|
width: 320,
|
||||||
}),
|
}),
|
||||||
isStickerPack: false,
|
isStickerPack: false,
|
||||||
|
isCallLink: false,
|
||||||
title: 'Signal',
|
title: 'Signal',
|
||||||
description:
|
description:
|
||||||
'Say "hello" to a different messaging experience. An unexpected focus on privacy, combined with all of the features you expect.',
|
'Say "hello" to a different messaging experience. An unexpected focus on privacy, combined with all of the features you expect.',
|
||||||
|
@ -1082,6 +1091,7 @@ LinkPreviewWithTooNewADate.args = {
|
||||||
width: 320,
|
width: 320,
|
||||||
}),
|
}),
|
||||||
isStickerPack: false,
|
isStickerPack: false,
|
||||||
|
isCallLink: false,
|
||||||
title: 'Signal',
|
title: 'Signal',
|
||||||
description:
|
description:
|
||||||
'Say "hello" to a different messaging experience. An unexpected focus on privacy, combined with all of the features you expect.',
|
'Say "hello" to a different messaging experience. An unexpected focus on privacy, combined with all of the features you expect.',
|
||||||
|
@ -1093,6 +1103,23 @@ LinkPreviewWithTooNewADate.args = {
|
||||||
text: 'Be sure to look at https://www.signal.org',
|
text: 'Be sure to look at https://www.signal.org',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const LinkPreviewWithCallLink = Template.bind({});
|
||||||
|
LinkPreviewWithCallLink.args = {
|
||||||
|
previews: [
|
||||||
|
{
|
||||||
|
url: 'https://signal.link/call/#key=hzcn-pcff-ctsc-bdbf-stcr-tzpc-bhqx-kghh',
|
||||||
|
title: 'Camping Prep',
|
||||||
|
description: 'Use this link to join a Signal call',
|
||||||
|
image: undefined,
|
||||||
|
date: undefined,
|
||||||
|
isCallLink: true,
|
||||||
|
isStickerPack: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
status: 'sent',
|
||||||
|
text: 'Use this link to join a Signal call: https://signal.link/call/#key=hzcn-pcff-ctsc-bdbf-stcr-tzpc-bhqx-kghh',
|
||||||
|
};
|
||||||
|
|
||||||
export function Image(): JSX.Element {
|
export function Image(): JSX.Element {
|
||||||
const darkImageProps = createProps({
|
const darkImageProps = createProps({
|
||||||
attachments: [
|
attachments: [
|
||||||
|
@ -1672,6 +1699,7 @@ NotApprovedWithLinkPreview.args = {
|
||||||
width: 320,
|
width: 320,
|
||||||
}),
|
}),
|
||||||
isStickerPack: false,
|
isStickerPack: false,
|
||||||
|
isCallLink: false,
|
||||||
title: 'Signal',
|
title: 'Signal',
|
||||||
description:
|
description:
|
||||||
'Say "hello" to a different messaging experience. An unexpected focus on privacy, combined with all of the features you expect.',
|
'Say "hello" to a different messaging experience. An unexpected focus on privacy, combined with all of the features you expect.',
|
||||||
|
|
|
@ -15,7 +15,7 @@ import { v4 as getGuid } from 'uuid';
|
||||||
import LRU from 'lru-cache';
|
import LRU from 'lru-cache';
|
||||||
import * as log from './logging/log';
|
import * as log from './logging/log';
|
||||||
import {
|
import {
|
||||||
getCheckedCredentialsForToday,
|
getCheckedGroupCredentialsForToday,
|
||||||
maybeFetchNewCredentials,
|
maybeFetchNewCredentials,
|
||||||
} from './services/groupCredentialFetcher';
|
} from './services/groupCredentialFetcher';
|
||||||
import { storageServiceUploadJob } from './services/storage';
|
import { storageServiceUploadJob } from './services/storage';
|
||||||
|
@ -1687,7 +1687,7 @@ async function makeRequestWithTemporalRetry<T>({
|
||||||
secretParams: string;
|
secretParams: string;
|
||||||
request: (sender: MessageSender, options: GroupCredentialsType) => Promise<T>;
|
request: (sender: MessageSender, options: GroupCredentialsType) => Promise<T>;
|
||||||
}): Promise<T> {
|
}): Promise<T> {
|
||||||
const groupCredentials = getCheckedCredentialsForToday(
|
const groupCredentials = getCheckedGroupCredentialsForToday(
|
||||||
`makeRequestWithTemporalRetry/${logId}`
|
`makeRequestWithTemporalRetry/${logId}`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
import { debounce, omit } from 'lodash';
|
import { debounce, omit } from 'lodash';
|
||||||
|
|
||||||
|
import { CallLinkRootKey } from '@signalapp/ringrtc';
|
||||||
import type { LinkPreviewWithHydratedData } from '../types/message/LinkPreviews';
|
import type { LinkPreviewWithHydratedData } from '../types/message/LinkPreviews';
|
||||||
import type {
|
import type {
|
||||||
LinkPreviewImage,
|
LinkPreviewImage,
|
||||||
|
@ -28,6 +29,8 @@ import { imageToBlurHash } from '../util/imageToBlurHash';
|
||||||
import { maybeParseUrl } from '../util/url';
|
import { maybeParseUrl } from '../util/url';
|
||||||
import { sniffImageMimeType } from '../util/sniffImageMimeType';
|
import { sniffImageMimeType } from '../util/sniffImageMimeType';
|
||||||
import { drop } from '../util/drop';
|
import { drop } from '../util/drop';
|
||||||
|
import { linkCallRoute } from '../util/signalRoutes';
|
||||||
|
import { calling } from './calling';
|
||||||
|
|
||||||
const LINK_PREVIEW_TIMEOUT = 60 * SECOND;
|
const LINK_PREVIEW_TIMEOUT = 60 * SECOND;
|
||||||
|
|
||||||
|
@ -164,6 +167,7 @@ export async function addLinkPreview(
|
||||||
window.reduxActions.linkPreviews.addLinkPreview(
|
window.reduxActions.linkPreviews.addLinkPreview(
|
||||||
{
|
{
|
||||||
url,
|
url,
|
||||||
|
isCallLink: false,
|
||||||
},
|
},
|
||||||
source,
|
source,
|
||||||
conversationId
|
conversationId
|
||||||
|
@ -220,6 +224,7 @@ export async function addLinkPreview(
|
||||||
date: dropNull(result.date),
|
date: dropNull(result.date),
|
||||||
domain: LinkPreview.getDomain(result.url),
|
domain: LinkPreview.getDomain(result.url),
|
||||||
isStickerPack: LinkPreview.isStickerPack(result.url),
|
isStickerPack: LinkPreview.isStickerPack(result.url),
|
||||||
|
isCallLink: LinkPreview.isCallLink(result.url),
|
||||||
},
|
},
|
||||||
source,
|
source,
|
||||||
conversationId
|
conversationId
|
||||||
|
@ -274,6 +279,7 @@ export function sanitizeLinkPreview(
|
||||||
date: dropNull(item.date),
|
date: dropNull(item.date),
|
||||||
domain: LinkPreview.getDomain(item.url),
|
domain: LinkPreview.getDomain(item.url),
|
||||||
isStickerPack: LinkPreview.isStickerPack(item.url),
|
isStickerPack: LinkPreview.isStickerPack(item.url),
|
||||||
|
isCallLink: LinkPreview.isCallLink(item.url),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -284,6 +290,7 @@ export function sanitizeLinkPreview(
|
||||||
date: dropNull(item.date),
|
date: dropNull(item.date),
|
||||||
domain: LinkPreview.getDomain(item.url),
|
domain: LinkPreview.getDomain(item.url),
|
||||||
isStickerPack: LinkPreview.isStickerPack(item.url),
|
isStickerPack: LinkPreview.isStickerPack(item.url),
|
||||||
|
isCallLink: LinkPreview.isCallLink(item.url),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -303,6 +310,9 @@ async function getPreview(
|
||||||
if (LinkPreview.isGroupLink(url)) {
|
if (LinkPreview.isGroupLink(url)) {
|
||||||
return getGroupPreview(url, abortSignal);
|
return getGroupPreview(url, abortSignal);
|
||||||
}
|
}
|
||||||
|
if (LinkPreview.isCallLink(url)) {
|
||||||
|
return getCallLinkPreview(url, abortSignal);
|
||||||
|
}
|
||||||
|
|
||||||
// This is already checked elsewhere, but we want to be extra-careful.
|
// This is already checked elsewhere, but we want to be extra-careful.
|
||||||
if (!LinkPreview.shouldPreviewHref(url)) {
|
if (!LinkPreview.shouldPreviewHref(url)) {
|
||||||
|
@ -563,3 +573,30 @@ async function getGroupPreview(
|
||||||
url,
|
url,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getCallLinkPreview(
|
||||||
|
url: string,
|
||||||
|
_abortSignal: Readonly<AbortSignal>
|
||||||
|
): Promise<null | LinkPreviewResult> {
|
||||||
|
const parsedUrl = linkCallRoute.fromUrl(url);
|
||||||
|
if (parsedUrl == null) {
|
||||||
|
throw new Error('Failed to parse call link URL');
|
||||||
|
}
|
||||||
|
|
||||||
|
const callLinkRootKey = CallLinkRootKey.parse(parsedUrl.args.key);
|
||||||
|
const callLinkState = await calling.readCallLink({ callLinkRootKey });
|
||||||
|
if (!callLinkState) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
url,
|
||||||
|
title:
|
||||||
|
callLinkState.name === ''
|
||||||
|
? window.i18n('icu:calling__call-link-default-title')
|
||||||
|
: callLinkState.name,
|
||||||
|
description: window.i18n('icu:message--call-link-description'),
|
||||||
|
image: undefined,
|
||||||
|
date: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -6,7 +6,9 @@ import { ipcRenderer } from 'electron';
|
||||||
import type {
|
import type {
|
||||||
AudioDevice,
|
AudioDevice,
|
||||||
CallId,
|
CallId,
|
||||||
|
CallLinkState,
|
||||||
DeviceId,
|
DeviceId,
|
||||||
|
GroupCallObserver,
|
||||||
PeekInfo,
|
PeekInfo,
|
||||||
UserId,
|
UserId,
|
||||||
VideoFrameSource,
|
VideoFrameSource,
|
||||||
|
@ -18,6 +20,7 @@ import {
|
||||||
Call,
|
Call,
|
||||||
CallingMessage,
|
CallingMessage,
|
||||||
CallMessageUrgency,
|
CallMessageUrgency,
|
||||||
|
CallLinkRootKey,
|
||||||
CallLogLevel,
|
CallLogLevel,
|
||||||
CallState,
|
CallState,
|
||||||
CanvasVideoRenderer,
|
CanvasVideoRenderer,
|
||||||
|
@ -40,8 +43,10 @@ import {
|
||||||
import { uniqBy, noop } from 'lodash';
|
import { uniqBy, noop } from 'lodash';
|
||||||
|
|
||||||
import Long from 'long';
|
import Long from 'long';
|
||||||
|
import type { CallLinkAuthCredentialPresentation } from '@signalapp/libsignal-client/zkgroup';
|
||||||
import type {
|
import type {
|
||||||
ActionsType as CallingReduxActionsType,
|
ActionsType as CallingReduxActionsType,
|
||||||
|
CallLinkStateType,
|
||||||
GroupCallParticipantInfoType,
|
GroupCallParticipantInfoType,
|
||||||
GroupCallPeekInfoType,
|
GroupCallPeekInfoType,
|
||||||
} from '../state/ducks/calling';
|
} from '../state/ducks/calling';
|
||||||
|
@ -123,6 +128,11 @@ import {
|
||||||
import { isNormalNumber } from '../util/isNormalNumber';
|
import { isNormalNumber } from '../util/isNormalNumber';
|
||||||
import { LocalCallEvent } from '../types/CallDisposition';
|
import { LocalCallEvent } from '../types/CallDisposition';
|
||||||
import { isInSystemContacts } from '../util/isInSystemContacts';
|
import { isInSystemContacts } from '../util/isInSystemContacts';
|
||||||
|
import {
|
||||||
|
getRoomIdFromRootKey,
|
||||||
|
getCallLinkAuthCredentialPresentation,
|
||||||
|
} from '../util/callLinks';
|
||||||
|
import { isAdhocCallingEnabled } from '../util/isAdhocCallingEnabled';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
processGroupCallRingCancellation,
|
processGroupCallRingCancellation,
|
||||||
|
@ -157,6 +167,7 @@ type CallingReduxInterface = Pick<
|
||||||
| 'callStateChange'
|
| 'callStateChange'
|
||||||
| 'cancelIncomingGroupCallRing'
|
| 'cancelIncomingGroupCallRing'
|
||||||
| 'groupCallAudioLevelsChange'
|
| 'groupCallAudioLevelsChange'
|
||||||
|
| 'groupCallEnded'
|
||||||
| 'groupCallRaisedHandsChange'
|
| 'groupCallRaisedHandsChange'
|
||||||
| 'groupCallStateChange'
|
| 'groupCallStateChange'
|
||||||
| 'outgoingCall'
|
| 'outgoingCall'
|
||||||
|
@ -168,6 +179,7 @@ type CallingReduxInterface = Pick<
|
||||||
| 'remoteVideoChange'
|
| 'remoteVideoChange'
|
||||||
| 'setPresenting'
|
| 'setPresenting'
|
||||||
| 'startCallingLobby'
|
| 'startCallingLobby'
|
||||||
|
| 'startCallLinkLobby'
|
||||||
| 'peekNotConnectedGroupCall'
|
| 'peekNotConnectedGroupCall'
|
||||||
> & {
|
> & {
|
||||||
areAnyCallsActiveOrRinging(): boolean;
|
areAnyCallsActiveOrRinging(): boolean;
|
||||||
|
@ -302,7 +314,7 @@ export class CallingClass {
|
||||||
|
|
||||||
private deviceReselectionTimer?: NodeJS.Timeout;
|
private deviceReselectionTimer?: NodeJS.Timeout;
|
||||||
|
|
||||||
private callsByConversation: { [conversationId: string]: Call | GroupCall };
|
private callsLookup: { [key: string]: Call | GroupCall };
|
||||||
|
|
||||||
private hadLocalVideoBeforePresenting?: boolean;
|
private hadLocalVideoBeforePresenting?: boolean;
|
||||||
|
|
||||||
|
@ -314,7 +326,7 @@ export class CallingClass {
|
||||||
});
|
});
|
||||||
this.videoRenderer = new CanvasVideoRenderer();
|
this.videoRenderer = new CanvasVideoRenderer();
|
||||||
|
|
||||||
this.callsByConversation = {};
|
this.callsLookup = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
initialize(reduxInterface: CallingReduxInterface, sfuUrl: string): void {
|
initialize(reduxInterface: CallingReduxInterface, sfuUrl: string): void {
|
||||||
|
@ -412,6 +424,11 @@ export class CallingClass {
|
||||||
}
|
}
|
||||||
case CallMode.Group:
|
case CallMode.Group:
|
||||||
break;
|
break;
|
||||||
|
case CallMode.Adhoc:
|
||||||
|
log.error(
|
||||||
|
'startCallingLobby() not implemented for adhoc calls. Did you mean: startCallLinkLobby()?'
|
||||||
|
);
|
||||||
|
return;
|
||||||
default:
|
default:
|
||||||
throw missingCaseError(callMode);
|
throw missingCaseError(callMode);
|
||||||
}
|
}
|
||||||
|
@ -511,6 +528,80 @@ export class CallingClass {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async readCallLink({
|
||||||
|
callLinkRootKey,
|
||||||
|
}: Readonly<{
|
||||||
|
callLinkRootKey: CallLinkRootKey;
|
||||||
|
}>): Promise<CallLinkState | undefined> {
|
||||||
|
if (!this._sfuUrl) {
|
||||||
|
throw new Error('Missing SFU URL; not handling call link');
|
||||||
|
}
|
||||||
|
|
||||||
|
const roomId = getRoomIdFromRootKey(callLinkRootKey);
|
||||||
|
const authCredentialPresentation =
|
||||||
|
await getCallLinkAuthCredentialPresentation(callLinkRootKey);
|
||||||
|
|
||||||
|
log.info(`readCallLink: roomId ${roomId}`);
|
||||||
|
const result = await RingRTC.readCallLink(
|
||||||
|
this._sfuUrl,
|
||||||
|
authCredentialPresentation.serialize(),
|
||||||
|
callLinkRootKey
|
||||||
|
);
|
||||||
|
if (!result.success) {
|
||||||
|
log.warn(`readCallLink: failed ${roomId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info('readCallLink: success', result);
|
||||||
|
return result.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async startCallLinkLobby({
|
||||||
|
callLinkRootKey,
|
||||||
|
hasLocalAudio,
|
||||||
|
hasLocalVideo = true,
|
||||||
|
}: Readonly<{
|
||||||
|
callLinkRootKey: CallLinkRootKey;
|
||||||
|
hasLocalAudio: boolean;
|
||||||
|
hasLocalVideo?: boolean;
|
||||||
|
}>): Promise<
|
||||||
|
| undefined
|
||||||
|
| {
|
||||||
|
callMode: CallMode.Adhoc;
|
||||||
|
connectionState: GroupCallConnectionState;
|
||||||
|
hasLocalAudio: boolean;
|
||||||
|
hasLocalVideo: boolean;
|
||||||
|
joinState: GroupCallJoinState;
|
||||||
|
peekInfo?: GroupCallPeekInfoType;
|
||||||
|
remoteParticipants: Array<GroupCallParticipantInfoType>;
|
||||||
|
}
|
||||||
|
> {
|
||||||
|
const roomId = getRoomIdFromRootKey(callLinkRootKey);
|
||||||
|
log.info('startCallLinkLobby() for roomId', roomId);
|
||||||
|
|
||||||
|
await this.startDeviceReselectionTimer();
|
||||||
|
|
||||||
|
const authCredentialPresentation =
|
||||||
|
await getCallLinkAuthCredentialPresentation(callLinkRootKey);
|
||||||
|
|
||||||
|
const groupCall = this.connectCallLinkCall({
|
||||||
|
roomId,
|
||||||
|
authCredentialPresentation,
|
||||||
|
callLinkRootKey,
|
||||||
|
adminPasskey: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
groupCall.setOutgoingAudioMuted(!hasLocalAudio);
|
||||||
|
groupCall.setOutgoingVideoMuted(!hasLocalVideo);
|
||||||
|
|
||||||
|
this.enableLocalCamera();
|
||||||
|
|
||||||
|
return {
|
||||||
|
callMode: CallMode.Adhoc,
|
||||||
|
...this.formatGroupCallForRedux(groupCall),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async startOutgoingDirectCall(
|
async startOutgoingDirectCall(
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
hasLocalAudio: boolean,
|
hasLocalAudio: boolean,
|
||||||
|
@ -575,12 +666,12 @@ export class CallingClass {
|
||||||
}
|
}
|
||||||
|
|
||||||
private getDirectCall(conversationId: string): undefined | Call {
|
private getDirectCall(conversationId: string): undefined | Call {
|
||||||
const call = getOwn(this.callsByConversation, conversationId);
|
const call = getOwn(this.callsLookup, conversationId);
|
||||||
return call instanceof Call ? call : undefined;
|
return call instanceof Call ? call : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getGroupCall(conversationId: string): undefined | GroupCall {
|
private getGroupCall(conversationId: string): undefined | GroupCall {
|
||||||
const call = getOwn(this.callsByConversation, conversationId);
|
const call = getOwn(this.callsLookup, conversationId);
|
||||||
return call instanceof GroupCall ? call : undefined;
|
return call instanceof GroupCall ? call : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -659,6 +750,45 @@ export class CallingClass {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async peekCallLinkCall(
|
||||||
|
roomId: string,
|
||||||
|
rootKey: string | undefined
|
||||||
|
): Promise<PeekInfo> {
|
||||||
|
log.info(`peekCallLinkCall: For roomId ${roomId}`);
|
||||||
|
const statefulPeekInfo = this.getGroupCall(roomId)?.getPeekInfo();
|
||||||
|
if (statefulPeekInfo) {
|
||||||
|
return statefulPeekInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rootKey) {
|
||||||
|
throw new Error(
|
||||||
|
'Missing call link root key, cannot do stateless peeking'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._sfuUrl) {
|
||||||
|
throw new Error('Missing SFU URL; not peeking call link call');
|
||||||
|
}
|
||||||
|
|
||||||
|
const callLinkRootKey = CallLinkRootKey.parse(rootKey);
|
||||||
|
|
||||||
|
const authCredentialPresentation =
|
||||||
|
await getCallLinkAuthCredentialPresentation(callLinkRootKey);
|
||||||
|
|
||||||
|
const result = await RingRTC.peekCallLinkCall(
|
||||||
|
this._sfuUrl,
|
||||||
|
authCredentialPresentation.serialize(),
|
||||||
|
callLinkRootKey
|
||||||
|
);
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to peek call link, error ${result.errorStatusCode}, roomId ${roomId}.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.value;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connect to a conversation's group call and connect it to Redux.
|
* Connect to a conversation's group call and connect it to Redux.
|
||||||
*
|
*
|
||||||
|
@ -695,7 +825,6 @@ export class CallingClass {
|
||||||
|
|
||||||
const groupIdBuffer = Buffer.from(Bytes.fromBase64(groupId));
|
const groupIdBuffer = Buffer.from(Bytes.fromBase64(groupId));
|
||||||
|
|
||||||
let updateMessageState = GroupCallUpdateMessageState.SentNothing;
|
|
||||||
let isRequestingMembershipProof = false;
|
let isRequestingMembershipProof = false;
|
||||||
|
|
||||||
const outerGroupCall = RingRTC.getGroupCall(
|
const outerGroupCall = RingRTC.getGroupCall(
|
||||||
|
@ -704,174 +833,7 @@ export class CallingClass {
|
||||||
Buffer.alloc(0),
|
Buffer.alloc(0),
|
||||||
AUDIO_LEVEL_INTERVAL_MS,
|
AUDIO_LEVEL_INTERVAL_MS,
|
||||||
{
|
{
|
||||||
onLocalDeviceStateChanged: groupCall => {
|
...this.getGroupCallObserver(conversationId, CallMode.Group),
|
||||||
const localDeviceState = groupCall.getLocalDeviceState();
|
|
||||||
const peekInfo = groupCall.getPeekInfo() ?? null;
|
|
||||||
|
|
||||||
log.info(
|
|
||||||
'GroupCall#onLocalDeviceStateChanged',
|
|
||||||
formatLocalDeviceState(localDeviceState),
|
|
||||||
peekInfo != null ? formatPeekInfo(peekInfo) : '(No PeekInfo)'
|
|
||||||
);
|
|
||||||
|
|
||||||
const groupCallMeta = getGroupCallMeta(peekInfo);
|
|
||||||
|
|
||||||
if (groupCallMeta != null) {
|
|
||||||
try {
|
|
||||||
const localCallEvent = getLocalCallEventFromJoinState(
|
|
||||||
convertJoinState(localDeviceState.joinState),
|
|
||||||
groupCallMeta
|
|
||||||
);
|
|
||||||
|
|
||||||
if (localCallEvent != null && peekInfo != null) {
|
|
||||||
const conversation =
|
|
||||||
window.ConversationController.get(conversationId);
|
|
||||||
strictAssert(
|
|
||||||
conversation != null,
|
|
||||||
'GroupCall#onLocalDeviceStateChanged: Missing conversation'
|
|
||||||
);
|
|
||||||
const peerId = getPeerIdFromConversation(
|
|
||||||
conversation.attributes
|
|
||||||
);
|
|
||||||
|
|
||||||
const callDetails = getCallDetailsFromGroupCallMeta(
|
|
||||||
peerId,
|
|
||||||
groupCallMeta
|
|
||||||
);
|
|
||||||
const callEvent = getCallEventDetails(
|
|
||||||
callDetails,
|
|
||||||
localCallEvent,
|
|
||||||
'RingRTC.onLocalDeviceStateChanged'
|
|
||||||
);
|
|
||||||
drop(updateCallHistoryFromLocalEvent(callEvent, null));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
log.error(
|
|
||||||
'GroupCall#onLocalDeviceStateChanged: Error updating state',
|
|
||||||
Errors.toLogFormat(error)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
localDeviceState.connectionState === ConnectionState.NotConnected
|
|
||||||
) {
|
|
||||||
// NOTE: This assumes that only one call is active at a time. For example, if
|
|
||||||
// there are two calls using the camera, this will disable both of them.
|
|
||||||
// That's fine for now, but this will break if that assumption changes.
|
|
||||||
this.disableLocalVideo();
|
|
||||||
|
|
||||||
delete this.callsByConversation[conversationId];
|
|
||||||
|
|
||||||
if (
|
|
||||||
updateMessageState === GroupCallUpdateMessageState.SentJoin &&
|
|
||||||
peekInfo?.eraId != null
|
|
||||||
) {
|
|
||||||
updateMessageState = GroupCallUpdateMessageState.SentLeft;
|
|
||||||
void this.sendGroupCallUpdateMessage(
|
|
||||||
conversationId,
|
|
||||||
peekInfo?.eraId
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.callsByConversation[conversationId] = groupCall;
|
|
||||||
|
|
||||||
// NOTE: This assumes only one active call at a time. See comment above.
|
|
||||||
if (localDeviceState.videoMuted) {
|
|
||||||
this.disableLocalVideo();
|
|
||||||
} else {
|
|
||||||
this.videoCapturer.enableCaptureAndSend(groupCall);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
updateMessageState === GroupCallUpdateMessageState.SentNothing &&
|
|
||||||
localDeviceState.joinState === JoinState.Joined &&
|
|
||||||
peekInfo?.eraId != null
|
|
||||||
) {
|
|
||||||
updateMessageState = GroupCallUpdateMessageState.SentJoin;
|
|
||||||
void this.sendGroupCallUpdateMessage(
|
|
||||||
conversationId,
|
|
||||||
peekInfo?.eraId
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.syncGroupCallToRedux(conversationId, groupCall);
|
|
||||||
},
|
|
||||||
onRemoteDeviceStatesChanged: groupCall => {
|
|
||||||
const localDeviceState = groupCall.getLocalDeviceState();
|
|
||||||
const peekInfo = groupCall.getPeekInfo();
|
|
||||||
|
|
||||||
log.info(
|
|
||||||
'GroupCall#onRemoteDeviceStatesChanged',
|
|
||||||
formatLocalDeviceState(localDeviceState),
|
|
||||||
peekInfo ? formatPeekInfo(peekInfo) : '(No PeekInfo)'
|
|
||||||
);
|
|
||||||
|
|
||||||
this.syncGroupCallToRedux(conversationId, groupCall);
|
|
||||||
},
|
|
||||||
onAudioLevels: groupCall => {
|
|
||||||
const remoteDeviceStates = groupCall.getRemoteDeviceStates();
|
|
||||||
if (!remoteDeviceStates) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const localAudioLevel = groupCall.getLocalDeviceState().audioLevel;
|
|
||||||
|
|
||||||
this.reduxInterface?.groupCallAudioLevelsChange({
|
|
||||||
conversationId,
|
|
||||||
localAudioLevel,
|
|
||||||
remoteDeviceStates,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onLowBandwidthForVideo: (_groupCall, _recovered) => {
|
|
||||||
// TODO: Implement handling of "low outgoing bandwidth for video" notification.
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param reactions A list of reactions received by the client ordered
|
|
||||||
* from oldest to newest.
|
|
||||||
*/
|
|
||||||
onReactions: (_groupCall, reactions) => {
|
|
||||||
this.reduxInterface?.receiveGroupCallReactions({
|
|
||||||
conversationId,
|
|
||||||
reactions,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onRaisedHands: (_groupCall, raisedHands) => {
|
|
||||||
this.reduxInterface?.groupCallRaisedHandsChange({
|
|
||||||
conversationId,
|
|
||||||
raisedHands,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onPeekChanged: groupCall => {
|
|
||||||
const localDeviceState = groupCall.getLocalDeviceState();
|
|
||||||
const peekInfo = groupCall.getPeekInfo() ?? null;
|
|
||||||
|
|
||||||
log.info(
|
|
||||||
'GroupCall#onPeekChanged',
|
|
||||||
formatLocalDeviceState(localDeviceState),
|
|
||||||
peekInfo ? formatPeekInfo(peekInfo) : '(No PeekInfo)'
|
|
||||||
);
|
|
||||||
|
|
||||||
const { eraId } = peekInfo ?? {};
|
|
||||||
|
|
||||||
if (
|
|
||||||
updateMessageState === GroupCallUpdateMessageState.SentNothing &&
|
|
||||||
localDeviceState.connectionState !== ConnectionState.NotConnected &&
|
|
||||||
localDeviceState.joinState === JoinState.Joined &&
|
|
||||||
eraId
|
|
||||||
) {
|
|
||||||
updateMessageState = GroupCallUpdateMessageState.SentJoin;
|
|
||||||
void this.sendGroupCallUpdateMessage(conversationId, eraId);
|
|
||||||
}
|
|
||||||
|
|
||||||
void this.updateCallHistoryForGroupCall(
|
|
||||||
conversationId,
|
|
||||||
convertJoinState(localDeviceState.joinState),
|
|
||||||
peekInfo
|
|
||||||
);
|
|
||||||
this.syncGroupCallToRedux(conversationId, groupCall);
|
|
||||||
},
|
|
||||||
async requestMembershipProof(groupCall) {
|
async requestMembershipProof(groupCall) {
|
||||||
if (isRequestingMembershipProof) {
|
if (isRequestingMembershipProof) {
|
||||||
return;
|
return;
|
||||||
|
@ -893,20 +855,6 @@ export class CallingClass {
|
||||||
isRequestingMembershipProof = false;
|
isRequestingMembershipProof = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
requestGroupMembers: groupCall => {
|
|
||||||
groupCall.setGroupMembers(this.getGroupCallMembers(conversationId));
|
|
||||||
},
|
|
||||||
onEnded: (groupCall, endedReason) => {
|
|
||||||
const localDeviceState = groupCall.getLocalDeviceState();
|
|
||||||
const peekInfo = groupCall.getPeekInfo();
|
|
||||||
|
|
||||||
log.info(
|
|
||||||
'GroupCall#onEnded',
|
|
||||||
endedReason,
|
|
||||||
formatLocalDeviceState(localDeviceState),
|
|
||||||
peekInfo ? formatPeekInfo(peekInfo) : '(No PeekInfo)'
|
|
||||||
);
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -918,7 +866,62 @@ export class CallingClass {
|
||||||
|
|
||||||
outerGroupCall.connect();
|
outerGroupCall.connect();
|
||||||
|
|
||||||
this.syncGroupCallToRedux(conversationId, outerGroupCall);
|
this.syncGroupCallToRedux(conversationId, outerGroupCall, CallMode.Group);
|
||||||
|
|
||||||
|
return outerGroupCall;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectCallLinkCall({
|
||||||
|
roomId,
|
||||||
|
authCredentialPresentation,
|
||||||
|
callLinkRootKey,
|
||||||
|
adminPasskey,
|
||||||
|
}: {
|
||||||
|
roomId: string;
|
||||||
|
authCredentialPresentation: CallLinkAuthCredentialPresentation;
|
||||||
|
callLinkRootKey: CallLinkRootKey;
|
||||||
|
adminPasskey: Buffer | undefined;
|
||||||
|
}): GroupCall {
|
||||||
|
if (!isAdhocCallingEnabled()) {
|
||||||
|
throw new Error(
|
||||||
|
'Adhoc calling is not enabled; not connecting call link call'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = this.getGroupCall(roomId);
|
||||||
|
if (existing) {
|
||||||
|
const isExistingCallNotConnected =
|
||||||
|
existing.getLocalDeviceState().connectionState ===
|
||||||
|
ConnectionState.NotConnected;
|
||||||
|
if (isExistingCallNotConnected) {
|
||||||
|
existing.connect();
|
||||||
|
}
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._sfuUrl) {
|
||||||
|
throw new Error('Missing SFU URL; not connecting group call link call');
|
||||||
|
}
|
||||||
|
|
||||||
|
const outerGroupCall = RingRTC.getCallLinkCall(
|
||||||
|
this._sfuUrl,
|
||||||
|
authCredentialPresentation.serialize(),
|
||||||
|
callLinkRootKey,
|
||||||
|
adminPasskey,
|
||||||
|
Buffer.alloc(0),
|
||||||
|
AUDIO_LEVEL_INTERVAL_MS,
|
||||||
|
this.getGroupCallObserver(roomId, CallMode.Adhoc)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!outerGroupCall) {
|
||||||
|
// This should be very rare, likely due to RingRTC not being able to get a lock
|
||||||
|
// or memory or something like that.
|
||||||
|
throw new Error('Failed to get a group call instance; cannot start call');
|
||||||
|
}
|
||||||
|
|
||||||
|
outerGroupCall.connect();
|
||||||
|
|
||||||
|
this.syncGroupCallToRedux(roomId, outerGroupCall, CallMode.Adhoc);
|
||||||
|
|
||||||
return outerGroupCall;
|
return outerGroupCall;
|
||||||
}
|
}
|
||||||
|
@ -971,6 +974,250 @@ export class CallingClass {
|
||||||
groupCall.join();
|
groupCall.join();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getGroupCallObserver(
|
||||||
|
conversationId: string,
|
||||||
|
callMode: CallMode.Group | CallMode.Adhoc
|
||||||
|
): GroupCallObserver {
|
||||||
|
let updateMessageState = GroupCallUpdateMessageState.SentNothing;
|
||||||
|
|
||||||
|
return {
|
||||||
|
onLocalDeviceStateChanged: groupCall => {
|
||||||
|
const localDeviceState = groupCall.getLocalDeviceState();
|
||||||
|
const peekInfo = groupCall.getPeekInfo() ?? null;
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
'GroupCall#onLocalDeviceStateChanged',
|
||||||
|
formatLocalDeviceState(localDeviceState),
|
||||||
|
peekInfo != null ? formatPeekInfo(peekInfo) : '(No PeekInfo)'
|
||||||
|
);
|
||||||
|
|
||||||
|
const groupCallMeta = getGroupCallMeta(peekInfo);
|
||||||
|
|
||||||
|
// TODO: Handle call history for adhoc calls
|
||||||
|
if (groupCallMeta != null && callMode === CallMode.Group) {
|
||||||
|
try {
|
||||||
|
const localCallEvent = getLocalCallEventFromJoinState(
|
||||||
|
convertJoinState(localDeviceState.joinState),
|
||||||
|
groupCallMeta
|
||||||
|
);
|
||||||
|
|
||||||
|
if (localCallEvent != null && peekInfo != null) {
|
||||||
|
const conversation =
|
||||||
|
window.ConversationController.get(conversationId);
|
||||||
|
strictAssert(
|
||||||
|
conversation != null,
|
||||||
|
'GroupCall#onLocalDeviceStateChanged: Missing conversation'
|
||||||
|
);
|
||||||
|
const peerId = getPeerIdFromConversation(conversation.attributes);
|
||||||
|
|
||||||
|
const callDetails = getCallDetailsFromGroupCallMeta(
|
||||||
|
peerId,
|
||||||
|
groupCallMeta
|
||||||
|
);
|
||||||
|
const callEvent = getCallEventDetails(
|
||||||
|
callDetails,
|
||||||
|
localCallEvent,
|
||||||
|
'RingRTC.onLocalDeviceStateChanged'
|
||||||
|
);
|
||||||
|
drop(updateCallHistoryFromLocalEvent(callEvent, null));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log.error(
|
||||||
|
'GroupCall#onLocalDeviceStateChanged: Error updating state',
|
||||||
|
Errors.toLogFormat(error)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localDeviceState.connectionState === ConnectionState.NotConnected) {
|
||||||
|
// NOTE: This assumes that only one call is active at a time. For example, if
|
||||||
|
// there are two calls using the camera, this will disable both of them.
|
||||||
|
// That's fine for now, but this will break if that assumption changes.
|
||||||
|
this.disableLocalVideo();
|
||||||
|
|
||||||
|
delete this.callsLookup[conversationId];
|
||||||
|
|
||||||
|
if (
|
||||||
|
updateMessageState === GroupCallUpdateMessageState.SentJoin &&
|
||||||
|
peekInfo?.eraId != null
|
||||||
|
) {
|
||||||
|
updateMessageState = GroupCallUpdateMessageState.SentLeft;
|
||||||
|
if (callMode === CallMode.Group) {
|
||||||
|
void this.sendGroupCallUpdateMessage(
|
||||||
|
conversationId,
|
||||||
|
peekInfo?.eraId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.callsLookup[conversationId] = groupCall;
|
||||||
|
|
||||||
|
// NOTE: This assumes only one active call at a time. See comment above.
|
||||||
|
if (localDeviceState.videoMuted) {
|
||||||
|
this.disableLocalVideo();
|
||||||
|
} else {
|
||||||
|
this.videoCapturer.enableCaptureAndSend(groupCall);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
updateMessageState === GroupCallUpdateMessageState.SentNothing &&
|
||||||
|
localDeviceState.joinState === JoinState.Joined &&
|
||||||
|
peekInfo?.eraId != null
|
||||||
|
) {
|
||||||
|
updateMessageState = GroupCallUpdateMessageState.SentJoin;
|
||||||
|
if (callMode === CallMode.Group) {
|
||||||
|
void this.sendGroupCallUpdateMessage(
|
||||||
|
conversationId,
|
||||||
|
peekInfo?.eraId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.syncGroupCallToRedux(conversationId, groupCall, callMode);
|
||||||
|
},
|
||||||
|
onRemoteDeviceStatesChanged: groupCall => {
|
||||||
|
const localDeviceState = groupCall.getLocalDeviceState();
|
||||||
|
const peekInfo = groupCall.getPeekInfo();
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
'GroupCall#onRemoteDeviceStatesChanged',
|
||||||
|
formatLocalDeviceState(localDeviceState),
|
||||||
|
peekInfo ? formatPeekInfo(peekInfo) : '(No PeekInfo)'
|
||||||
|
);
|
||||||
|
|
||||||
|
this.syncGroupCallToRedux(conversationId, groupCall, callMode);
|
||||||
|
},
|
||||||
|
onAudioLevels: groupCall => {
|
||||||
|
const remoteDeviceStates = groupCall.getRemoteDeviceStates();
|
||||||
|
if (!remoteDeviceStates) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const localAudioLevel = groupCall.getLocalDeviceState().audioLevel;
|
||||||
|
|
||||||
|
this.reduxInterface?.groupCallAudioLevelsChange({
|
||||||
|
callMode,
|
||||||
|
conversationId,
|
||||||
|
localAudioLevel,
|
||||||
|
remoteDeviceStates,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onLowBandwidthForVideo: (_groupCall, _recovered) => {
|
||||||
|
// TODO: Implement handling of "low outgoing bandwidth for video" notification.
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param reactions A list of reactions received by the client ordered
|
||||||
|
* from oldest to newest.
|
||||||
|
*/
|
||||||
|
onReactions: (_groupCall, reactions) => {
|
||||||
|
this.reduxInterface?.receiveGroupCallReactions({
|
||||||
|
callMode,
|
||||||
|
conversationId,
|
||||||
|
reactions,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onRaisedHands: (_groupCall, raisedHands) => {
|
||||||
|
this.reduxInterface?.groupCallRaisedHandsChange({
|
||||||
|
callMode,
|
||||||
|
conversationId,
|
||||||
|
raisedHands,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onPeekChanged: groupCall => {
|
||||||
|
const localDeviceState = groupCall.getLocalDeviceState();
|
||||||
|
const peekInfo = groupCall.getPeekInfo() ?? null;
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
'GroupCall#onPeekChanged',
|
||||||
|
formatLocalDeviceState(localDeviceState),
|
||||||
|
peekInfo ? formatPeekInfo(peekInfo) : '(No PeekInfo)'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (callMode === CallMode.Group) {
|
||||||
|
const { eraId } = peekInfo ?? {};
|
||||||
|
if (
|
||||||
|
updateMessageState === GroupCallUpdateMessageState.SentNothing &&
|
||||||
|
localDeviceState.connectionState !== ConnectionState.NotConnected &&
|
||||||
|
localDeviceState.joinState === JoinState.Joined &&
|
||||||
|
eraId
|
||||||
|
) {
|
||||||
|
updateMessageState = GroupCallUpdateMessageState.SentJoin;
|
||||||
|
void this.sendGroupCallUpdateMessage(conversationId, eraId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void this.updateCallHistoryForGroupCall(
|
||||||
|
conversationId,
|
||||||
|
convertJoinState(localDeviceState.joinState),
|
||||||
|
peekInfo
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// TODO: Call history for adhoc calls
|
||||||
|
|
||||||
|
this.syncGroupCallToRedux(conversationId, groupCall, callMode);
|
||||||
|
},
|
||||||
|
async requestMembershipProof(_groupCall) {
|
||||||
|
log.error('GroupCall#requestMembershipProof not implemented.');
|
||||||
|
},
|
||||||
|
requestGroupMembers: groupCall => {
|
||||||
|
groupCall.setGroupMembers(this.getGroupCallMembers(conversationId));
|
||||||
|
},
|
||||||
|
onEnded: (groupCall, endedReason) => {
|
||||||
|
const localDeviceState = groupCall.getLocalDeviceState();
|
||||||
|
const peekInfo = groupCall.getPeekInfo();
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
'GroupCall#onEnded',
|
||||||
|
endedReason,
|
||||||
|
formatLocalDeviceState(localDeviceState),
|
||||||
|
peekInfo ? formatPeekInfo(peekInfo) : '(No PeekInfo)'
|
||||||
|
);
|
||||||
|
|
||||||
|
this.reduxInterface?.groupCallEnded({
|
||||||
|
conversationId,
|
||||||
|
endedReason,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async joinCallLinkCall({
|
||||||
|
roomId,
|
||||||
|
rootKey,
|
||||||
|
hasLocalAudio,
|
||||||
|
hasLocalVideo,
|
||||||
|
}: {
|
||||||
|
roomId: string;
|
||||||
|
rootKey: string;
|
||||||
|
hasLocalAudio: boolean;
|
||||||
|
hasLocalVideo: boolean;
|
||||||
|
}): Promise<void> {
|
||||||
|
const haveMediaPermissions = await this.requestPermissions(hasLocalVideo);
|
||||||
|
if (!haveMediaPermissions) {
|
||||||
|
log.info('Permissions were denied, but allow joining call link call');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.startDeviceReselectionTimer();
|
||||||
|
|
||||||
|
const callLinkRootKey = CallLinkRootKey.parse(rootKey);
|
||||||
|
const authCredentialPresentation =
|
||||||
|
await getCallLinkAuthCredentialPresentation(callLinkRootKey);
|
||||||
|
|
||||||
|
// RingRTC reuses the same type GroupCall between Adhoc and Group calls.
|
||||||
|
const groupCall = this.connectCallLinkCall({
|
||||||
|
roomId,
|
||||||
|
authCredentialPresentation,
|
||||||
|
callLinkRootKey,
|
||||||
|
adminPasskey: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
groupCall.setOutgoingAudioMuted(!hasLocalAudio);
|
||||||
|
groupCall.setOutgoingVideoMuted(!hasLocalVideo);
|
||||||
|
this.videoCapturer.enableCaptureAndSend(groupCall);
|
||||||
|
|
||||||
|
groupCall.join();
|
||||||
|
}
|
||||||
|
|
||||||
private getCallIdForConversation(conversationId: string): undefined | CallId {
|
private getCallIdForConversation(conversationId: string): undefined | CallId {
|
||||||
return this.getDirectCall(conversationId)?.callId;
|
return this.getDirectCall(conversationId)?.callId;
|
||||||
}
|
}
|
||||||
|
@ -1130,6 +1377,17 @@ export class CallingClass {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public formatCallLinkStateForRedux(
|
||||||
|
callLinkState: CallLinkState
|
||||||
|
): CallLinkStateType {
|
||||||
|
const { name, restrictions, expiration } = callLinkState;
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
restrictions,
|
||||||
|
expiration: expiration.getTime(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public getGroupCallVideoFrameSource(
|
public getGroupCallVideoFrameSource(
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
demuxId: number
|
demuxId: number
|
||||||
|
@ -1167,10 +1425,12 @@ export class CallingClass {
|
||||||
|
|
||||||
private syncGroupCallToRedux(
|
private syncGroupCallToRedux(
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
groupCall: GroupCall
|
groupCall: GroupCall,
|
||||||
|
callMode: CallMode.Group | CallMode.Adhoc
|
||||||
): void {
|
): void {
|
||||||
this.reduxInterface?.groupCallStateChange({
|
this.reduxInterface?.groupCallStateChange({
|
||||||
conversationId,
|
conversationId,
|
||||||
|
callMode,
|
||||||
...this.formatGroupCallForRedux(groupCall),
|
...this.formatGroupCallForRedux(groupCall),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1287,7 +1547,7 @@ export class CallingClass {
|
||||||
hangup(conversationId: string, reason: string): void {
|
hangup(conversationId: string, reason: string): void {
|
||||||
log.info(`CallingClass.hangup(${conversationId}): ${reason}`);
|
log.info(`CallingClass.hangup(${conversationId}): ${reason}`);
|
||||||
|
|
||||||
const specificCall = getOwn(this.callsByConversation, conversationId);
|
const specificCall = getOwn(this.callsLookup, conversationId);
|
||||||
if (!specificCall) {
|
if (!specificCall) {
|
||||||
log.error(
|
log.error(
|
||||||
`hangup: Trying to hang up a non-existent call for conversation ${conversationId}`
|
`hangup: Trying to hang up a non-existent call for conversation ${conversationId}`
|
||||||
|
@ -1296,7 +1556,7 @@ export class CallingClass {
|
||||||
|
|
||||||
ipcRenderer.send('close-screen-share-controller');
|
ipcRenderer.send('close-screen-share-controller');
|
||||||
|
|
||||||
const entries = Object.entries(this.callsByConversation);
|
const entries = Object.entries(this.callsLookup);
|
||||||
log.info(`hangup: ${entries.length} call(s) to hang up...`);
|
log.info(`hangup: ${entries.length} call(s) to hang up...`);
|
||||||
|
|
||||||
entries.forEach(([callConversationId, call]) => {
|
entries.forEach(([callConversationId, call]) => {
|
||||||
|
@ -1317,13 +1577,14 @@ export class CallingClass {
|
||||||
}
|
}
|
||||||
|
|
||||||
hangupAllCalls(reason: string): void {
|
hangupAllCalls(reason: string): void {
|
||||||
for (const conversationId of Object.keys(this.callsByConversation)) {
|
const conversationIds = Object.keys(this.callsLookup);
|
||||||
|
for (const conversationId of conversationIds) {
|
||||||
this.hangup(conversationId, reason);
|
this.hangup(conversationId, reason);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setOutgoingAudio(conversationId: string, enabled: boolean): void {
|
setOutgoingAudio(conversationId: string, enabled: boolean): void {
|
||||||
const call = getOwn(this.callsByConversation, conversationId);
|
const call = getOwn(this.callsLookup, conversationId);
|
||||||
if (!call) {
|
if (!call) {
|
||||||
log.warn('Trying to set outgoing audio for a non-existent call');
|
log.warn('Trying to set outgoing audio for a non-existent call');
|
||||||
return;
|
return;
|
||||||
|
@ -1339,7 +1600,7 @@ export class CallingClass {
|
||||||
}
|
}
|
||||||
|
|
||||||
setOutgoingVideo(conversationId: string, enabled: boolean): void {
|
setOutgoingVideo(conversationId: string, enabled: boolean): void {
|
||||||
const call = getOwn(this.callsByConversation, conversationId);
|
const call = getOwn(this.callsLookup, conversationId);
|
||||||
if (!call) {
|
if (!call) {
|
||||||
log.warn('Trying to set outgoing video for a non-existent call');
|
log.warn('Trying to set outgoing video for a non-existent call');
|
||||||
return;
|
return;
|
||||||
|
@ -1413,7 +1674,7 @@ export class CallingClass {
|
||||||
hasLocalVideo: boolean,
|
hasLocalVideo: boolean,
|
||||||
source?: PresentedSource
|
source?: PresentedSource
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const call = getOwn(this.callsByConversation, conversationId);
|
const call = getOwn(this.callsLookup, conversationId);
|
||||||
if (!call) {
|
if (!call) {
|
||||||
log.warn('Trying to set presenting for a non-existent call');
|
log.warn('Trying to set presenting for a non-existent call');
|
||||||
return;
|
return;
|
||||||
|
@ -2156,7 +2417,7 @@ export class CallingClass {
|
||||||
}
|
}
|
||||||
|
|
||||||
private attachToCall(conversation: ConversationModel, call: Call): void {
|
private attachToCall(conversation: ConversationModel, call: Call): void {
|
||||||
this.callsByConversation[conversation.id] = call;
|
this.callsLookup[conversation.id] = call;
|
||||||
|
|
||||||
const { reduxInterface } = this;
|
const { reduxInterface } = this;
|
||||||
if (!reduxInterface) {
|
if (!reduxInterface) {
|
||||||
|
@ -2174,7 +2435,7 @@ export class CallingClass {
|
||||||
if (call.state === CallState.Ended) {
|
if (call.state === CallState.Ended) {
|
||||||
this.stopDeviceReselectionTimer();
|
this.stopDeviceReselectionTimer();
|
||||||
this.lastMediaDeviceSettings = undefined;
|
this.lastMediaDeviceSettings = undefined;
|
||||||
delete this.callsByConversation[conversation.id];
|
delete this.callsLookup[conversation.id];
|
||||||
}
|
}
|
||||||
|
|
||||||
const localCallEvent = getLocalCallEventFromDirectCall(call);
|
const localCallEvent = getLocalCallEventFromDirectCall(call);
|
||||||
|
|
|
@ -2,7 +2,11 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { first, last, sortBy } from 'lodash';
|
import { first, last, sortBy } from 'lodash';
|
||||||
import { AuthCredentialWithPniResponse } from '@signalapp/libsignal-client/zkgroup';
|
import {
|
||||||
|
AuthCredentialWithPniResponse,
|
||||||
|
CallLinkAuthCredentialResponse,
|
||||||
|
GenericServerPublicParams,
|
||||||
|
} from '@signalapp/libsignal-client/zkgroup';
|
||||||
|
|
||||||
import { getClientZkAuthOperations } from '../util/zkgroup';
|
import { getClientZkAuthOperations } from '../util/zkgroup';
|
||||||
|
|
||||||
|
@ -23,14 +27,14 @@ type RequestDatesType = {
|
||||||
startDayInMs: number;
|
startDayInMs: number;
|
||||||
endDayInMs: number;
|
endDayInMs: number;
|
||||||
};
|
};
|
||||||
type NextCredentialsType = {
|
export type NextCredentialsType = {
|
||||||
today: GroupCredentialType;
|
today: GroupCredentialType;
|
||||||
tomorrow: GroupCredentialType;
|
tomorrow: GroupCredentialType;
|
||||||
};
|
};
|
||||||
|
|
||||||
let started = false;
|
let started = false;
|
||||||
|
|
||||||
function getCheckedCredentials(reason: string): CredentialsDataType {
|
function getCheckedGroupCredentials(reason: string): CredentialsDataType {
|
||||||
const result = window.storage.get('groupCredentials');
|
const result = window.storage.get('groupCredentials');
|
||||||
strictAssert(
|
strictAssert(
|
||||||
result !== undefined,
|
result !== undefined,
|
||||||
|
@ -39,6 +43,17 @@ function getCheckedCredentials(reason: string): CredentialsDataType {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCheckedCallLinkAuthCredentials(
|
||||||
|
reason: string
|
||||||
|
): CredentialsDataType {
|
||||||
|
const result = window.storage.get('callLinkAuthCredentials');
|
||||||
|
strictAssert(
|
||||||
|
result !== undefined,
|
||||||
|
`getCheckedCallLinkAuthCredentials: no credentials found, ${reason}`
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
export async function initializeGroupCredentialFetcher(): Promise<void> {
|
export async function initializeGroupCredentialFetcher(): Promise<void> {
|
||||||
if (started) {
|
if (started) {
|
||||||
return;
|
return;
|
||||||
|
@ -97,46 +112,57 @@ export async function runWithRetry(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// In cases where we are at a day boundary, we might need to use tomorrow in a retry
|
function getCredentialsForToday(
|
||||||
export function getCheckedCredentialsForToday(
|
credentials: CredentialsDataType
|
||||||
reason: string
|
|
||||||
): NextCredentialsType {
|
): NextCredentialsType {
|
||||||
const data = getCheckedCredentials(reason);
|
|
||||||
|
|
||||||
const today = toDayMillis(Date.now());
|
const today = toDayMillis(Date.now());
|
||||||
const todayIndex = data.findIndex(
|
const todayIndex = credentials.findIndex(
|
||||||
(item: GroupCredentialType) => item.redemptionTime === today
|
(item: GroupCredentialType) => item.redemptionTime === today
|
||||||
);
|
);
|
||||||
if (todayIndex < 0) {
|
if (todayIndex < 0) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'getCredentialsForToday: Cannot find credentials for today. ' +
|
'getCredentialsForToday: Cannot find credentials for today. ' +
|
||||||
`First: ${first(data)?.redemptionTime}, ` +
|
`First: ${first(credentials)?.redemptionTime}, ` +
|
||||||
`last: ${last(data)?.redemptionTime}`
|
`last: ${last(credentials)?.redemptionTime}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
today: data[todayIndex],
|
today: credentials[todayIndex],
|
||||||
tomorrow: data[todayIndex + 1],
|
tomorrow: credentials[todayIndex + 1],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In cases where we are at a day boundary, we might need to use tomorrow in a retry
|
||||||
|
export function getCheckedGroupCredentialsForToday(
|
||||||
|
reason: string
|
||||||
|
): NextCredentialsType {
|
||||||
|
return getCredentialsForToday(getCheckedGroupCredentials(reason));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCheckedCallLinkAuthCredentialsForToday(
|
||||||
|
reason: string
|
||||||
|
): NextCredentialsType {
|
||||||
|
return getCredentialsForToday(getCheckedCallLinkAuthCredentials(reason));
|
||||||
|
}
|
||||||
|
|
||||||
export async function maybeFetchNewCredentials(): Promise<void> {
|
export async function maybeFetchNewCredentials(): Promise<void> {
|
||||||
const logId = 'maybeFetchNewCredentials';
|
const logId = 'maybeFetchNewCredentials';
|
||||||
|
|
||||||
const aci = window.textsecure.storage.user.getAci();
|
const maybeAci = window.textsecure.storage.user.getAci();
|
||||||
if (!aci) {
|
if (!maybeAci) {
|
||||||
log.info(`${logId}: no ACI, returning early`);
|
log.info(`${logId}: no ACI, returning early`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const aci = maybeAci;
|
||||||
|
|
||||||
const previous: CredentialsDataType =
|
const prevGroupCredentials: CredentialsDataType =
|
||||||
window.storage.get('groupCredentials') ?? [];
|
window.storage.get('groupCredentials') ?? [];
|
||||||
const requestDates = getDatesForRequest(previous);
|
const prevCallLinkAuthCredentials: CredentialsDataType =
|
||||||
if (!requestDates) {
|
window.storage.get('callLinkAuthCredentials') ?? [];
|
||||||
log.info(`${logId}: no new credentials needed`);
|
|
||||||
return;
|
const requestDates = getDatesForRequest(prevGroupCredentials);
|
||||||
}
|
const requestDatesCallLinks = getDatesForRequest(prevCallLinkAuthCredentials);
|
||||||
|
|
||||||
const { server } = window.textsecure;
|
const { server } = window.textsecure;
|
||||||
if (!server) {
|
if (!server) {
|
||||||
|
@ -144,7 +170,22 @@ export async function maybeFetchNewCredentials(): Promise<void> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { startDayInMs, endDayInMs } = requestDates;
|
let startDayInMs: number;
|
||||||
|
let endDayInMs: number;
|
||||||
|
if (requestDates) {
|
||||||
|
startDayInMs = requestDates.startDayInMs;
|
||||||
|
endDayInMs = requestDates.endDayInMs;
|
||||||
|
if (requestDatesCallLinks) {
|
||||||
|
startDayInMs = Math.min(startDayInMs, requestDatesCallLinks.startDayInMs);
|
||||||
|
endDayInMs = Math.max(endDayInMs, requestDatesCallLinks.endDayInMs);
|
||||||
|
}
|
||||||
|
} else if (requestDatesCallLinks) {
|
||||||
|
startDayInMs = requestDatesCallLinks.startDayInMs;
|
||||||
|
endDayInMs = requestDatesCallLinks.endDayInMs;
|
||||||
|
} else {
|
||||||
|
log.info(`${logId}: no new credentials needed`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
log.info(
|
log.info(
|
||||||
`${logId}: fetching credentials for ${startDayInMs} through ${endDayInMs}`
|
`${logId}: fetching credentials for ${startDayInMs} through ${endDayInMs}`
|
||||||
);
|
);
|
||||||
|
@ -156,8 +197,11 @@ export async function maybeFetchNewCredentials(): Promise<void> {
|
||||||
|
|
||||||
// Received credentials depend on us knowing up-to-date PNI. Use the latest
|
// Received credentials depend on us knowing up-to-date PNI. Use the latest
|
||||||
// value from the server and log error on mismatch.
|
// value from the server and log error on mismatch.
|
||||||
const { pni: untaggedPni, credentials: rawCredentials } =
|
const {
|
||||||
await server.getGroupCredentials({ startDayInMs, endDayInMs });
|
pni: untaggedPni,
|
||||||
|
credentials: rawCredentials,
|
||||||
|
callLinkAuthCredentials,
|
||||||
|
} = await server.getGroupCredentials({ startDayInMs, endDayInMs });
|
||||||
strictAssert(
|
strictAssert(
|
||||||
untaggedPni,
|
untaggedPni,
|
||||||
'Server must give pni along with group credentials'
|
'Server must give pni along with group credentials'
|
||||||
|
@ -169,8 +213,7 @@ export async function maybeFetchNewCredentials(): Promise<void> {
|
||||||
log.error(`${logId}: local PNI ${localPni}, does not match remote ${pni}`);
|
log.error(`${logId}: local PNI ${localPni}, does not match remote ${pni}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const newCredentials = sortCredentials(rawCredentials).map(
|
function formatCredential(item: GroupCredentialType): GroupCredentialType {
|
||||||
(item: GroupCredentialType) => {
|
|
||||||
const authCredential =
|
const authCredential =
|
||||||
clientZKAuthOperations.receiveAuthCredentialWithPniAsServiceId(
|
clientZKAuthOperations.receiveAuthCredentialWithPniAsServiceId(
|
||||||
toAciObject(aci),
|
toAciObject(aci),
|
||||||
|
@ -187,24 +230,81 @@ export async function maybeFetchNewCredentials(): Promise<void> {
|
||||||
credential,
|
credential,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const newGroupCredentials =
|
||||||
|
sortCredentials(rawCredentials).map(formatCredential);
|
||||||
|
const genericServerPublicParamsBase64 = window.getGenericServerPublicParams();
|
||||||
|
const genericServerPublicParams = new GenericServerPublicParams(
|
||||||
|
Buffer.from(genericServerPublicParamsBase64, 'base64')
|
||||||
|
);
|
||||||
|
|
||||||
|
function formatCallingCredential(
|
||||||
|
item: GroupCredentialType
|
||||||
|
): GroupCredentialType {
|
||||||
|
const response = new CallLinkAuthCredentialResponse(
|
||||||
|
Buffer.from(item.credential, 'base64')
|
||||||
|
);
|
||||||
|
const authCredential = response.receive(
|
||||||
|
toAciObject(aci),
|
||||||
|
item.redemptionTime,
|
||||||
|
genericServerPublicParams
|
||||||
|
);
|
||||||
|
const credential = authCredential.serialize().toString('base64');
|
||||||
|
|
||||||
|
return {
|
||||||
|
redemptionTime: item.redemptionTime * durations.SECOND,
|
||||||
|
credential,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const newCallLinkAuthCredentialsRaw = sortCredentials(
|
||||||
|
callLinkAuthCredentials
|
||||||
|
);
|
||||||
|
const newCallLinkAuthCredentials = newCallLinkAuthCredentialsRaw.map(
|
||||||
|
formatCallingCredential
|
||||||
);
|
);
|
||||||
|
|
||||||
const today = toDayMillis(Date.now());
|
const today = toDayMillis(Date.now());
|
||||||
const previousCleaned = previous
|
const prevGroupCredentialsCleaned =
|
||||||
? previous.filter(
|
prevGroupCredentials?.filter(
|
||||||
(item: GroupCredentialType) => item.redemptionTime >= today
|
(item: GroupCredentialType) => item.redemptionTime >= today
|
||||||
)
|
) ?? [];
|
||||||
: [];
|
const prevCallLinkAuthCredentialsCleaned =
|
||||||
const finalCredentials = [...previousCleaned, ...newCredentials];
|
prevCallLinkAuthCredentials?.filter(
|
||||||
|
(item: GroupCredentialType) => item.redemptionTime >= today
|
||||||
|
) ?? [];
|
||||||
|
const finalGroupCredentials = [
|
||||||
|
...prevGroupCredentialsCleaned,
|
||||||
|
...newGroupCredentials,
|
||||||
|
];
|
||||||
|
const finalCallLinkAuthCredentials = [
|
||||||
|
...prevCallLinkAuthCredentialsCleaned,
|
||||||
|
...newCallLinkAuthCredentials,
|
||||||
|
];
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
`${logId}: saving ${newCredentials.length} new credentials, ` +
|
`${logId}: saving ${
|
||||||
`cleaning up ${previous.length - previousCleaned.length} old ` +
|
finalGroupCredentials.length
|
||||||
`credentials, haveToday=${haveToday(finalCredentials)}`
|
} new group credentials, cleaning up ${
|
||||||
|
prevGroupCredentials.length - prevGroupCredentialsCleaned.length
|
||||||
|
} old group credentials, haveToday=${haveToday(finalGroupCredentials)}`
|
||||||
|
);
|
||||||
|
log.info(
|
||||||
|
`${logId}: saving ${
|
||||||
|
finalCallLinkAuthCredentials.length
|
||||||
|
} new call link auth credentials, cleaning up ${
|
||||||
|
prevCallLinkAuthCredentials.length -
|
||||||
|
prevCallLinkAuthCredentialsCleaned.length
|
||||||
|
} old call link auth credentials, haveToday=${haveToday(
|
||||||
|
finalCallLinkAuthCredentials
|
||||||
|
)}`
|
||||||
);
|
);
|
||||||
|
|
||||||
// Note: we don't wait for this to finish
|
await window.storage.put('groupCredentials', finalGroupCredentials);
|
||||||
await window.storage.put('groupCredentials', finalCredentials);
|
await window.storage.put(
|
||||||
|
'callLinkAuthCredentials',
|
||||||
|
finalCallLinkAuthCredentials
|
||||||
|
);
|
||||||
log.info(`${logId}: Save complete.`);
|
log.info(`${logId}: Save complete.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -34,6 +34,9 @@ export const getIncomingCall = (
|
||||||
call.connectionState === GroupCallConnectionState.NotConnected &&
|
call.connectionState === GroupCallConnectionState.NotConnected &&
|
||||||
isAnybodyElseInGroupCall(call.peekInfo, ourAci)
|
isAnybodyElseInGroupCall(call.peekInfo, ourAci)
|
||||||
);
|
);
|
||||||
|
case CallMode.Adhoc:
|
||||||
|
// Adhoc calls cannot be incoming.
|
||||||
|
return;
|
||||||
default:
|
default:
|
||||||
throw missingCaseError(call);
|
throw missingCaseError(call);
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,7 @@ import {
|
||||||
import { SHOW_TOAST } from './toast';
|
import { SHOW_TOAST } from './toast';
|
||||||
import type { ShowToastActionType } from './toast';
|
import type { ShowToastActionType } from './toast';
|
||||||
import { isDownloaded } from '../../types/Attachment';
|
import { isDownloaded } from '../../types/Attachment';
|
||||||
|
import type { ButtonVariant } from '../../components/Button';
|
||||||
|
|
||||||
// State
|
// State
|
||||||
|
|
||||||
|
@ -86,6 +87,7 @@ export type GlobalModalsStateType = ReadonlyDeep<{
|
||||||
deleteMessagesProps?: DeleteMessagesPropsType;
|
deleteMessagesProps?: DeleteMessagesPropsType;
|
||||||
editHistoryMessages?: EditHistoryMessagesType;
|
editHistoryMessages?: EditHistoryMessagesType;
|
||||||
errorModalProps?: {
|
errorModalProps?: {
|
||||||
|
buttonVariant?: ButtonVariant;
|
||||||
description?: string;
|
description?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
};
|
};
|
||||||
|
@ -308,6 +310,7 @@ type CloseErrorModalActionType = ReadonlyDeep<{
|
||||||
export type ShowErrorModalActionType = ReadonlyDeep<{
|
export type ShowErrorModalActionType = ReadonlyDeep<{
|
||||||
type: typeof SHOW_ERROR_MODAL;
|
type: typeof SHOW_ERROR_MODAL;
|
||||||
payload: {
|
payload: {
|
||||||
|
buttonVariant?: ButtonVariant;
|
||||||
description?: string;
|
description?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
};
|
};
|
||||||
|
@ -729,15 +732,18 @@ function closeErrorModal(): CloseErrorModalActionType {
|
||||||
}
|
}
|
||||||
|
|
||||||
function showErrorModal({
|
function showErrorModal({
|
||||||
|
buttonVariant,
|
||||||
description,
|
description,
|
||||||
title,
|
title,
|
||||||
}: {
|
}: {
|
||||||
title?: string;
|
buttonVariant?: ButtonVariant;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
title?: string;
|
||||||
}): ShowErrorModalActionType {
|
}): ShowErrorModalActionType {
|
||||||
return {
|
return {
|
||||||
type: SHOW_ERROR_MODAL,
|
type: SHOW_ERROR_MODAL,
|
||||||
payload: {
|
payload: {
|
||||||
|
buttonVariant,
|
||||||
description,
|
description,
|
||||||
title,
|
title,
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,10 +7,14 @@ import type { StateType } from '../reducer';
|
||||||
import type {
|
import type {
|
||||||
CallingStateType,
|
CallingStateType,
|
||||||
CallsByConversationType,
|
CallsByConversationType,
|
||||||
|
AdhocCallsType,
|
||||||
|
CallLinksByRoomIdType,
|
||||||
DirectCallStateType,
|
DirectCallStateType,
|
||||||
GroupCallStateType,
|
GroupCallStateType,
|
||||||
} from '../ducks/calling';
|
} from '../ducks/calling';
|
||||||
import { getIncomingCall as getIncomingCallHelper } from '../ducks/callingHelpers';
|
import { getIncomingCall as getIncomingCallHelper } from '../ducks/callingHelpers';
|
||||||
|
import { CallMode } from '../../types/Calling';
|
||||||
|
import type { CallLinkType } from '../../types/CallLink';
|
||||||
import { getUserACI } from './user';
|
import { getUserACI } from './user';
|
||||||
import { getOwn } from '../../util/getOwn';
|
import { getOwn } from '../../util/getOwn';
|
||||||
import type { AciString } from '../../types/ServiceId';
|
import type { AciString } from '../../types/ServiceId';
|
||||||
|
@ -30,6 +34,38 @@ export const getCallsByConversation = createSelector(
|
||||||
state.callsByConversation
|
state.callsByConversation
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const getAdhocCalls = createSelector(
|
||||||
|
getCalling,
|
||||||
|
(state: CallingStateType): AdhocCallsType => state.adhocCalls
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getCallLinksByRoomId = createSelector(
|
||||||
|
getCalling,
|
||||||
|
(state: CallingStateType): CallLinksByRoomIdType => state.callLinks
|
||||||
|
);
|
||||||
|
|
||||||
|
export type CallLinkSelectorType = (roomId: string) => CallLinkType | undefined;
|
||||||
|
|
||||||
|
export const getCallLinkSelector = createSelector(
|
||||||
|
getCallLinksByRoomId,
|
||||||
|
(callLinksByRoomId: CallLinksByRoomIdType): CallLinkSelectorType =>
|
||||||
|
(roomId: string): CallLinkType | undefined => {
|
||||||
|
const callLinkState = getOwn(callLinksByRoomId, roomId);
|
||||||
|
if (!callLinkState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { name, restrictions, rootKey, expiration } = callLinkState;
|
||||||
|
return {
|
||||||
|
roomId,
|
||||||
|
name,
|
||||||
|
restrictions,
|
||||||
|
rootKey,
|
||||||
|
expiration,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export type CallSelectorType = (
|
export type CallSelectorType = (
|
||||||
conversationId: string
|
conversationId: string
|
||||||
) => CallStateType | undefined;
|
) => CallStateType | undefined;
|
||||||
|
@ -40,15 +76,33 @@ export const getCallSelector = createSelector(
|
||||||
getOwn(callsByConversation, conversationId)
|
getOwn(callsByConversation, conversationId)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export type AdhocCallSelectorType = (
|
||||||
|
conversationId: string
|
||||||
|
) => GroupCallStateType | undefined;
|
||||||
|
export const getAdhocCallSelector = createSelector(
|
||||||
|
getAdhocCalls,
|
||||||
|
(adhocCalls: AdhocCallsType): AdhocCallSelectorType =>
|
||||||
|
(roomId: string) =>
|
||||||
|
getOwn(adhocCalls, roomId)
|
||||||
|
);
|
||||||
|
|
||||||
export const getActiveCall = createSelector(
|
export const getActiveCall = createSelector(
|
||||||
getActiveCallState,
|
getActiveCallState,
|
||||||
getCallSelector,
|
getCallSelector,
|
||||||
(activeCallState, callSelector): undefined | CallStateType => {
|
getAdhocCallSelector,
|
||||||
if (activeCallState && activeCallState.conversationId) {
|
(
|
||||||
return callSelector(activeCallState.conversationId);
|
activeCallState,
|
||||||
|
callSelector,
|
||||||
|
adhocCallSelector
|
||||||
|
): undefined | CallStateType => {
|
||||||
|
const { callMode, conversationId } = activeCallState || {};
|
||||||
|
if (!conversationId) {
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return callMode === CallMode.Adhoc
|
||||||
|
? adhocCallSelector(conversationId)
|
||||||
|
: callSelector(conversationId);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,7 @@ import type {
|
||||||
import type { PropsType as ProfileChangeNotificationPropsType } from '../../components/conversation/ProfileChangeNotification';
|
import type { PropsType as ProfileChangeNotificationPropsType } from '../../components/conversation/ProfileChangeNotification';
|
||||||
import type { QuotedAttachmentType } from '../../components/conversation/Quote';
|
import type { QuotedAttachmentType } from '../../components/conversation/Quote';
|
||||||
|
|
||||||
import { getDomain, isStickerPack } from '../../types/LinkPreview';
|
import { getDomain, isCallLink, isStickerPack } from '../../types/LinkPreview';
|
||||||
import type {
|
import type {
|
||||||
AciString,
|
AciString,
|
||||||
PniString,
|
PniString,
|
||||||
|
@ -383,6 +383,7 @@ const getPreviewsForMessage = ({
|
||||||
return previews.map(preview => ({
|
return previews.map(preview => ({
|
||||||
...preview,
|
...preview,
|
||||||
isStickerPack: isStickerPack(preview.url),
|
isStickerPack: isStickerPack(preview.url),
|
||||||
|
isCallLink: isCallLink(preview.url),
|
||||||
domain: getDomain(preview.url),
|
domain: getDomain(preview.url),
|
||||||
image: preview.image ? getPropsForAttachment(preview.image) : undefined,
|
image: preview.image ? getPropsForAttachment(preview.image) : undefined,
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -15,7 +15,7 @@ import { getIntl, getTheme } from '../selectors/user';
|
||||||
import { getMe, getConversationSelector } from '../selectors/conversations';
|
import { getMe, getConversationSelector } from '../selectors/conversations';
|
||||||
import { getActiveCall } from '../ducks/calling';
|
import { getActiveCall } from '../ducks/calling';
|
||||||
import type { ConversationType } from '../ducks/conversations';
|
import type { ConversationType } from '../ducks/conversations';
|
||||||
import { getIncomingCall } from '../selectors/calling';
|
import { getCallLinkSelector, getIncomingCall } from '../selectors/calling';
|
||||||
import { isGroupCallRaiseHandEnabled } from '../../util/isGroupCallRaiseHandEnabled';
|
import { isGroupCallRaiseHandEnabled } from '../../util/isGroupCallRaiseHandEnabled';
|
||||||
import { isGroupCallReactionsEnabled } from '../../util/isGroupCallReactionsEnabled';
|
import { isGroupCallReactionsEnabled } from '../../util/isGroupCallReactionsEnabled';
|
||||||
import type {
|
import type {
|
||||||
|
@ -23,12 +23,14 @@ import type {
|
||||||
ActiveCallType,
|
ActiveCallType,
|
||||||
ActiveDirectCallType,
|
ActiveDirectCallType,
|
||||||
ActiveGroupCallType,
|
ActiveGroupCallType,
|
||||||
|
CallingConversationType,
|
||||||
ConversationsByDemuxIdType,
|
ConversationsByDemuxIdType,
|
||||||
GroupCallRemoteParticipantType,
|
GroupCallRemoteParticipantType,
|
||||||
} from '../../types/Calling';
|
} from '../../types/Calling';
|
||||||
import { isAciString } from '../../util/isAciString';
|
import { isAciString } from '../../util/isAciString';
|
||||||
import type { AciString } from '../../types/ServiceId';
|
import type { AciString } from '../../types/ServiceId';
|
||||||
import { CallMode, CallState } from '../../types/Calling';
|
import { CallMode, CallState } from '../../types/Calling';
|
||||||
|
import type { CallLinkType } from '../../types/CallLink';
|
||||||
import type { StateType } from '../reducer';
|
import type { StateType } from '../reducer';
|
||||||
import { missingCaseError } from '../../util/missingCaseError';
|
import { missingCaseError } from '../../util/missingCaseError';
|
||||||
import { SmartCallingDeviceSelection } from './CallingDeviceSelection';
|
import { SmartCallingDeviceSelection } from './CallingDeviceSelection';
|
||||||
|
@ -51,6 +53,7 @@ import { isConversationTooBigToRing } from '../../conversations/isConversationTo
|
||||||
import { strictAssert } from '../../util/assert';
|
import { strictAssert } from '../../util/assert';
|
||||||
import { renderEmojiPicker } from './renderEmojiPicker';
|
import { renderEmojiPicker } from './renderEmojiPicker';
|
||||||
import { renderReactionPicker } from './renderReactionPicker';
|
import { renderReactionPicker } from './renderReactionPicker';
|
||||||
|
import { callLinkToConversation } from '../../util/callLinks';
|
||||||
|
|
||||||
function renderDeviceSelection(): JSX.Element {
|
function renderDeviceSelection(): JSX.Element {
|
||||||
return <SmartCallingDeviceSelection />;
|
return <SmartCallingDeviceSelection />;
|
||||||
|
@ -133,7 +136,19 @@ const mapStateToActiveCallProp = (
|
||||||
}
|
}
|
||||||
|
|
||||||
const conversationSelector = getConversationSelector(state);
|
const conversationSelector = getConversationSelector(state);
|
||||||
const conversation = conversationSelector(activeCallState.conversationId);
|
let conversation: CallingConversationType;
|
||||||
|
if (call.callMode === CallMode.Adhoc) {
|
||||||
|
const callLinkSelector = getCallLinkSelector(state);
|
||||||
|
const callLink = callLinkSelector(activeCallState.conversationId);
|
||||||
|
if (!callLink) {
|
||||||
|
// An error is logged in mapStateToCallLinkProp
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
conversation = callLinkToConversation(callLink, window.i18n);
|
||||||
|
} else {
|
||||||
|
conversation = conversationSelector(activeCallState.conversationId);
|
||||||
|
}
|
||||||
if (!conversation) {
|
if (!conversation) {
|
||||||
log.error('The active call has no corresponding conversation');
|
log.error('The active call has no corresponding conversation');
|
||||||
return undefined;
|
return undefined;
|
||||||
|
@ -199,7 +214,8 @@ const mapStateToActiveCallProp = (
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
} satisfies ActiveDirectCallType;
|
} satisfies ActiveDirectCallType;
|
||||||
case CallMode.Group: {
|
case CallMode.Group:
|
||||||
|
case CallMode.Adhoc: {
|
||||||
const conversationsWithSafetyNumberChanges: Array<ConversationType> = [];
|
const conversationsWithSafetyNumberChanges: Array<ConversationType> = [];
|
||||||
const groupMembers: Array<ConversationType> = [];
|
const groupMembers: Array<ConversationType> = [];
|
||||||
const remoteParticipants: Array<GroupCallRemoteParticipantType> = [];
|
const remoteParticipants: Array<GroupCallRemoteParticipantType> = [];
|
||||||
|
@ -305,7 +321,7 @@ const mapStateToActiveCallProp = (
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...baseResult,
|
...baseResult,
|
||||||
callMode: CallMode.Group,
|
callMode: call.callMode,
|
||||||
connectionState: call.connectionState,
|
connectionState: call.connectionState,
|
||||||
conversationsWithSafetyNumberChanges,
|
conversationsWithSafetyNumberChanges,
|
||||||
conversationsByDemuxId,
|
conversationsByDemuxId,
|
||||||
|
@ -326,6 +342,31 @@ const mapStateToActiveCallProp = (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mapStateToCallLinkProp = (state: StateType): CallLinkType | undefined => {
|
||||||
|
const { calling } = state;
|
||||||
|
const { activeCallState } = calling;
|
||||||
|
|
||||||
|
if (!activeCallState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const call = getActiveCall(calling);
|
||||||
|
if (call?.callMode !== CallMode.Adhoc) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const callLinkSelector = getCallLinkSelector(state);
|
||||||
|
const callLink = callLinkSelector(activeCallState.conversationId);
|
||||||
|
if (!callLink) {
|
||||||
|
log.error(
|
||||||
|
'Active call referred to a call link but no corresponding call link in state.'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return callLink;
|
||||||
|
};
|
||||||
|
|
||||||
const mapStateToIncomingCallProp = (
|
const mapStateToIncomingCallProp = (
|
||||||
state: StateType
|
state: StateType
|
||||||
): DirectIncomingCall | GroupIncomingCall | null => {
|
): DirectIncomingCall | GroupIncomingCall | null => {
|
||||||
|
@ -371,6 +412,9 @@ const mapStateToIncomingCallProp = (
|
||||||
remoteParticipants: call.remoteParticipants,
|
remoteParticipants: call.remoteParticipants,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case CallMode.Adhoc:
|
||||||
|
log.error('Cannot handle an incoming adhoc call');
|
||||||
|
return null;
|
||||||
default:
|
default:
|
||||||
throw missingCaseError(call);
|
throw missingCaseError(call);
|
||||||
}
|
}
|
||||||
|
@ -381,6 +425,7 @@ const mapStateToProps = (state: StateType) => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
activeCall: mapStateToActiveCallProp(state),
|
activeCall: mapStateToActiveCallProp(state),
|
||||||
|
callLink: mapStateToCallLinkProp(state),
|
||||||
bounceAppIconStart,
|
bounceAppIconStart,
|
||||||
bounceAppIconStop,
|
bounceAppIconStop,
|
||||||
availableCameras: state.calling.availableCameras,
|
availableCameras: state.calling.availableCameras,
|
||||||
|
|
|
@ -36,6 +36,7 @@ import { useSearchActions } from '../ducks/search';
|
||||||
import { useStoriesActions } from '../ducks/stories';
|
import { useStoriesActions } from '../ducks/stories';
|
||||||
import { getCannotLeaveBecauseYouAreLastAdmin } from '../../components/conversation/conversation-details/ConversationDetails';
|
import { getCannotLeaveBecauseYouAreLastAdmin } from '../../components/conversation/conversation-details/ConversationDetails';
|
||||||
import { getGroupMemberships } from '../../util/getGroupMemberships';
|
import { getGroupMemberships } from '../../util/getGroupMemberships';
|
||||||
|
import { isGroupOrAdhocCallState } from '../../util/isGroupOrAdhocCall';
|
||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -59,10 +60,11 @@ const getOutgoingCallButtonStyle = (
|
||||||
return OutgoingCallButtonStyle.None;
|
return OutgoingCallButtonStyle.None;
|
||||||
case CallMode.Direct:
|
case CallMode.Direct:
|
||||||
return OutgoingCallButtonStyle.Both;
|
return OutgoingCallButtonStyle.Both;
|
||||||
case CallMode.Group: {
|
case CallMode.Group:
|
||||||
|
case CallMode.Adhoc: {
|
||||||
const call = getOwn(calling.callsByConversation, conversation.id);
|
const call = getOwn(calling.callsByConversation, conversation.id);
|
||||||
if (
|
if (
|
||||||
call?.callMode === CallMode.Group &&
|
isGroupOrAdhocCallState(call) &&
|
||||||
isAnybodyElseInGroupCall(call.peekInfo, ourAci)
|
isAnybodyElseInGroupCall(call.peekInfo, ourAci)
|
||||||
) {
|
) {
|
||||||
return OutgoingCallButtonStyle.Join;
|
return OutgoingCallButtonStyle.Join;
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
import type { GlobalModalsStateType } from '../ducks/globalModals';
|
import type { GlobalModalsStateType } from '../ducks/globalModals';
|
||||||
import type { StateType } from '../reducer';
|
import type { StateType } from '../reducer';
|
||||||
|
import type { ButtonVariant } from '../../components/Button';
|
||||||
import { ErrorModal } from '../../components/ErrorModal';
|
import { ErrorModal } from '../../components/ErrorModal';
|
||||||
import { GlobalModalContainer } from '../../components/GlobalModalContainer';
|
import { GlobalModalContainer } from '../../components/GlobalModalContainer';
|
||||||
import { SmartAboutContactModal } from './AboutContactModal';
|
import { SmartAboutContactModal } from './AboutContactModal';
|
||||||
|
@ -133,10 +134,19 @@ export function SmartGlobalModalContainer(): JSX.Element {
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderErrorModal = useCallback(
|
const renderErrorModal = useCallback(
|
||||||
({ description, title }: { description?: string; title?: string }) => (
|
({
|
||||||
|
buttonVariant,
|
||||||
|
description,
|
||||||
|
title,
|
||||||
|
}: {
|
||||||
|
buttonVariant?: ButtonVariant;
|
||||||
|
description?: string;
|
||||||
|
title?: string;
|
||||||
|
}) => (
|
||||||
<ErrorModal
|
<ErrorModal
|
||||||
title={title}
|
buttonVariant={buttonVariant}
|
||||||
description={description}
|
description={description}
|
||||||
|
title={title}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
onClose={closeErrorModal}
|
onClose={closeErrorModal}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -14,6 +14,7 @@ describe('shouldUseFullSizeLinkPreviewImage', () => {
|
||||||
domain: 'example.com',
|
domain: 'example.com',
|
||||||
url: 'https://example.com/foo.html',
|
url: 'https://example.com/foo.html',
|
||||||
isStickerPack: false,
|
isStickerPack: false,
|
||||||
|
isCallLink: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
it('returns false if there is no image', () => {
|
it('returns false if there is no image', () => {
|
||||||
|
|
|
@ -17,6 +17,7 @@ describe('both/state/ducks/linkPreviews', () => {
|
||||||
domain: 'signal.org',
|
domain: 'signal.org',
|
||||||
url: 'https://www.signal.org',
|
url: 'https://www.signal.org',
|
||||||
isStickerPack: false,
|
isStickerPack: false,
|
||||||
|
isCallLink: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -75,6 +75,34 @@ describe('Privacy', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('redactCallLinkRoomIds', () => {
|
||||||
|
it('should redact call link room IDs', () => {
|
||||||
|
const text =
|
||||||
|
'Log line with call link room ID 7f3d431d4512b30754915a262db43cd789f799d710525a83429d48aee8c2cd4b\n' +
|
||||||
|
'and another IN ALL UPPERCASE 7F3D431D4512B30754915A262DB43CD789F799D710525A83429D48AEE8C2CD4B';
|
||||||
|
|
||||||
|
const actual = Privacy.redactCallLinkRoomIds(text);
|
||||||
|
const expected =
|
||||||
|
'Log line with call link room ID [REDACTED]d4b\n' +
|
||||||
|
'and another IN ALL UPPERCASE [REDACTED]D4B';
|
||||||
|
assert.equal(actual, expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('redactCallLinkRootKeys', () => {
|
||||||
|
it('should redact call link root keys', () => {
|
||||||
|
const text =
|
||||||
|
'Log line with call link https://signal.link/call/#key=hktt-kskq-dhcn-bgkm-hbbg-qqkq-sfbp-czmc\n' +
|
||||||
|
'and another IN ALL UPPERCASE HKTT-KSKQ-DHCN-BGKM-HBBG-QQKQ-SFBP-CZMC';
|
||||||
|
|
||||||
|
const actual = Privacy.redactCallLinkRootKeys(text);
|
||||||
|
const expected =
|
||||||
|
'Log line with call link https://signal.link/call/#key=[REDACTED]hktt\n' +
|
||||||
|
'and another IN ALL UPPERCASE [REDACTED]HKTT';
|
||||||
|
assert.equal(actual, expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('redactAll', () => {
|
describe('redactAll', () => {
|
||||||
it('should redact all sensitive information', () => {
|
it('should redact all sensitive information', () => {
|
||||||
const encodedAppRootPath = APP_ROOT_PATH.replace(/ /g, '%20');
|
const encodedAppRootPath = APP_ROOT_PATH.replace(/ /g, '%20');
|
||||||
|
|
|
@ -138,6 +138,7 @@ describe('Conversations', () => {
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
url: 'https://sometest.signal.org/',
|
url: 'https://sometest.signal.org/',
|
||||||
|
isCallLink: false,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
@ -154,6 +155,7 @@ describe('Conversations', () => {
|
||||||
size: 100,
|
size: 100,
|
||||||
data: new Uint8Array(),
|
data: new Uint8Array(),
|
||||||
},
|
},
|
||||||
|
isCallLink: false,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
|
@ -63,6 +63,7 @@ describe('calling duck', () => {
|
||||||
const stateWithActiveDirectCall: CallingStateTypeWithActiveCall = {
|
const stateWithActiveDirectCall: CallingStateTypeWithActiveCall = {
|
||||||
...stateWithDirectCall,
|
...stateWithDirectCall,
|
||||||
activeCallState: {
|
activeCallState: {
|
||||||
|
callMode: CallMode.Direct,
|
||||||
conversationId: directCallState.conversationId,
|
conversationId: directCallState.conversationId,
|
||||||
hasLocalAudio: true,
|
hasLocalAudio: true,
|
||||||
hasLocalVideo: false,
|
hasLocalVideo: false,
|
||||||
|
@ -145,6 +146,7 @@ describe('calling duck', () => {
|
||||||
const stateWithActiveGroupCall: CallingStateTypeWithActiveCall = {
|
const stateWithActiveGroupCall: CallingStateTypeWithActiveCall = {
|
||||||
...stateWithGroupCall,
|
...stateWithGroupCall,
|
||||||
activeCallState: {
|
activeCallState: {
|
||||||
|
callMode: CallMode.Group,
|
||||||
conversationId: 'fake-group-call-conversation-id',
|
conversationId: 'fake-group-call-conversation-id',
|
||||||
hasLocalAudio: true,
|
hasLocalAudio: true,
|
||||||
hasLocalVideo: false,
|
hasLocalVideo: false,
|
||||||
|
@ -473,6 +475,7 @@ describe('calling duck', () => {
|
||||||
const result = reducer(stateWithIncomingDirectCall, action);
|
const result = reducer(stateWithIncomingDirectCall, action);
|
||||||
|
|
||||||
assert.deepEqual(result.activeCallState, {
|
assert.deepEqual(result.activeCallState, {
|
||||||
|
callMode: CallMode.Direct,
|
||||||
conversationId: 'fake-direct-call-conversation-id',
|
conversationId: 'fake-direct-call-conversation-id',
|
||||||
hasLocalAudio: true,
|
hasLocalAudio: true,
|
||||||
hasLocalVideo: true,
|
hasLocalVideo: true,
|
||||||
|
@ -567,6 +570,7 @@ describe('calling duck', () => {
|
||||||
const result = reducer(stateWithIncomingGroupCall, action);
|
const result = reducer(stateWithIncomingGroupCall, action);
|
||||||
|
|
||||||
assert.deepEqual(result.activeCallState, {
|
assert.deepEqual(result.activeCallState, {
|
||||||
|
callMode: CallMode.Group,
|
||||||
conversationId: 'fake-group-call-conversation-id',
|
conversationId: 'fake-group-call-conversation-id',
|
||||||
hasLocalAudio: true,
|
hasLocalAudio: true,
|
||||||
hasLocalVideo: true,
|
hasLocalVideo: true,
|
||||||
|
@ -816,6 +820,7 @@ describe('calling duck', () => {
|
||||||
|
|
||||||
it("does nothing if there's no relevant call", () => {
|
it("does nothing if there's no relevant call", () => {
|
||||||
const action = groupCallAudioLevelsChange({
|
const action = groupCallAudioLevelsChange({
|
||||||
|
callMode: CallMode.Group,
|
||||||
conversationId: 'garbage',
|
conversationId: 'garbage',
|
||||||
localAudioLevel: 1,
|
localAudioLevel: 1,
|
||||||
remoteDeviceStates,
|
remoteDeviceStates,
|
||||||
|
@ -839,6 +844,7 @@ describe('calling duck', () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const action = groupCallAudioLevelsChange({
|
const action = groupCallAudioLevelsChange({
|
||||||
|
callMode: CallMode.Group,
|
||||||
conversationId: 'fake-group-call-conversation-id',
|
conversationId: 'fake-group-call-conversation-id',
|
||||||
localAudioLevel: 0.001,
|
localAudioLevel: 0.001,
|
||||||
remoteDeviceStates,
|
remoteDeviceStates,
|
||||||
|
@ -851,6 +857,7 @@ describe('calling duck', () => {
|
||||||
|
|
||||||
it('updates the set of speaking participants, including yourself', () => {
|
it('updates the set of speaking participants, including yourself', () => {
|
||||||
const action = groupCallAudioLevelsChange({
|
const action = groupCallAudioLevelsChange({
|
||||||
|
callMode: CallMode.Group,
|
||||||
conversationId: 'fake-group-call-conversation-id',
|
conversationId: 'fake-group-call-conversation-id',
|
||||||
localAudioLevel: 0.8,
|
localAudioLevel: 0.8,
|
||||||
remoteDeviceStates,
|
remoteDeviceStates,
|
||||||
|
@ -888,6 +895,7 @@ describe('calling duck', () => {
|
||||||
const result = reducer(
|
const result = reducer(
|
||||||
getEmptyState(),
|
getEmptyState(),
|
||||||
getAction({
|
getAction({
|
||||||
|
callMode: CallMode.Group,
|
||||||
conversationId: 'fake-group-call-conversation-id',
|
conversationId: 'fake-group-call-conversation-id',
|
||||||
connectionState: GroupCallConnectionState.Connected,
|
connectionState: GroupCallConnectionState.Connected,
|
||||||
joinState: GroupCallJoinState.Joining,
|
joinState: GroupCallJoinState.Joining,
|
||||||
|
@ -952,6 +960,7 @@ describe('calling duck', () => {
|
||||||
const result = reducer(
|
const result = reducer(
|
||||||
stateWithGroupCall,
|
stateWithGroupCall,
|
||||||
getAction({
|
getAction({
|
||||||
|
callMode: CallMode.Group,
|
||||||
conversationId: 'fake-group-call-conversation-id',
|
conversationId: 'fake-group-call-conversation-id',
|
||||||
connectionState: GroupCallConnectionState.Connected,
|
connectionState: GroupCallConnectionState.Connected,
|
||||||
joinState: GroupCallJoinState.Joined,
|
joinState: GroupCallJoinState.Joined,
|
||||||
|
@ -1025,6 +1034,7 @@ describe('calling duck', () => {
|
||||||
const result = reducer(
|
const result = reducer(
|
||||||
state,
|
state,
|
||||||
getAction({
|
getAction({
|
||||||
|
callMode: CallMode.Group,
|
||||||
conversationId: 'fake-group-call-conversation-id',
|
conversationId: 'fake-group-call-conversation-id',
|
||||||
connectionState: GroupCallConnectionState.Connected,
|
connectionState: GroupCallConnectionState.Connected,
|
||||||
joinState: GroupCallJoinState.NotJoined,
|
joinState: GroupCallJoinState.NotJoined,
|
||||||
|
@ -1078,6 +1088,7 @@ describe('calling duck', () => {
|
||||||
const result = reducer(
|
const result = reducer(
|
||||||
state,
|
state,
|
||||||
getAction({
|
getAction({
|
||||||
|
callMode: CallMode.Group,
|
||||||
conversationId: 'fake-group-call-conversation-id',
|
conversationId: 'fake-group-call-conversation-id',
|
||||||
connectionState: GroupCallConnectionState.Connected,
|
connectionState: GroupCallConnectionState.Connected,
|
||||||
localDemuxId: 1,
|
localDemuxId: 1,
|
||||||
|
@ -1118,6 +1129,7 @@ describe('calling duck', () => {
|
||||||
const result = reducer(
|
const result = reducer(
|
||||||
stateWithGroupCall,
|
stateWithGroupCall,
|
||||||
getAction({
|
getAction({
|
||||||
|
callMode: CallMode.Group,
|
||||||
conversationId: 'fake-group-call-conversation-id',
|
conversationId: 'fake-group-call-conversation-id',
|
||||||
connectionState: GroupCallConnectionState.Connected,
|
connectionState: GroupCallConnectionState.Connected,
|
||||||
joinState: GroupCallJoinState.Joined,
|
joinState: GroupCallJoinState.Joined,
|
||||||
|
@ -1151,6 +1163,7 @@ describe('calling duck', () => {
|
||||||
const result = reducer(
|
const result = reducer(
|
||||||
stateWithActiveGroupCall,
|
stateWithActiveGroupCall,
|
||||||
getAction({
|
getAction({
|
||||||
|
callMode: CallMode.Group,
|
||||||
conversationId: 'another-fake-conversation-id',
|
conversationId: 'another-fake-conversation-id',
|
||||||
connectionState: GroupCallConnectionState.Connected,
|
connectionState: GroupCallConnectionState.Connected,
|
||||||
joinState: GroupCallJoinState.Joined,
|
joinState: GroupCallJoinState.Joined,
|
||||||
|
@ -1178,6 +1191,7 @@ describe('calling duck', () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.deepEqual(result.activeCallState, {
|
assert.deepEqual(result.activeCallState, {
|
||||||
|
callMode: CallMode.Group,
|
||||||
conversationId: 'fake-group-call-conversation-id',
|
conversationId: 'fake-group-call-conversation-id',
|
||||||
hasLocalAudio: true,
|
hasLocalAudio: true,
|
||||||
hasLocalVideo: false,
|
hasLocalVideo: false,
|
||||||
|
@ -1196,6 +1210,7 @@ describe('calling duck', () => {
|
||||||
const result = reducer(
|
const result = reducer(
|
||||||
stateWithActiveGroupCall,
|
stateWithActiveGroupCall,
|
||||||
getAction({
|
getAction({
|
||||||
|
callMode: CallMode.Group,
|
||||||
conversationId: 'fake-group-call-conversation-id',
|
conversationId: 'fake-group-call-conversation-id',
|
||||||
connectionState: GroupCallConnectionState.Connected,
|
connectionState: GroupCallConnectionState.Connected,
|
||||||
joinState: GroupCallJoinState.Joined,
|
joinState: GroupCallJoinState.Joined,
|
||||||
|
@ -1241,6 +1256,7 @@ describe('calling duck', () => {
|
||||||
const result = reducer(
|
const result = reducer(
|
||||||
state,
|
state,
|
||||||
getAction({
|
getAction({
|
||||||
|
callMode: CallMode.Group,
|
||||||
conversationId: 'fake-group-call-conversation-id',
|
conversationId: 'fake-group-call-conversation-id',
|
||||||
connectionState: GroupCallConnectionState.Connected,
|
connectionState: GroupCallConnectionState.Connected,
|
||||||
joinState: GroupCallJoinState.Joined,
|
joinState: GroupCallJoinState.Joined,
|
||||||
|
@ -1270,6 +1286,7 @@ describe('calling duck', () => {
|
||||||
const result = reducer(
|
const result = reducer(
|
||||||
state,
|
state,
|
||||||
getAction({
|
getAction({
|
||||||
|
callMode: CallMode.Group,
|
||||||
conversationId: 'fake-group-call-conversation-id',
|
conversationId: 'fake-group-call-conversation-id',
|
||||||
connectionState: GroupCallConnectionState.Connected,
|
connectionState: GroupCallConnectionState.Connected,
|
||||||
joinState: GroupCallJoinState.Joined,
|
joinState: GroupCallJoinState.Joined,
|
||||||
|
@ -1484,6 +1501,7 @@ describe('calling duck', () => {
|
||||||
|
|
||||||
it('adds reactions by timestamp', function (this: Mocha.Context) {
|
it('adds reactions by timestamp', function (this: Mocha.Context) {
|
||||||
const firstAction = getAction({
|
const firstAction = getAction({
|
||||||
|
callMode: CallMode.Group,
|
||||||
conversationId: 'fake-group-call-conversation-id',
|
conversationId: 'fake-group-call-conversation-id',
|
||||||
reactions: [
|
reactions: [
|
||||||
{
|
{
|
||||||
|
@ -1504,6 +1522,7 @@ describe('calling duck', () => {
|
||||||
const secondDate = new Date(NOW.getTime() + 1234);
|
const secondDate = new Date(NOW.getTime() + 1234);
|
||||||
this.sandbox.useFakeTimers({ now: secondDate });
|
this.sandbox.useFakeTimers({ now: secondDate });
|
||||||
const secondAction = getAction({
|
const secondAction = getAction({
|
||||||
|
callMode: CallMode.Group,
|
||||||
conversationId: 'fake-group-call-conversation-id',
|
conversationId: 'fake-group-call-conversation-id',
|
||||||
reactions: [
|
reactions: [
|
||||||
{
|
{
|
||||||
|
@ -1529,6 +1548,7 @@ describe('calling duck', () => {
|
||||||
|
|
||||||
it('sets multiple reactions with the same timestamp', () => {
|
it('sets multiple reactions with the same timestamp', () => {
|
||||||
const action = getAction({
|
const action = getAction({
|
||||||
|
callMode: CallMode.Group,
|
||||||
conversationId: 'fake-group-call-conversation-id',
|
conversationId: 'fake-group-call-conversation-id',
|
||||||
reactions: [
|
reactions: [
|
||||||
{
|
{
|
||||||
|
@ -1588,6 +1608,7 @@ describe('calling duck', () => {
|
||||||
|
|
||||||
it('adds a local copy', () => {
|
it('adds a local copy', () => {
|
||||||
const action = getAction({
|
const action = getAction({
|
||||||
|
callMode: CallMode.Group,
|
||||||
conversationId: 'fake-group-call-conversation-id',
|
conversationId: 'fake-group-call-conversation-id',
|
||||||
value: '❤️',
|
value: '❤️',
|
||||||
});
|
});
|
||||||
|
@ -1858,6 +1879,7 @@ describe('calling duck', () => {
|
||||||
isVideoCall: true,
|
isVideoCall: true,
|
||||||
});
|
});
|
||||||
assert.deepEqual(result.activeCallState, {
|
assert.deepEqual(result.activeCallState, {
|
||||||
|
callMode: CallMode.Direct,
|
||||||
conversationId: 'fake-conversation-id',
|
conversationId: 'fake-conversation-id',
|
||||||
hasLocalAudio: true,
|
hasLocalAudio: true,
|
||||||
hasLocalVideo: true,
|
hasLocalVideo: true,
|
||||||
|
@ -2151,6 +2173,7 @@ describe('calling duck', () => {
|
||||||
isVideoCall: false,
|
isVideoCall: false,
|
||||||
});
|
});
|
||||||
assert.deepEqual(result.activeCallState, {
|
assert.deepEqual(result.activeCallState, {
|
||||||
|
callMode: CallMode.Direct,
|
||||||
conversationId: 'fake-conversation-id',
|
conversationId: 'fake-conversation-id',
|
||||||
hasLocalAudio: true,
|
hasLocalAudio: true,
|
||||||
hasLocalVideo: false,
|
hasLocalVideo: false,
|
||||||
|
|
|
@ -983,6 +983,7 @@ describe('both/state/ducks/stories', () => {
|
||||||
digest: 'digest-1',
|
digest: 'digest-1',
|
||||||
size: 0,
|
size: 0,
|
||||||
},
|
},
|
||||||
|
isCallLink: false,
|
||||||
};
|
};
|
||||||
const messageAttributes = {
|
const messageAttributes = {
|
||||||
...getStoryMessage(storyId),
|
...getStoryMessage(storyId),
|
||||||
|
|
|
@ -62,6 +62,7 @@ describe('state/selectors/calling', () => {
|
||||||
const stateWithActiveDirectCall: CallingStateType = {
|
const stateWithActiveDirectCall: CallingStateType = {
|
||||||
...stateWithDirectCall,
|
...stateWithDirectCall,
|
||||||
activeCallState: {
|
activeCallState: {
|
||||||
|
callMode: CallMode.Direct,
|
||||||
conversationId: 'fake-direct-call-conversation-id',
|
conversationId: 'fake-direct-call-conversation-id',
|
||||||
hasLocalAudio: true,
|
hasLocalAudio: true,
|
||||||
hasLocalVideo: false,
|
hasLocalVideo: false,
|
||||||
|
|
|
@ -649,6 +649,10 @@ export type GroupCredentialsType = {
|
||||||
groupPublicParamsHex: string;
|
groupPublicParamsHex: string;
|
||||||
authCredentialPresentationHex: string;
|
authCredentialPresentationHex: string;
|
||||||
};
|
};
|
||||||
|
export type CallLinkAuthCredentialsType = {
|
||||||
|
callLinkPublicParamsHex: string;
|
||||||
|
authCredentialPresentationHex: string;
|
||||||
|
};
|
||||||
export type GetGroupLogOptionsType = Readonly<{
|
export type GetGroupLogOptionsType = Readonly<{
|
||||||
startVersion: number | undefined;
|
startVersion: number | undefined;
|
||||||
includeFirstState: boolean;
|
includeFirstState: boolean;
|
||||||
|
@ -798,6 +802,7 @@ export type GetGroupCredentialsOptionsType = Readonly<{
|
||||||
export type GetGroupCredentialsResultType = Readonly<{
|
export type GetGroupCredentialsResultType = Readonly<{
|
||||||
pni?: UntaggedPniString | null;
|
pni?: UntaggedPniString | null;
|
||||||
credentials: ReadonlyArray<GroupCredentialType>;
|
credentials: ReadonlyArray<GroupCredentialType>;
|
||||||
|
callLinkAuthCredentials: ReadonlyArray<GroupCredentialType>;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
const verifyServiceIdResponse = z.object({
|
const verifyServiceIdResponse = z.object({
|
||||||
|
@ -3114,10 +3119,6 @@ export function initialize({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type CredentialResponseType = {
|
|
||||||
credentials: Array<GroupCredentialType>;
|
|
||||||
};
|
|
||||||
|
|
||||||
async function getGroupCredentials({
|
async function getGroupCredentials({
|
||||||
startDayInMs,
|
startDayInMs,
|
||||||
endDayInMs,
|
endDayInMs,
|
||||||
|
@ -3132,7 +3133,7 @@ export function initialize({
|
||||||
'pniAsServiceId=true',
|
'pniAsServiceId=true',
|
||||||
httpType: 'GET',
|
httpType: 'GET',
|
||||||
responseType: 'json',
|
responseType: 'json',
|
||||||
})) as CredentialResponseType;
|
})) as GetGroupCredentialsResultType;
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
33
ts/types/CallLink.ts
Normal file
33
ts/types/CallLink.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
// Copyright 2024 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
import type { ReadonlyDeep } from 'type-fest';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import type { CallLinkRestrictions as RingRTCCallLinkRestrictions } from '@signalapp/ringrtc';
|
||||||
|
import type { ConversationType } from '../state/ducks/conversations';
|
||||||
|
|
||||||
|
export type CallLinkConversationType = ReadonlyDeep<
|
||||||
|
Omit<ConversationType, 'type'> & {
|
||||||
|
type: 'callLink';
|
||||||
|
storySendMode?: undefined;
|
||||||
|
acknowledgedGroupNameCollisions?: undefined;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
// Must match `CallLinkRestrictions` in @signalapp/ringrtc
|
||||||
|
export enum CallLinkRestrictions {
|
||||||
|
None = 0,
|
||||||
|
AdminApproval = 1,
|
||||||
|
Unknown = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const callLinkRestrictionsSchema = z.nativeEnum(
|
||||||
|
CallLinkRestrictions
|
||||||
|
) satisfies z.ZodType<RingRTCCallLinkRestrictions>;
|
||||||
|
|
||||||
|
export type CallLinkType = Readonly<{
|
||||||
|
roomId: string;
|
||||||
|
rootKey: string;
|
||||||
|
name: string;
|
||||||
|
restrictions: CallLinkRestrictions;
|
||||||
|
expiration: number;
|
||||||
|
}>;
|
|
@ -4,6 +4,7 @@
|
||||||
import type { AudioDevice, Reaction as CallReaction } from '@signalapp/ringrtc';
|
import type { AudioDevice, Reaction as CallReaction } from '@signalapp/ringrtc';
|
||||||
import type { ConversationType } from '../state/ducks/conversations';
|
import type { ConversationType } from '../state/ducks/conversations';
|
||||||
import type { AciString, ServiceIdString } from './ServiceId';
|
import type { AciString, ServiceIdString } from './ServiceId';
|
||||||
|
import type { CallLinkConversationType } from './CallLink';
|
||||||
|
|
||||||
export const MAX_CALLING_REACTIONS = 5;
|
export const MAX_CALLING_REACTIONS = 5;
|
||||||
export const CALLING_REACTIONS_LIFETIME = 4000;
|
export const CALLING_REACTIONS_LIFETIME = 4000;
|
||||||
|
@ -12,6 +13,7 @@ export const CALLING_REACTIONS_LIFETIME = 4000;
|
||||||
export enum CallMode {
|
export enum CallMode {
|
||||||
Direct = 'Direct',
|
Direct = 'Direct',
|
||||||
Group = 'Group',
|
Group = 'Group',
|
||||||
|
Adhoc = 'Adhoc',
|
||||||
}
|
}
|
||||||
|
|
||||||
// Speaker and Presentation mode have the same UI, but Presentation is only set
|
// Speaker and Presentation mode have the same UI, but Presentation is only set
|
||||||
|
@ -48,7 +50,7 @@ export type ActiveCallReaction = {
|
||||||
export type ActiveCallReactionsType = ReadonlyArray<ActiveCallReaction>;
|
export type ActiveCallReactionsType = ReadonlyArray<ActiveCallReaction>;
|
||||||
|
|
||||||
export type ActiveCallBaseType = {
|
export type ActiveCallBaseType = {
|
||||||
conversation: ConversationType;
|
conversation: CallingConversationType;
|
||||||
hasLocalAudio: boolean;
|
hasLocalAudio: boolean;
|
||||||
hasLocalVideo: boolean;
|
hasLocalVideo: boolean;
|
||||||
localAudioLevel: number;
|
localAudioLevel: number;
|
||||||
|
@ -85,7 +87,7 @@ export type ActiveDirectCallType = ActiveCallBaseType & {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ActiveGroupCallType = ActiveCallBaseType & {
|
export type ActiveGroupCallType = ActiveCallBaseType & {
|
||||||
callMode: CallMode.Group;
|
callMode: CallMode.Group | CallMode.Adhoc;
|
||||||
connectionState: GroupCallConnectionState;
|
connectionState: GroupCallConnectionState;
|
||||||
conversationsByDemuxId: ConversationsByDemuxIdType;
|
conversationsByDemuxId: ConversationsByDemuxIdType;
|
||||||
conversationsWithSafetyNumberChanges: Array<ConversationType>;
|
conversationsWithSafetyNumberChanges: Array<ConversationType>;
|
||||||
|
@ -199,3 +201,7 @@ export type ChangeIODevicePayloadType =
|
||||||
| { type: CallingDeviceType.CAMERA; selectedDevice: string }
|
| { type: CallingDeviceType.CAMERA; selectedDevice: string }
|
||||||
| { type: CallingDeviceType.MICROPHONE; selectedDevice: AudioDevice }
|
| { type: CallingDeviceType.MICROPHONE; selectedDevice: AudioDevice }
|
||||||
| { type: CallingDeviceType.SPEAKER; selectedDevice: AudioDevice };
|
| { type: CallingDeviceType.SPEAKER; selectedDevice: AudioDevice };
|
||||||
|
|
||||||
|
export type CallingConversationType =
|
||||||
|
| ConversationType
|
||||||
|
| CallLinkConversationType;
|
||||||
|
|
|
@ -9,7 +9,11 @@ import { maybeParseUrl } from '../util/url';
|
||||||
import { replaceEmojiWithSpaces } from '../util/emoji';
|
import { replaceEmojiWithSpaces } from '../util/emoji';
|
||||||
|
|
||||||
import type { AttachmentWithHydratedData } from './Attachment';
|
import type { AttachmentWithHydratedData } from './Attachment';
|
||||||
import { artAddStickersRoute, groupInvitesRoute } from '../util/signalRoutes';
|
import {
|
||||||
|
artAddStickersRoute,
|
||||||
|
groupInvitesRoute,
|
||||||
|
linkCallRoute,
|
||||||
|
} from '../util/signalRoutes';
|
||||||
|
|
||||||
export type LinkPreviewImage = AttachmentWithHydratedData;
|
export type LinkPreviewImage = AttachmentWithHydratedData;
|
||||||
|
|
||||||
|
@ -95,6 +99,11 @@ export function shouldLinkifyMessage(
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isCallLink(link = ''): boolean {
|
||||||
|
const url = maybeParseUrl(link);
|
||||||
|
return url?.protocol === 'https:' && linkCallRoute.isMatch(url);
|
||||||
|
}
|
||||||
|
|
||||||
export function isStickerPack(link = ''): boolean {
|
export function isStickerPack(link = ''): boolean {
|
||||||
const url = maybeParseUrl(link);
|
const url = maybeParseUrl(link);
|
||||||
return url?.protocol === 'https:' && artAddStickersRoute.isMatch(url);
|
return url?.protocol === 'https:' && artAddStickersRoute.isMatch(url);
|
||||||
|
|
|
@ -59,6 +59,7 @@ export const rendererConfigSchema = z.object({
|
||||||
registrationChallengeUrl: configRequiredStringSchema,
|
registrationChallengeUrl: configRequiredStringSchema,
|
||||||
serverPublicParams: configRequiredStringSchema,
|
serverPublicParams: configRequiredStringSchema,
|
||||||
serverTrustRoot: configRequiredStringSchema,
|
serverTrustRoot: configRequiredStringSchema,
|
||||||
|
genericServerPublicParams: configRequiredStringSchema,
|
||||||
serverUrl: configRequiredStringSchema,
|
serverUrl: configRequiredStringSchema,
|
||||||
sfuUrl: configRequiredStringSchema,
|
sfuUrl: configRequiredStringSchema,
|
||||||
storageUrl: configRequiredStringSchema,
|
storageUrl: configRequiredStringSchema,
|
||||||
|
|
1
ts/types/Storage.d.ts
vendored
1
ts/types/Storage.d.ts
vendored
|
@ -141,6 +141,7 @@ export type StorageAccessType = {
|
||||||
serverTimeSkew: number;
|
serverTimeSkew: number;
|
||||||
unidentifiedDeliveryIndicators: boolean;
|
unidentifiedDeliveryIndicators: boolean;
|
||||||
groupCredentials: ReadonlyArray<GroupCredentialType>;
|
groupCredentials: ReadonlyArray<GroupCredentialType>;
|
||||||
|
callLinkAuthCredentials: ReadonlyArray<GroupCredentialType>;
|
||||||
lastReceivedAtCounter: number;
|
lastReceivedAtCounter: number;
|
||||||
preferredReactionEmoji: ReadonlyArray<string>;
|
preferredReactionEmoji: ReadonlyArray<string>;
|
||||||
skinTone: number;
|
skinTone: number;
|
||||||
|
|
|
@ -20,6 +20,7 @@ export enum ToastType {
|
||||||
ConversationMarkedUnread = 'ConversationMarkedUnread',
|
ConversationMarkedUnread = 'ConversationMarkedUnread',
|
||||||
ConversationRemoved = 'ConversationRemoved',
|
ConversationRemoved = 'ConversationRemoved',
|
||||||
ConversationUnarchived = 'ConversationUnarchived',
|
ConversationUnarchived = 'ConversationUnarchived',
|
||||||
|
CopiedCallLink = 'CopiedCallLink',
|
||||||
CopiedUsername = 'CopiedUsername',
|
CopiedUsername = 'CopiedUsername',
|
||||||
CopiedUsernameLink = 'CopiedUsernameLink',
|
CopiedUsernameLink = 'CopiedUsernameLink',
|
||||||
DangerousFileType = 'DangerousFileType',
|
DangerousFileType = 'DangerousFileType',
|
||||||
|
@ -86,6 +87,7 @@ export type AnyToast =
|
||||||
| { toastType: ToastType.ConversationMarkedUnread }
|
| { toastType: ToastType.ConversationMarkedUnread }
|
||||||
| { toastType: ToastType.ConversationRemoved; parameters: { title: string } }
|
| { toastType: ToastType.ConversationRemoved; parameters: { title: string } }
|
||||||
| { toastType: ToastType.ConversationUnarchived }
|
| { toastType: ToastType.ConversationUnarchived }
|
||||||
|
| { toastType: ToastType.CopiedCallLink }
|
||||||
| { toastType: ToastType.CopiedUsername }
|
| { toastType: ToastType.CopiedUsername }
|
||||||
| { toastType: ToastType.CopiedUsernameLink }
|
| { toastType: ToastType.CopiedUsernameLink }
|
||||||
| { toastType: ToastType.DangerousFileType }
|
| { toastType: ToastType.DangerousFileType }
|
||||||
|
|
|
@ -9,6 +9,7 @@ type GenericLinkPreviewType<Image> = {
|
||||||
domain?: string;
|
domain?: string;
|
||||||
url: string;
|
url: string;
|
||||||
isStickerPack?: boolean;
|
isStickerPack?: boolean;
|
||||||
|
isCallLink: boolean;
|
||||||
image?: Readonly<Image>;
|
image?: Readonly<Image>;
|
||||||
date?: number;
|
date?: number;
|
||||||
};
|
};
|
||||||
|
|
|
@ -514,6 +514,9 @@ export function transitionCallHistory(
|
||||||
event,
|
event,
|
||||||
direction
|
direction
|
||||||
);
|
);
|
||||||
|
} else if (mode === CallMode.Adhoc) {
|
||||||
|
// TODO: DESKTOP-6653
|
||||||
|
strictAssert(false, 'cannot transitionCallHistory for adhoc calls yet');
|
||||||
} else {
|
} else {
|
||||||
throw missingCaseError(mode);
|
throw missingCaseError(mode);
|
||||||
}
|
}
|
||||||
|
|
10
ts/util/callLinkRootKeyToUrl.ts
Normal file
10
ts/util/callLinkRootKeyToUrl.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
// Copyright 2024 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
export function callLinkRootKeyToUrl(rootKey: string): string | undefined {
|
||||||
|
if (!rootKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `https://signal.link/call/#key=${rootKey}`;
|
||||||
|
}
|
73
ts/util/callLinks.ts
Normal file
73
ts/util/callLinks.ts
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
// Copyright 2024 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
import { CallLinkRootKey } from '@signalapp/ringrtc';
|
||||||
|
import { Aci } from '@signalapp/libsignal-client';
|
||||||
|
import type { CallLinkAuthCredentialPresentation } from './zkgroup';
|
||||||
|
import {
|
||||||
|
CallLinkAuthCredential,
|
||||||
|
CallLinkSecretParams,
|
||||||
|
GenericServerPublicParams,
|
||||||
|
} from './zkgroup';
|
||||||
|
import { getCheckedCallLinkAuthCredentialsForToday } from '../services/groupCredentialFetcher';
|
||||||
|
import * as durations from './durations';
|
||||||
|
import type { CallLinkConversationType, CallLinkType } from '../types/CallLink';
|
||||||
|
import type { LocalizerType } from '../types/Util';
|
||||||
|
|
||||||
|
export function getRoomIdFromRootKey(rootKey: CallLinkRootKey): string {
|
||||||
|
return rootKey.deriveRoomId().toString('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCallLinkRootKeyFromUrlKey(key: string): Uint8Array {
|
||||||
|
// Returns `Buffer` which inherits from `Uint8Array`
|
||||||
|
return CallLinkRootKey.parse(key).bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCallLinkAuthCredentialPresentation(
|
||||||
|
callLinkRootKey: CallLinkRootKey
|
||||||
|
): Promise<CallLinkAuthCredentialPresentation> {
|
||||||
|
const credentials = getCheckedCallLinkAuthCredentialsForToday(
|
||||||
|
'getCallLinkAuthCredentialPresentation'
|
||||||
|
);
|
||||||
|
const todaysCredentials = credentials.today.credential;
|
||||||
|
const credential = new CallLinkAuthCredential(
|
||||||
|
Buffer.from(todaysCredentials, 'base64')
|
||||||
|
);
|
||||||
|
|
||||||
|
const genericServerPublicParamsBase64 = window.getGenericServerPublicParams();
|
||||||
|
const genericServerPublicParams = new GenericServerPublicParams(
|
||||||
|
Buffer.from(genericServerPublicParamsBase64, 'base64')
|
||||||
|
);
|
||||||
|
|
||||||
|
const ourAci = window.textsecure.storage.user.getAci();
|
||||||
|
if (ourAci == null) {
|
||||||
|
throw new Error('Failed to get our ACI');
|
||||||
|
}
|
||||||
|
const userId = Aci.fromUuid(ourAci);
|
||||||
|
|
||||||
|
const callLinkSecretParams = CallLinkSecretParams.deriveFromRootKey(
|
||||||
|
callLinkRootKey.bytes
|
||||||
|
);
|
||||||
|
const presentation = credential.present(
|
||||||
|
userId,
|
||||||
|
credentials.today.redemptionTime / durations.SECOND,
|
||||||
|
genericServerPublicParams,
|
||||||
|
callLinkSecretParams
|
||||||
|
);
|
||||||
|
return presentation;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function callLinkToConversation(
|
||||||
|
callLink: CallLinkType,
|
||||||
|
i18n: LocalizerType
|
||||||
|
): CallLinkConversationType {
|
||||||
|
const { roomId, name } = callLink;
|
||||||
|
return {
|
||||||
|
id: roomId,
|
||||||
|
type: 'callLink',
|
||||||
|
isMe: false,
|
||||||
|
title: name || i18n('icu:calling__call-link-default-title'),
|
||||||
|
sharedGroupNames: [],
|
||||||
|
acceptedMessageRequest: true,
|
||||||
|
badges: [],
|
||||||
|
};
|
||||||
|
}
|
|
@ -128,6 +128,10 @@ export function getCallingNotificationText(
|
||||||
);
|
);
|
||||||
return getGroupCallNotificationText(groupCallEnded, callCreator, i18n);
|
return getGroupCallNotificationText(groupCallEnded, callCreator, i18n);
|
||||||
}
|
}
|
||||||
|
if (callHistory.mode === CallMode.Adhoc) {
|
||||||
|
// TODO: DESKTOP-6653
|
||||||
|
return null;
|
||||||
|
}
|
||||||
throw missingCaseError(callHistory.mode);
|
throw missingCaseError(callHistory.mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
8
ts/util/isAdhocCallingEnabled.ts
Normal file
8
ts/util/isAdhocCallingEnabled.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
// Copyright 2024 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import * as RemoteConfig from '../RemoteConfig';
|
||||||
|
|
||||||
|
export function isAdhocCallingEnabled(): boolean {
|
||||||
|
return Boolean(RemoteConfig.isEnabled('desktop.calling.adhoc'));
|
||||||
|
}
|
31
ts/util/isGroupOrAdhocCall.ts
Normal file
31
ts/util/isGroupOrAdhocCall.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
// Copyright 2024 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { CallMode } from '../types/Calling';
|
||||||
|
import type { ActiveCallType, ActiveGroupCallType } from '../types/Calling';
|
||||||
|
import type {
|
||||||
|
DirectCallStateType,
|
||||||
|
GroupCallStateType,
|
||||||
|
} from '../state/ducks/calling';
|
||||||
|
|
||||||
|
export function isGroupOrAdhocActiveCall(
|
||||||
|
activeCall: ActiveCallType | undefined
|
||||||
|
): activeCall is ActiveGroupCallType {
|
||||||
|
return Boolean(activeCall && isGroupOrAdhocCallMode(activeCall.callMode));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isGroupOrAdhocCallMode(
|
||||||
|
callMode: CallMode | undefined | null
|
||||||
|
): callMode is CallMode.Group | CallMode.Adhoc {
|
||||||
|
return callMode === CallMode.Group || callMode === CallMode.Adhoc;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isGroupOrAdhocCallState(
|
||||||
|
callState: DirectCallStateType | GroupCallStateType | undefined
|
||||||
|
): callState is GroupCallStateType {
|
||||||
|
return Boolean(
|
||||||
|
callState &&
|
||||||
|
(callState.callMode === CallMode.Group ||
|
||||||
|
callState.callMode === CallMode.Adhoc)
|
||||||
|
);
|
||||||
|
}
|
|
@ -16,6 +16,9 @@ const UUID_OR_STORY_ID_PATTERN =
|
||||||
/[0-9A-F]{8}-[0-9A-F]{4}-[04][0-9A-F]{3}-[089AB][0-9A-F]{3}-[0-9A-F]{9}([0-9A-F]{3})/gi;
|
/[0-9A-F]{8}-[0-9A-F]{4}-[04][0-9A-F]{3}-[089AB][0-9A-F]{3}-[0-9A-F]{9}([0-9A-F]{3})/gi;
|
||||||
const GROUP_ID_PATTERN = /(group\()([^)]+)(\))/g;
|
const GROUP_ID_PATTERN = /(group\()([^)]+)(\))/g;
|
||||||
const GROUP_V2_ID_PATTERN = /(groupv2\()([^=)]+)(=?=?\))/g;
|
const GROUP_V2_ID_PATTERN = /(groupv2\()([^=)]+)(=?=?\))/g;
|
||||||
|
const CALL_LINK_ROOM_ID_PATTERN = /[0-9A-F]{61}([0-9A-F]{3})/gi;
|
||||||
|
const CALL_LINK_ROOT_KEY_PATTERN =
|
||||||
|
/([A-Z]{4})-[A-Z]{4}-[A-Z]{4}-[A-Z]{4}-[A-Z]{4}-[A-Z]{4}-[A-Z]{4}-[A-Z]{4}/gi;
|
||||||
const REDACTION_PLACEHOLDER = '[REDACTED]';
|
const REDACTION_PLACEHOLDER = '[REDACTED]';
|
||||||
|
|
||||||
export type RedactFunction = (value: string) => string;
|
export type RedactFunction = (value: string) => string;
|
||||||
|
@ -125,6 +128,22 @@ export const redactGroupIds = (text: string): string => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const redactCallLinkRoomIds = (text: string): string => {
|
||||||
|
if (!isString(text)) {
|
||||||
|
throw new TypeError("'text' must be a string");
|
||||||
|
}
|
||||||
|
|
||||||
|
return text.replace(CALL_LINK_ROOM_ID_PATTERN, `${REDACTION_PLACEHOLDER}$1`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const redactCallLinkRootKeys = (text: string): string => {
|
||||||
|
if (!isString(text)) {
|
||||||
|
throw new TypeError("'text' must be a string");
|
||||||
|
}
|
||||||
|
|
||||||
|
return text.replace(CALL_LINK_ROOT_KEY_PATTERN, `${REDACTION_PLACEHOLDER}$1`);
|
||||||
|
};
|
||||||
|
|
||||||
const createRedactSensitivePaths = (
|
const createRedactSensitivePaths = (
|
||||||
paths: ReadonlyArray<string>
|
paths: ReadonlyArray<string>
|
||||||
): RedactFunction => {
|
): RedactFunction => {
|
||||||
|
@ -146,7 +165,9 @@ export const redactAll: RedactFunction = compose(
|
||||||
(text: string) => redactSensitivePaths(text),
|
(text: string) => redactSensitivePaths(text),
|
||||||
redactGroupIds,
|
redactGroupIds,
|
||||||
redactPhoneNumbers,
|
redactPhoneNumbers,
|
||||||
redactUuids
|
redactUuids,
|
||||||
|
redactCallLinkRoomIds,
|
||||||
|
redactCallLinkRootKeys
|
||||||
);
|
);
|
||||||
|
|
||||||
const removeNewlines: RedactFunction = text => text.replace(/\r?\n|\r/g, '');
|
const removeNewlines: RedactFunction = text => text.replace(/\r?\n|\r/g, '');
|
||||||
|
|
1
ts/window.d.ts
vendored
1
ts/window.d.ts
vendored
|
@ -193,6 +193,7 @@ declare global {
|
||||||
getHostName: () => string;
|
getHostName: () => string;
|
||||||
getInteractionMode: () => 'mouse' | 'keyboard';
|
getInteractionMode: () => 'mouse' | 'keyboard';
|
||||||
getServerPublicParams: () => string;
|
getServerPublicParams: () => string;
|
||||||
|
getGenericServerPublicParams: () => string;
|
||||||
getSfuUrl: () => string;
|
getSfuUrl: () => string;
|
||||||
getSocketStatus: () => SocketStatus;
|
getSocketStatus: () => SocketStatus;
|
||||||
getSyncRequest: (timeoutMillis?: number) => SyncRequest;
|
getSyncRequest: (timeoutMillis?: number) => SyncRequest;
|
||||||
|
|
|
@ -21,6 +21,7 @@ import type {
|
||||||
NotificationClickData,
|
NotificationClickData,
|
||||||
WindowsNotificationData,
|
WindowsNotificationData,
|
||||||
} from '../../services/notifications';
|
} from '../../services/notifications';
|
||||||
|
import { isAdhocCallingEnabled } from '../../util/isAdhocCallingEnabled';
|
||||||
|
|
||||||
// It is important to call this as early as possible
|
// It is important to call this as early as possible
|
||||||
window.i18n = SignalContext.i18n;
|
window.i18n = SignalContext.i18n;
|
||||||
|
@ -52,6 +53,7 @@ window.getBuildExpiration = () => config.buildExpiration;
|
||||||
window.getHostName = () => config.hostname;
|
window.getHostName = () => config.hostname;
|
||||||
window.getServerTrustRoot = () => config.serverTrustRoot;
|
window.getServerTrustRoot = () => config.serverTrustRoot;
|
||||||
window.getServerPublicParams = () => config.serverPublicParams;
|
window.getServerPublicParams = () => config.serverPublicParams;
|
||||||
|
window.getGenericServerPublicParams = () => config.genericServerPublicParams;
|
||||||
window.getSfuUrl = () => config.sfuUrl;
|
window.getSfuUrl = () => config.sfuUrl;
|
||||||
window.isBehindProxy = () => Boolean(config.proxyUrl);
|
window.isBehindProxy = () => Boolean(config.proxyUrl);
|
||||||
|
|
||||||
|
@ -329,6 +331,19 @@ ipc.on('start-call-lobby', (_event, { conversationId }) => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipc.on('start-call-link', (_event, { key }) => {
|
||||||
|
if (isAdhocCallingEnabled()) {
|
||||||
|
window.reduxActions?.calling?.startCallLinkLobby({
|
||||||
|
rootKey: key,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const { unknownSignalLink } = window.Events;
|
||||||
|
if (unknownSignalLink) {
|
||||||
|
unknownSignalLink();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ipc.on('show-window', () => {
|
ipc.on('show-window', () => {
|
||||||
window.IPC.showWindow();
|
window.IPC.showWindow();
|
||||||
});
|
});
|
||||||
|
|
|
@ -3973,10 +3973,10 @@
|
||||||
type-fest "^3.5.0"
|
type-fest "^3.5.0"
|
||||||
uuid "^8.3.0"
|
uuid "^8.3.0"
|
||||||
|
|
||||||
"@signalapp/mock-server@5.0.1":
|
"@signalapp/mock-server@5.1.0":
|
||||||
version "5.0.1"
|
version "5.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@signalapp/mock-server/-/mock-server-5.0.1.tgz#648eb44b91f1ecbb0b3dc1791101fb5d67ac0e73"
|
resolved "https://registry.yarnpkg.com/@signalapp/mock-server/-/mock-server-5.1.0.tgz#d561f3553cb744fee3f5b29686603c1aa35627c7"
|
||||||
integrity sha512-KuaHznpxubFib9LJiIP6lT8Oynuni4yzrItetxVwN9YPMTlFa0TYvaf+InODnH7tnM1bY1NCku9SYy+0H2ejMQ==
|
integrity sha512-92J4ZeplNJ2HnSPfZRw26uJ8OMnPy053pJsWai3nZbcnbjgGgNbkJPEW8c1dz80Fex97/zcgqhEy8v3vr3LjEg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@signalapp/libsignal-client" "^0.39.2"
|
"@signalapp/libsignal-client" "^0.39.2"
|
||||||
debug "^4.3.2"
|
debug "^4.3.2"
|
||||||
|
|
Loading…
Reference in a new issue