diff --git a/.prettierignore b/.prettierignore index 9f00c2eb9..a5e570d31 100644 --- a/.prettierignore +++ b/.prettierignore @@ -27,8 +27,9 @@ js/curve/** js/Mp3LameEncoder.min.js js/WebAudioRecorderMp3.js -# Test fixtures -test/fixtures.js +# Assets +/images/ +/fixtures/ # Github workflows .github/** diff --git a/ACKNOWLEDGMENTS.md b/ACKNOWLEDGMENTS.md index e13704796..3d7911b14 100644 --- a/ACKNOWLEDGMENTS.md +++ b/ACKNOWLEDGMENTS.md @@ -5,6 +5,55 @@ Signal Desktop makes use of the following open source projects. +## @evanhahn/lottie-web-light + + The MIT License (MIT) + + Copyright (c) 2022 Evan Hahn + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + + ************ + + Original lottie-web license: + The MIT License (MIT) + + Copyright (c) 2015 Bodymovin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + ## @popperjs/core License: MIT diff --git a/fixtures/lottie-loader-by-lucas-bariani.json b/fixtures/lottie-loader-by-lucas-bariani.json new file mode 100644 index 000000000..a6346c1a4 --- /dev/null +++ b/fixtures/lottie-loader-by-lucas-bariani.json @@ -0,0 +1 @@ +{"v":"5.8.1","fr":29.9700012207031,"ip":0,"op":60.0000024438501,"w":124,"h":124,"nm":"loader","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":60.0000024438501,"s":[360]}],"ix":10},"p":{"a":0,"k":[62,62,0],"ix":2,"l":2},"a":{"a":0,"k":[0.576,4.576,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[89.152,89.152],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[50.1]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":30.338,"s":[70.1]},{"t":60.0000024438501,"s":[50.1]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[49.9]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":30.338,"s":[29.9]},{"t":60.0000024438501,"s":[49.9]}],"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":60.0000024438501,"s":[360]}],"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.40000000596,0.349019616842,0.890196084976,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":25,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0.576,4.576],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":90.0000036657751,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":60.0000024438501,"s":[360]}],"ix":10},"p":{"a":0,"k":[62,62,0],"ix":2,"l":2},"a":{"a":0,"k":[0.576,4.576,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[89.152,89.152],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[50.1]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":30.338,"s":[70.1]},{"t":60.0000024438501,"s":[50.1]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[49.9]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":30.338,"s":[19.9]},{"t":60.0000024438501,"s":[49.9]}],"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":60.0000024438501,"s":[360]}],"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.337254911661,0.811764717102,0.490196079016,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":24,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0.576,4.576],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":90.0000036657751,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Shape Layer 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":60.0000024438501,"s":[360]}],"ix":10},"p":{"a":0,"k":[62,62,0],"ix":2,"l":2},"a":{"a":0,"k":[0.576,4.576,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[89.152,89.152],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[50.1]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":30.338,"s":[70.1]},{"t":60.0000024438501,"s":[50.1]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[49.9]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":30.338,"s":[9.9]},{"t":60.0000024438501,"s":[49.9]}],"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":60.0000024438501,"s":[360]}],"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.956862747669,0.764705896378,0.015686275437,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":23,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0.576,4.576],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":90.0000036657751,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":60.0000024438501,"s":[360]}],"ix":10},"p":{"a":0,"k":[62,62,0],"ix":2,"l":2},"a":{"a":0,"k":[0.576,4.576,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[89.152,89.152],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[50.1]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":30.338,"s":[70.1]},{"t":60.0000024438501,"s":[50.1]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[49.9]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":30.338,"s":[0]},{"t":60.0000024438501,"s":[49.9]}],"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":60.0000024438501,"s":[360]}],"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.035294119269,0.78823530674,0.96862745285,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":22,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0.576,4.576],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":90.0000036657751,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/images/lottie-animations/CallingSpeakingIndicator.json b/images/lottie-animations/CallingSpeakingIndicator.json new file mode 100644 index 000000000..8911ba99c --- /dev/null +++ b/images/lottie-animations/CallingSpeakingIndicator.json @@ -0,0 +1 @@ +{"v":"5.5.10","fr":60,"ip":0,"op":60,"w":20,"h":20,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Bars","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[10,10,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[-0.828,0],[0,0.828],[0,0],[0.828,0],[0,-0.828]],"o":[[0,0.828],[0.828,0],[0,0],[0,-0.828],[-0.828,0],[0,0]],"v":[[-1.5,0],[0,1.5],[1.5,0],[1.5,0],[0,-1.5],[-1.5,0]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":15,"s":[{"i":[[0,0],[-0.828,0],[0,0.828],[0,0],[0.828,0],[0,-0.828]],"o":[[0,0.828],[0.828,0],[0,0],[0,-0.828],[-0.828,0],[0,0]],"v":[[-1.5,6.5],[0,8],[1.5,6.5],[1.5,-6.5],[0,-8],[-1.5,-6.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0,0],[-0.828,0],[0,0.828],[0,0],[0.828,0],[0,-0.828]],"o":[[0,0.828],[0.828,0],[0,0],[0,-0.828],[-0.828,0],[0,0]],"v":[[-1.5,0],[0,1.5],[1.5,0],[1.5,0],[0,-1.5],[-1.5,0]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":45,"s":[{"i":[[0,0],[-0.828,0],[0,0.828],[0,0],[0.828,0],[0,-0.828]],"o":[[0,0.828],[0.828,0],[0,0],[0,-0.828],[-0.828,0],[0,0]],"v":[[-1.5,2.5],[0,4],[1.5,2.5],[1.5,-2.5],[0,-4],[-1.5,-2.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":60,"s":[{"i":[[0,0],[-0.828,0],[0,0.828],[0,0],[0.828,0],[0,-0.828]],"o":[[0,0.828],[0.828,0],[0,0],[0,-0.828],[-0.828,0],[0,0]],"v":[[-1.5,0],[0,1.5],[1.5,0],[1.5,0],[0,-1.5],[-1.5,0]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":65,"s":[{"i":[[0,0],[-0.828,0],[0,0.828],[0,0],[0.828,0],[0,-0.828]],"o":[[0,0.828],[0.828,0],[0,0],[0,-0.828],[-0.828,0],[0,0]],"v":[[-1.5,2.5],[0,4],[1.5,2.5],[1.5,-2.5],[0,-4],[-1.5,-2.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":72,"s":[{"i":[[0,0],[-0.828,0],[0,0.828],[0,0],[0.828,0],[0,-0.828]],"o":[[0,0.828],[0.828,0],[0,0],[0,-0.828],[-0.828,0],[0,0]],"v":[[-1.5,4.5],[0,6],[1.5,4.5],[1.5,-4.5],[0,-6],[-1.5,-4.5]],"c":true}]},{"t":77,"s":[{"i":[[0,0],[-0.828,0],[0,0.828],[0,0],[0.828,0],[0,-0.828]],"o":[[0,0.828],[0.828,0],[0,0],[0,-0.828],[-0.828,0],[0,0]],"v":[[-1.5,2.5],[0,4],[1.5,2.5],[1.5,-2.5],[0,-4],[-1.5,-2.5]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[-0.828,0],[0,0.828],[0,0],[0.828,0],[0,-0.828]],"o":[[0,0.828],[0.828,0],[0,0],[0,-0.828],[-0.828,0],[0,0]],"v":[[-8,0],[-6.5,1.5],[-5,0],[-5,0],[-6.5,-1.5],[-8,0]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":15,"s":[{"i":[[0,0],[-0.828,0],[0,0.828],[0,0],[0.828,0],[0,-0.828]],"o":[[0,0.828],[0.828,0],[0,0],[0,-0.828],[-0.828,0],[0,0]],"v":[[-8.012,4.5],[-6.512,6],[-5.012,4.5],[-5.012,-4.5],[-6.512,-6],[-8.012,-4.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0,0],[-0.828,0],[0,0.828],[0,0],[0.828,0],[0,-0.828]],"o":[[0,0.828],[0.828,0],[0,0],[0,-0.828],[-0.828,0],[0,0]],"v":[[-8,0],[-6.5,1.5],[-5,0],[-5,0],[-6.5,-1.5],[-8,0]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":45,"s":[{"i":[[0,0],[-0.828,0],[0,0.828],[0,0],[0.828,0],[0,-0.828]],"o":[[0,0.828],[0.828,0],[0,0],[0,-0.828],[-0.828,0],[0,0]],"v":[[-8.012,4.5],[-6.512,6],[-5.012,4.5],[-5.012,-4.5],[-6.512,-6],[-8.012,-4.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":60,"s":[{"i":[[0,0],[-0.828,0],[0,0.828],[0,0],[0.828,0],[0,-0.828]],"o":[[0,0.828],[0.828,0],[0,0],[0,-0.828],[-0.828,0],[0,0]],"v":[[-8,0],[-6.5,1.5],[-5,0],[-5,0],[-6.5,-1.5],[-8,0]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":65,"s":[{"i":[[0,0],[-0.828,0],[0,0.828],[0,0],[0.828,0],[0,-0.828]],"o":[[0,0.828],[0.828,0],[0,0],[0,-0.828],[-0.828,0],[0,0]],"v":[[-8,0],[-6.5,1.5],[-5,0],[-5,0],[-6.5,-1.5],[-8,0]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":72,"s":[{"i":[[0,0],[-0.828,0],[0,0.828],[0,0],[0.828,0],[0,-0.828]],"o":[[0,0.828],[0.828,0],[0,0],[0,-0.828],[-0.828,0],[0,0]],"v":[[-8.012,2.5],[-6.512,4],[-5.012,2.5],[-5.012,-2.5],[-6.512,-4],[-8.012,-2.5]],"c":true}]},{"t":77,"s":[{"i":[[0,0],[-0.828,0],[0,0.828],[0,0],[0.828,0],[0,-0.828]],"o":[[0,0.828],[0.828,0],[0,0],[0,-0.828],[-0.828,0],[0,0]],"v":[[-8.012,4.5],[-6.512,6],[-5.012,4.5],[-5.012,-4.5],[-6.512,-6],[-8.012,-4.5]],"c":true}]}],"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[-0.828,0],[0,-0.828],[0,0],[0.828,0],[0,0.828]],"o":[[0,-0.828],[0.828,0],[0,0],[0,0.828],[-0.828,0],[0,0]],"v":[[5,0],[6.5,-1.5],[8,0],[8,0],[6.5,1.5],[5,0]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":15,"s":[{"i":[[0,0],[-0.828,0],[0,-0.828],[0,0],[0.828,0],[0,0.828]],"o":[[0,-0.828],[0.828,0],[0,0],[0,0.828],[-0.828,0],[0,0]],"v":[[4.988,-4.5],[6.488,-6],[7.988,-4.5],[7.988,4.5],[6.488,6],[4.988,4.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0,0],[-0.828,0],[0,-0.828],[0,0],[0.828,0],[0,0.828]],"o":[[0,-0.828],[0.828,0],[0,0],[0,0.828],[-0.828,0],[0,0]],"v":[[5,0],[6.5,-1.5],[8,0],[8,0],[6.5,1.5],[5,0]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":45,"s":[{"i":[[0,0],[-0.828,0],[0,-0.828],[0,0],[0.828,0],[0,0.828]],"o":[[0,-0.828],[0.828,0],[0,0],[0,0.828],[-0.828,0],[0,0]],"v":[[4.988,-4.5],[6.488,-6],[7.988,-4.5],[7.988,4.5],[6.488,6],[4.988,4.5]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":60,"s":[{"i":[[0,0],[-0.828,0],[0,-0.828],[0,0],[0.828,0],[0,0.828]],"o":[[0,-0.828],[0.828,0],[0,0],[0,0.828],[-0.828,0],[0,0]],"v":[[5,0],[6.5,-1.5],[8,0],[8,0],[6.5,1.5],[5,0]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":65,"s":[{"i":[[0,0],[-0.828,0],[0,-0.828],[0,0],[0.828,0],[0,0.828]],"o":[[0,-0.828],[0.828,0],[0,0],[0,0.828],[-0.828,0],[0,0]],"v":[[5,0],[6.5,-1.5],[8,0],[8,0],[6.5,1.5],[5,0]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":72,"s":[{"i":[[0,0],[-0.828,0],[0,-0.828],[0,0],[0.828,0],[0,0.828]],"o":[[0,-0.828],[0.828,0],[0,0],[0,0.828],[-0.828,0],[0,0]],"v":[[4.988,-2.5],[6.488,-4],[7.988,-2.5],[7.988,2.5],[6.488,4],[4.988,2.5]],"c":true}]},{"t":77,"s":[{"i":[[0,0],[-0.828,0],[0,-0.828],[0,0],[0.828,0],[0,0.828]],"o":[[0,-0.828],[0.828,0],[0,0],[0,0.828],[-0.828,0],[0,0]],"v":[[4.988,-4.5],[6.488,-6],[7.988,-4.5],[7.988,4.5],[6.488,6],[4.988,4.5]],"c":true}]}],"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":90,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/package.json b/package.json index 597addef6..84b794271 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "fs-xattr": "0.3.0" }, "dependencies": { + "@evanhahn/lottie-web-light": "5.8.1", "@popperjs/core": "2.9.2", "@react-spring/web": "9.4.1", "@signalapp/signal-client": "0.11.1", @@ -163,7 +164,7 @@ "redux-ts-utils": "3.2.2", "reselect": "4.1.2", "rimraf": "2.6.2", - "ringrtc": "https://github.com/signalapp/signal-ringrtc-node.git#f22009252bd3742f5b8a2761fe8f9c76a3bbc11d", + "ringrtc": "https://github.com/signalapp/signal-ringrtc-node.git#561484a82f75f64391da5ce9f48217db30e9ba4b", "rotating-file-stream": "2.1.5", "sanitize.css": "11.0.0", "semver": "5.4.1", diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index 686e04d40..fe439f6b3 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -4208,20 +4208,6 @@ button.module-image__border-overlay:focus { visibility: hidden; white-space: nowrap; } - - &--audio-muted::after { - $size: 14px; - @include color-svg( - '../images/icons/v2/mic-off-solid-28.svg', - $color-white - ); - content: ''; - height: $size; - min-width: $size; - right: 6px; - width: $size; - z-index: $z-index-base; - } } &:hover { diff --git a/stylesheets/components/CallingAudioIndicator.scss b/stylesheets/components/CallingAudioIndicator.scss new file mode 100644 index 000000000..9b403d657 --- /dev/null +++ b/stylesheets/components/CallingAudioIndicator.scss @@ -0,0 +1,14 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +.CallingAudioIndicator { + $size: 14px; + height: $size; + min-width: $size; + width: $size; + z-index: $z-index-base; + + &--muted { + @include color-svg('../images/icons/v2/mic-off-solid-28.svg', $color-white); + } +} diff --git a/stylesheets/manifest.scss b/stylesheets/manifest.scss index 041e1d0c1..e7f1c4e47 100644 --- a/stylesheets/manifest.scss +++ b/stylesheets/manifest.scss @@ -37,6 +37,7 @@ @import './components/BadgeSustainerInstructionsDialog.scss'; @import './components/BetterAvatarBubble.scss'; @import './components/Button.scss'; +@import './components/CallingAudioIndicator.scss'; @import './components/CallingButton.scss'; @import './components/CallingLobby.scss'; @import './components/CallingLobbyJoinButton.scss'; diff --git a/ts/components/CallManager.stories.tsx b/ts/components/CallManager.stories.tsx index e79075502..f6f53e2e8 100644 --- a/ts/components/CallManager.stories.tsx +++ b/ts/components/CallManager.stories.tsx @@ -1,4 +1,4 @@ -// Copyright 2020-2021 Signal Messenger, LLC +// Copyright 2020-2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import * as React from 'react'; @@ -144,6 +144,7 @@ story.add('Ongoing Group Call', () => ( groupMembers: [], peekedParticipants: [], remoteParticipants: [], + speakingDemuxIds: new Set(), }, })} /> @@ -218,6 +219,7 @@ story.add('Group call - Safety Number Changed', () => ( groupMembers: [], peekedParticipants: [], remoteParticipants: [], + speakingDemuxIds: new Set(), }, })} /> diff --git a/ts/components/CallManager.tsx b/ts/components/CallManager.tsx index 0a67d214c..cbe40b638 100644 --- a/ts/components/CallManager.tsx +++ b/ts/components/CallManager.tsx @@ -1,4 +1,4 @@ -// Copyright 2020-2021 Signal Messenger, LLC +// Copyright 2020-2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import React, { useCallback, useEffect } from 'react'; diff --git a/ts/components/CallScreen.stories.tsx b/ts/components/CallScreen.stories.tsx index 245b2d09f..37624fde5 100644 --- a/ts/components/CallScreen.stories.tsx +++ b/ts/components/CallScreen.stories.tsx @@ -1,4 +1,4 @@ -// Copyright 2020-2021 Signal Messenger, LLC +// Copyright 2020-2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import * as React from 'react'; @@ -103,6 +103,7 @@ const createActiveGroupCallProp = (overrideProps: GroupCallOverrideProps) => ({ peekedParticipants: overrideProps.peekedParticipants || overrideProps.remoteParticipants || [], remoteParticipants: overrideProps.remoteParticipants || [], + speakingDemuxIds: new Set(), }); const createActiveCallProp = ( diff --git a/ts/components/CallScreen.tsx b/ts/components/CallScreen.tsx index 5a1d7dcd3..4c93669f3 100644 --- a/ts/components/CallScreen.tsx +++ b/ts/components/CallScreen.tsx @@ -1,4 +1,4 @@ -// Copyright 2020-2021 Signal Messenger, LLC +// Copyright 2020-2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { ReactNode } from 'react'; @@ -299,6 +299,7 @@ export const CallScreen: React.FC = ({ isInSpeakerView={isInSpeakerView} remoteParticipants={activeCall.remoteParticipants} setGroupCallVideoRequest={setGroupCallVideoRequest} + speakingDemuxIds={activeCall.speakingDemuxIds} /> ); break; diff --git a/ts/components/CallingAudioIndicator.tsx b/ts/components/CallingAudioIndicator.tsx new file mode 100644 index 000000000..31487b571 --- /dev/null +++ b/ts/components/CallingAudioIndicator.tsx @@ -0,0 +1,30 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import type { ReactElement } from 'react'; +import React from 'react'; +import animationData from '../../images/lottie-animations/CallingSpeakingIndicator.json'; +import { Lottie } from './Lottie'; + +export function CallingAudioIndicator({ + hasRemoteAudio, + isSpeaking, +}: Readonly<{ + hasRemoteAudio: boolean; + isSpeaking: boolean; +}>): ReactElement { + if (!hasRemoteAudio) { + return ( +
+ ); + } + + if (isSpeaking) { + return ( + + ); + } + + // Render an empty spacer so that names don't move around. + return
; +} diff --git a/ts/components/CallingPip.stories.tsx b/ts/components/CallingPip.stories.tsx index b36bf924a..6f520981b 100644 --- a/ts/components/CallingPip.stories.tsx +++ b/ts/components/CallingPip.stories.tsx @@ -1,4 +1,4 @@ -// Copyright 2020-2021 Signal Messenger, LLC +// Copyright 2020-2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import * as React from 'react'; @@ -119,6 +119,7 @@ story.add('Group Call', () => { deviceCount: 0, peekedParticipants: [], remoteParticipants: [], + speakingDemuxIds: new Set(), }, }); return ; diff --git a/ts/components/CallingPip.tsx b/ts/components/CallingPip.tsx index 800790059..3fcf9316c 100644 --- a/ts/components/CallingPip.tsx +++ b/ts/components/CallingPip.tsx @@ -1,4 +1,4 @@ -// Copyright 2020-2021 Signal Messenger, LLC +// Copyright 2020-2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import React from 'react'; diff --git a/ts/components/CallingPipRemoteVideo.tsx b/ts/components/CallingPipRemoteVideo.tsx index 2a34b5529..6322c9d36 100644 --- a/ts/components/CallingPipRemoteVideo.tsx +++ b/ts/components/CallingPipRemoteVideo.tsx @@ -1,4 +1,4 @@ -// Copyright 2020-2021 Signal Messenger, LLC +// Copyright 2020-2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import React, { useMemo, useEffect } from 'react'; diff --git a/ts/components/GroupCallOverflowArea.stories.tsx b/ts/components/GroupCallOverflowArea.stories.tsx index 69cdd8aba..8fb5abf64 100644 --- a/ts/components/GroupCallOverflowArea.stories.tsx +++ b/ts/components/GroupCallOverflowArea.stories.tsx @@ -1,4 +1,4 @@ -// Copyright 2021 Signal Messenger, LLC +// Copyright 2021-2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { FC } from 'react'; @@ -39,6 +39,7 @@ const defaultProps = { getGroupCallVideoFrameSource: fakeGetGroupCallVideoFrameSource, i18n, onParticipantVisibilityChanged: action('onParticipantVisibilityChanged'), + speakingDemuxIds: new Set(), }; // This component is usually rendered on a call screen. diff --git a/ts/components/GroupCallOverflowArea.tsx b/ts/components/GroupCallOverflowArea.tsx index e54abdc03..55290fde9 100644 --- a/ts/components/GroupCallOverflowArea.tsx +++ b/ts/components/GroupCallOverflowArea.tsx @@ -1,4 +1,4 @@ -// Copyright 2021 Signal Messenger, LLC +// Copyright 2021-2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { FC, ReactElement } from 'react'; @@ -24,6 +24,7 @@ type PropsType = { isVisible: boolean ) => unknown; overflowedParticipants: ReadonlyArray; + speakingDemuxIds: Set; }; export const GroupCallOverflowArea: FC = ({ @@ -32,6 +33,7 @@ export const GroupCallOverflowArea: FC = ({ i18n, onParticipantVisibilityChanged, overflowedParticipants, + speakingDemuxIds, }) => { const overflowRef = useRef(null); const [overflowScrollTop, setOverflowScrollTop] = useState(0); @@ -114,6 +116,7 @@ export const GroupCallOverflowArea: FC = ({ getFrameBuffer={getFrameBuffer} getGroupCallVideoFrameSource={getGroupCallVideoFrameSource} i18n={i18n} + isSpeaking={speakingDemuxIds.has(remoteParticipant.demuxId)} onVisibilityChanged={onParticipantVisibilityChanged} width={OVERFLOW_PARTICIPANT_WIDTH} height={Math.floor( diff --git a/ts/components/GroupCallRemoteParticipant.stories.tsx b/ts/components/GroupCallRemoteParticipant.stories.tsx index 733eff3c7..2dc516617 100644 --- a/ts/components/GroupCallRemoteParticipant.stories.tsx +++ b/ts/components/GroupCallRemoteParticipant.stories.tsx @@ -1,4 +1,4 @@ -// Copyright 2020-2021 Signal Messenger, LLC +// Copyright 2020-2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import * as React from 'react'; @@ -36,6 +36,7 @@ const createProps = ( // eslint-disable-next-line @typescript-eslint/no-explicit-any getGroupCallVideoFrameSource: noop as any, i18n, + isSpeaking: false, remoteParticipant: { demuxId: 123, hasRemoteAudio: false, @@ -51,6 +52,7 @@ const createProps = ( }), }, ...overrideProps, + ...(overrideProps.isInPip ? {} : { isSpeaking: false }), }); const story = storiesOf('Components/GroupCallRemoteParticipant', module); diff --git a/ts/components/GroupCallRemoteParticipant.tsx b/ts/components/GroupCallRemoteParticipant.tsx index 1ab5bbf07..543fdd530 100644 --- a/ts/components/GroupCallRemoteParticipant.tsx +++ b/ts/components/GroupCallRemoteParticipant.tsx @@ -1,4 +1,4 @@ -// Copyright 2020-2021 Signal Messenger, LLC +// Copyright 2020-2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { CSSProperties } from 'react'; @@ -16,6 +16,7 @@ import type { GroupCallRemoteParticipantType } from '../types/Calling'; import type { LocalizerType } from '../types/Util'; import { AvatarColors } from '../types/Colors'; import { CallBackgroundBlur } from './CallBackgroundBlur'; +import { CallingAudioIndicator } from './CallingAudioIndicator'; import { Avatar, AvatarSize } from './Avatar'; import { ConfirmationDialog } from './ConfirmationDialog'; import { Intl } from './Intl'; @@ -41,6 +42,7 @@ type InPipPropsType = { type InOverflowAreaPropsType = { height: number; isInPip?: false; + isSpeaking: boolean; width: number; }; @@ -282,6 +284,10 @@ export const GroupCallRemoteParticipant: React.FC = React.memo( module="module-ongoing-call__group-call-remote-participant__info__contact-name" title={title} /> +
)} {wantsToShowVideo && ( diff --git a/ts/components/GroupCallRemoteParticipants.tsx b/ts/components/GroupCallRemoteParticipants.tsx index 387e1c283..7f145fba5 100644 --- a/ts/components/GroupCallRemoteParticipants.tsx +++ b/ts/components/GroupCallRemoteParticipants.tsx @@ -1,4 +1,4 @@ -// Copyright 2020-2021 Signal Messenger, LLC +// Copyright 2020-2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import React, { useCallback, useState, useMemo, useEffect } from 'react'; @@ -48,6 +48,7 @@ type PropsType = { isInSpeakerView: boolean; remoteParticipants: ReadonlyArray; setGroupCallVideoRequest: (_: Array) => void; + speakingDemuxIds: Set; }; enum VideoRequestMode { @@ -85,6 +86,7 @@ export const GroupCallRemoteParticipants: React.FC = ({ isInSpeakerView, remoteParticipants, setGroupCallVideoRequest, + speakingDemuxIds, }) => { const [containerDimensions, setContainerDimensions] = useState({ width: 0, @@ -266,8 +268,12 @@ export const GroupCallRemoteParticipants: React.FC = ({ let rowWidthSoFar = 0; return remoteParticipantsInRow.map(remoteParticipant => { + const { demuxId, videoAspectRatio } = remoteParticipant; + + const isSpeaking = speakingDemuxIds.has(demuxId); + const renderedWidth = Math.floor( - remoteParticipant.videoAspectRatio * gridParticipantHeight + videoAspectRatio * gridParticipantHeight ); const left = rowWidthSoFar + leftOffset; @@ -275,11 +281,12 @@ export const GroupCallRemoteParticipants: React.FC = ({ return ( = ({ i18n={i18n} onParticipantVisibilityChanged={onParticipantVisibilityChanged} overflowedParticipants={overflowedParticipants} + speakingDemuxIds={speakingDemuxIds} />
)} diff --git a/ts/components/Lottie.stories.tsx b/ts/components/Lottie.stories.tsx new file mode 100644 index 000000000..1c64c35e6 --- /dev/null +++ b/ts/components/Lottie.stories.tsx @@ -0,0 +1,21 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import React from 'react'; +import { storiesOf } from '@storybook/react'; + +import { Lottie } from './Lottie'; + +import testAnimationData from '../../fixtures/lottie-loader-by-lucas-bariani.json'; + +const STORYBOOK_CONTAINER_CLASS_NAME = 'lottie-test-storybook-container'; + +const story = storiesOf('Components/Lottie', module); + +story.add('Default', () => ( + +)); diff --git a/ts/components/Lottie.tsx b/ts/components/Lottie.tsx new file mode 100644 index 000000000..44e3a2f33 --- /dev/null +++ b/ts/components/Lottie.tsx @@ -0,0 +1,42 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import type { CSSProperties, ReactElement } from 'react'; +import React, { useEffect, useRef } from 'react'; +import lottie from '@evanhahn/lottie-web-light'; + +import { lottieNoopAudioFactory } from '../util/lottieNoopAudioFactory'; + +export function Lottie({ + animationData, + className, + style, +}: Readonly<{ + animationData: unknown; + className?: string; + style?: CSSProperties; +}>): ReactElement { + const containerRef = useRef(null); + + useEffect(() => { + const container = containerRef.current; + if (!container) { + return; + } + + const animationItem = lottie.loadAnimation({ + container, + renderer: 'svg', + loop: true, + autoplay: true, + animationData, + audioFactory: lottieNoopAudioFactory, + }); + + return () => { + animationItem.destroy(); + }; + }, [animationData]); + + return
; +} diff --git a/ts/services/calling.ts b/ts/services/calling.ts index 3be01dcce..928147cda 100644 --- a/ts/services/calling.ts +++ b/ts/services/calling.ts @@ -627,6 +627,7 @@ export class CallingClass { groupIdBuffer, this.sfuUrl, Buffer.alloc(0), + 500, { onLocalDeviceStateChanged: groupCall => { const localDeviceState = groupCall.getLocalDeviceState(); @@ -674,8 +675,16 @@ export class CallingClass { onRemoteDeviceStatesChanged: groupCall => { this.syncGroupCallToRedux(conversationId, groupCall); }, - onAudioLevels: _groupCall => { - // TODO: Implement audio level handling for group calls. + onAudioLevels: groupCall => { + const remoteDeviceStates = groupCall.getRemoteDeviceStates(); + if (!remoteDeviceStates) { + return; + } + + this.uxActions?.groupCallAudioLevelsChange({ + conversationId, + remoteDeviceStates, + }); }, onPeekChanged: groupCall => { const localDeviceState = groupCall.getLocalDeviceState(); @@ -1955,6 +1964,8 @@ export class CallingClass { }, hideIp: shouldRelayCalls || isContactUnknown, bandwidthMode: BandwidthMode.Normal, + // TODO: DESKTOP-3101 + // audioLevelsIntervalMillis: 500, }; } diff --git a/ts/state/ducks/calling.ts b/ts/state/ducks/calling.ts index 5077cdec0..e194d0642 100644 --- a/ts/state/ducks/calling.ts +++ b/ts/state/ducks/calling.ts @@ -39,6 +39,7 @@ import type { UUIDStringType } from '../../types/UUID'; import type { ConversationChangedActionType } from './conversations'; import * as log from '../../logging/log'; import { strictAssert } from '../../util/assert'; +import * as setUtil from '../../util/setUtil'; // State @@ -89,6 +90,7 @@ export type GroupCallStateType = { joinState: GroupCallJoinState; peekInfo: GroupCallPeekInfoType; remoteParticipants: Array; + speakingDemuxIds?: Set; } & GroupCallRingStateType; export type ActiveCallStateType = { @@ -305,6 +307,7 @@ const CALL_STATE_CHANGE_FULFILLED = 'calling/CALL_STATE_CHANGE_FULFILLED'; const CHANGE_IO_DEVICE_FULFILLED = 'calling/CHANGE_IO_DEVICE_FULFILLED'; const CLOSE_NEED_PERMISSION_SCREEN = 'calling/CLOSE_NEED_PERMISSION_SCREEN'; const DECLINE_DIRECT_CALL = 'calling/DECLINE_DIRECT_CALL'; +const GROUP_CALL_AUDIO_LEVELS_CHANGE = 'calling/GROUP_CALL_AUDIO_LEVELS_CHANGE'; const GROUP_CALL_STATE_CHANGE = 'calling/GROUP_CALL_STATE_CHANGE'; const HANG_UP = 'calling/HANG_UP'; const INCOMING_DIRECT_CALL = 'calling/INCOMING_DIRECT_CALL'; @@ -370,6 +373,16 @@ type DeclineCallActionType = { payload: DeclineCallType; }; +type GroupCallAudioLevelsChangeActionPayloadType = Readonly<{ + conversationId: string; + remoteDeviceStates: ReadonlyArray<{ audioLevel: number; demuxId: number }>; +}>; + +type GroupCallAudioLevelsChangeActionType = { + type: 'calling/GROUP_CALL_AUDIO_LEVELS_CHANGE'; + payload: GroupCallAudioLevelsChangeActionPayloadType; +}; + export type GroupCallStateChangeActionType = { type: 'calling/GROUP_CALL_STATE_CHANGE'; payload: GroupCallStateChangeActionPayloadType; @@ -500,6 +513,7 @@ export type CallingActionType = | CloseNeedPermissionScreenActionType | ConversationChangedActionType | DeclineCallActionType + | GroupCallAudioLevelsChangeActionType | GroupCallStateChangeActionType | HangUpActionType | IncomingDirectCallActionType @@ -706,6 +720,12 @@ function getPresentingSources(): ThunkAction< }; } +function groupCallAudioLevelsChange( + payload: GroupCallAudioLevelsChangeActionPayloadType +): GroupCallAudioLevelsChangeActionType { + return { type: GROUP_CALL_AUDIO_LEVELS_CHANGE, payload }; +} + function groupCallStateChange( payload: GroupCallStateChangeArgumentType ): ThunkAction { @@ -1242,6 +1262,7 @@ export const actions = { closeNeedPermissionScreen, declineCall, getPresentingSources, + groupCallAudioLevelsChange, groupCallStateChange, hangUp, hangUpActiveCall, @@ -1631,6 +1652,40 @@ export function reducer( }; } + if (action.type === GROUP_CALL_AUDIO_LEVELS_CHANGE) { + const { conversationId, remoteDeviceStates } = action.payload; + + const existingCall = getGroupCall(conversationId, state); + if (!existingCall) { + return state; + } + + const speakingDemuxIds = new Set(); + remoteDeviceStates.forEach(({ audioLevel, demuxId }) => { + // We expect `audioLevel` to be a number but have this check just in case. + if (typeof audioLevel === 'number' && audioLevel > 0.25) { + speakingDemuxIds.add(demuxId); + } + }); + + // This action is dispatched frequently. This equality check helps avoid re-renders. + const oldSpeakingDemuxIds = existingCall.speakingDemuxIds; + if ( + oldSpeakingDemuxIds && + setUtil.isEqual(oldSpeakingDemuxIds, speakingDemuxIds) + ) { + return state; + } + + return { + ...state, + callsByConversation: { + ...callsByConversation, + [conversationId]: { ...existingCall, speakingDemuxIds }, + }, + }; + } + if (action.type === GROUP_CALL_STATE_CHANGE) { const { connectionState, diff --git a/ts/state/smart/CallManager.tsx b/ts/state/smart/CallManager.tsx index d8d6f68b4..76e3f6381 100644 --- a/ts/state/smart/CallManager.tsx +++ b/ts/state/smart/CallManager.tsx @@ -1,4 +1,4 @@ -// Copyright 2020-2021 Signal Messenger, LLC +// Copyright 2020-2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import React from 'react'; @@ -251,6 +251,7 @@ const mapStateToActiveCallProp = ( maxDevices: call.peekInfo.maxDevices, peekedParticipants, remoteParticipants, + speakingDemuxIds: call.speakingDemuxIds || new Set(), }; } default: diff --git a/ts/test-both/util/iterables_test.ts b/ts/test-both/util/iterables_test.ts index 60d2b1572..d054ec6f9 100644 --- a/ts/test-both/util/iterables_test.ts +++ b/ts/test-both/util/iterables_test.ts @@ -1,4 +1,4 @@ -// Copyright 2021 Signal Messenger, LLC +// Copyright 2021-2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import { assert } from 'chai'; @@ -6,6 +6,7 @@ import * as sinon from 'sinon'; import { concat, + every, filter, find, groupBy, @@ -170,6 +171,30 @@ describe('iterable utilities', () => { }); }); + describe('every', () => { + const isOdd = (n: number): boolean => Boolean(n % 2); + + it('returns true for empty iterables and never checks the predicate', () => { + const fn = sinon.fake(); + + assert.isTrue(every([], fn)); + assert.isTrue(every(new Set(), fn)); + assert.isTrue(every(new Map(), fn)); + + sinon.assert.notCalled(fn); + }); + + it('returns false if any values make the predicate return false', () => { + assert.isFalse(every([2], isOdd)); + assert.isFalse(every([1, 2, 3], isOdd)); + }); + + it('returns true if all values make the predicate return true', () => { + assert.isTrue(every([1], isOdd)); + assert.isTrue(every([1, 3, 5], isOdd)); + }); + }); + describe('filter', () => { it('returns an empty iterable when passed an empty iterable', () => { const fn = sinon.fake(); diff --git a/ts/test-both/util/setUtil_test.ts b/ts/test-both/util/setUtil_test.ts index 798ccb2ec..856e8925a 100644 --- a/ts/test-both/util/setUtil_test.ts +++ b/ts/test-both/util/setUtil_test.ts @@ -1,13 +1,42 @@ -// Copyright 2021 Signal Messenger, LLC +// Copyright 2021-2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import { assert } from 'chai'; -import { remove, toggle } from '../../util/setUtil'; +import { isEqual, remove, toggle } from '../../util/setUtil'; describe('set utilities', () => { const original = new Set([1, 2, 3]); + describe('isEqual', () => { + it('returns false if the sets are different', () => { + const sets = [ + new Set([1, 2, 3]), + new Set([1, 2, 3, 4]), + new Set([1, 2]), + new Set([4, 5, 6]), + ]; + + for (const a of sets) { + for (const b of sets) { + if (a !== b) { + assert.isFalse(isEqual(a, b)); + } + } + } + }); + + it('returns true if both arguments are the same set', () => { + const set = new Set([1, 2, 3]); + assert.isTrue(isEqual(set, set)); + }); + + it('returns true if the sets have the same values', () => { + assert.isTrue(isEqual(new Set(), new Set())); + assert.isTrue(isEqual(new Set([1, 2]), new Set([2, 1]))); + }); + }); + describe('remove', () => { it('accepts zero arguments, returning a new set', () => { const result = remove(original); diff --git a/ts/test-electron/state/ducks/calling_test.ts b/ts/test-electron/state/ducks/calling_test.ts index c700b7c57..294090a6c 100644 --- a/ts/test-electron/state/ducks/calling_test.ts +++ b/ts/test-electron/state/ducks/calling_test.ts @@ -776,6 +776,67 @@ describe('calling duck', () => { }); }); + describe('groupCallAudioLevelsChange', () => { + const { groupCallAudioLevelsChange } = actions; + + const remoteDeviceStates = [ + { audioLevel: 0.3, demuxId: 1 }, + { audioLevel: 0.4, demuxId: 2 }, + { audioLevel: 0.5, demuxId: 3 }, + { audioLevel: 0.2, demuxId: 7 }, + { audioLevel: 0.1, demuxId: 8 }, + { audioLevel: 0, demuxId: 9 }, + ]; + + it("does nothing if there's no relevant call", () => { + const action = groupCallAudioLevelsChange({ + conversationId: 'garbage', + remoteDeviceStates, + }); + + const result = reducer(stateWithActiveGroupCall, action); + + assert.strictEqual(result, stateWithActiveGroupCall); + }); + + it('does nothing if the state change would be a no-op', () => { + const state = { + ...stateWithActiveGroupCall, + callsByConversation: { + 'fake-group-call-conversation-id': { + ...stateWithActiveGroupCall.callsByConversation[ + 'fake-group-call-conversation-id' + ], + speakingDemuxIds: new Set([3, 2, 1]), + }, + }, + }; + const action = groupCallAudioLevelsChange({ + conversationId: 'fake-group-call-conversation-id', + remoteDeviceStates, + }); + + const result = reducer(state, action); + + assert.strictEqual(result, state); + }); + + it('updates the set of speaking participants', () => { + const action = groupCallAudioLevelsChange({ + conversationId: 'fake-group-call-conversation-id', + remoteDeviceStates, + }); + const result = reducer(stateWithActiveGroupCall, action); + + const call = + result.callsByConversation['fake-group-call-conversation-id']; + if (call?.callMode !== CallMode.Group) { + throw new Error('Expected a group call to be found'); + } + assert.deepStrictEqual(call.speakingDemuxIds, new Set([1, 2, 3])); + }); + }); + describe('groupCallStateChange', () => { const { groupCallStateChange } = actions; diff --git a/ts/types/Calling.ts b/ts/types/Calling.ts index a65e540ea..1ea6057a1 100644 --- a/ts/types/Calling.ts +++ b/ts/types/Calling.ts @@ -65,6 +65,7 @@ type ActiveGroupCallType = ActiveCallBaseType & { groupMembers: Array>; peekedParticipants: Array; remoteParticipants: Array; + speakingDemuxIds: Set; }; export type ActiveCallType = ActiveDirectCallType | ActiveGroupCallType; diff --git a/ts/util/iterables.ts b/ts/util/iterables.ts index 64e1ba98b..e32940c26 100644 --- a/ts/util/iterables.ts +++ b/ts/util/iterables.ts @@ -1,4 +1,4 @@ -// Copyright 2021 Signal Messenger, LLC +// Copyright 2021-2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only /* eslint-disable max-classes-per-file */ @@ -46,6 +46,18 @@ class ConcatIterable implements Iterable { } } +export function every( + iterable: Iterable, + predicate: (value: T) => boolean +): boolean { + for (const value of iterable) { + if (!predicate(value)) { + return false; + } + } + return true; +} + export function filter( iterable: Iterable, predicate: (value: T) => value is S diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index 01797dc28..887c7020e 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -132,6 +132,41 @@ "reasonCategory": "falseMatch", "updated": "2021-04-05T20:48:36.065Z" }, + { + "rule": "jQuery-append(", + "path": "node_modules/@evanhahn/lottie-web-light/index.js", + "line": " this._elementHelper.append(img);", + "reasonCategory": "falseMatch", + "updated": "2022-01-27T20:06:59.988Z" + }, + { + "rule": "jQuery-insertBefore(", + "path": "node_modules/@evanhahn/lottie-web-light/index.js", + "line": " this.layerElement.insertBefore(newElement, nextElement);", + "reasonCategory": "falseMatch", + "updated": "2022-01-27T20:06:59.988Z" + }, + { + "rule": "jQuery-insertBefore(", + "path": "node_modules/@evanhahn/lottie-web-light/index.js", + "line": " parentNode.insertBefore(useElem, nextChild);", + "reasonCategory": "falseMatch", + "updated": "2022-01-27T20:06:59.988Z" + }, + { + "rule": "jQuery-load(", + "path": "node_modules/@evanhahn/lottie-web-light/index.js", + "line": " _workerSelf.assetLoader.load(", + "reasonCategory": "falseMatch", + "updated": "2022-01-27T20:06:59.988Z" + }, + { + "rule": "jQuery-load(", + "path": "node_modules/@evanhahn/lottie-web-light/index.js", + "line": " _workerSelf.assetLoader.load(", + "reasonCategory": "falseMatch", + "updated": "2022-01-27T20:06:59.988Z" + }, { "rule": "eval", "path": "node_modules/@protobufjs/inquire/index.js", @@ -7494,6 +7529,14 @@ "reasonCategory": "usageTrusted", "updated": "2021-10-11T21:21:08.188Z" }, + { + "rule": "React-useRef", + "path": "ts/components/Lottie.tsx", + "line": " const containerRef = useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2022-01-27T20:06:59.988Z", + "reasonDetail": "Doesn't manipulate the DOM." + }, { "rule": "React-useRef", "path": "ts/components/Modal.tsx", diff --git a/ts/util/lottieNoopAudioFactory.ts b/ts/util/lottieNoopAudioFactory.ts new file mode 100644 index 000000000..e84a55dd6 --- /dev/null +++ b/ts/util/lottieNoopAudioFactory.ts @@ -0,0 +1,18 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { noop } from 'lodash'; +import type { AnimationConfig } from '@evanhahn/lottie-web-light'; + +type LottieAudioFactory = NonNullable; +type LottieAudio = ReturnType; + +const lottieNoopAudio: LottieAudio = { + play: noop, + seek: noop, + playing: noop, + rate: noop, + setVolume: noop, +}; + +export const lottieNoopAudioFactory: LottieAudioFactory = () => lottieNoopAudio; diff --git a/ts/util/setUtil.ts b/ts/util/setUtil.ts index 425fee46a..1652bb551 100644 --- a/ts/util/setUtil.ts +++ b/ts/util/setUtil.ts @@ -1,6 +1,8 @@ -// Copyright 2021 Signal Messenger, LLC +// Copyright 2021-2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +import { every } from './iterables'; + const add = (set: Readonly>, item: T): Set => new Set(set).add(item); @@ -20,3 +22,8 @@ export const toggle = ( item: Readonly, shouldInclude: boolean ): Set => (shouldInclude ? add : remove)(set, item); + +export const isEqual = ( + a: Readonly>, + b: Readonly> +): boolean => a === b || (a.size === b.size && every(a, item => b.has(item))); diff --git a/yarn.lock b/yarn.lock index cdb9b62d9..22dea38ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1176,6 +1176,11 @@ resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.3.tgz#dfa0c92efe44a1d1a7974fb49ffeb40ef2da5a27" integrity sha512-zVgvPwGK7c1aVdUVc9Qv7SqepOGRDrqCw7KZPSZziWGxSlbII3gmvGLPzLX4d0n0BMbamBacUrN22zOMyFFEkQ== +"@evanhahn/lottie-web-light@5.8.1": + version "5.8.1" + resolved "https://registry.yarnpkg.com/@evanhahn/lottie-web-light/-/lottie-web-light-5.8.1.tgz#9154f9301479ec16745da925d44bd721efa04cbb" + integrity sha512-U0G1tt3/UEYnyCNNslWPi1dB7X1xQ9aoSip+B3GTKO/Bns8yz/p39vBkRSN9d25nkbHuCsbjky2coQftj5YVKw== + "@icons/material@^0.2.4": version "0.2.4" resolved "https://registry.yarnpkg.com/@icons/material/-/material-0.2.4.tgz#e90c9f71768b3736e76d7dd6783fc6c2afa88bc8" @@ -13000,9 +13005,9 @@ rimraf@^3.0.0, rimraf@^3.0.2: dependencies: glob "^7.1.3" -"ringrtc@https://github.com/signalapp/signal-ringrtc-node.git#f22009252bd3742f5b8a2761fe8f9c76a3bbc11d": - version "2.17.2" - resolved "https://github.com/signalapp/signal-ringrtc-node.git#f22009252bd3742f5b8a2761fe8f9c76a3bbc11d" +"ringrtc@https://github.com/signalapp/signal-ringrtc-node.git#561484a82f75f64391da5ce9f48217db30e9ba4b": + version "2.18.0" + resolved "https://github.com/signalapp/signal-ringrtc-node.git#561484a82f75f64391da5ce9f48217db30e9ba4b" ripemd160@^2.0.0, ripemd160@^2.0.1: version "2.0.1"