diff --git a/ACKNOWLEDGMENTS.md b/ACKNOWLEDGMENTS.md
index 6e91dd01b611..6c33a0b6fcf3 100644
--- a/ACKNOWLEDGMENTS.md
+++ b/ACKNOWLEDGMENTS.md
@@ -457,6 +457,210 @@ Signal Desktop makes use of the following open source projects.
License: MIT
+## @react-aria/utils
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2019 Adobe
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
## @react-spring/web
MIT License
@@ -2494,6 +2698,414 @@ Signal Desktop makes use of the following open source projects.
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+## react-aria
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2019 Adobe
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+## react-aria-components
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2019 Adobe
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
## react-blurhash
License: MIT
diff --git a/_locales/en/messages.json b/_locales/en/messages.json
index cc564a62970f..f7c2a71b0764 100644
--- a/_locales/en/messages.json
+++ b/_locales/en/messages.json
@@ -299,6 +299,34 @@
"messageformat": "Chats",
"description": "Shown as a header for non-pinned conversations in the left pane"
},
+ "icu:NavTabsToggle__showTabs": {
+ "messageformat": "Show Tabs",
+ "description": "Show in the left pane when the nav tabs are hidden, shows the nav tabs"
+ },
+ "icu:NavTabsToggle__hideTabs": {
+ "messageformat": "Hide Tabs",
+ "description": "Show in the nav tabs when the nav tabs are visible, hides the nav tabs"
+ },
+ "icu:NavTabs__ItemIconLabel--UnreadCount": {
+ "messageformat": "{count, number} unread",
+ "description": "Nav Tabs > Unread badge > Accessibility Text"
+ },
+ "icu:NavTabs__ItemIconLabel--MarkedUnread": {
+ "messageformat": "Marked unread",
+ "description": "Nav Tabs > Unread badge > when conversation is marked unread > Accessibility Text"
+ },
+ "icu:NavTabs__ItemLabel--Settings": {
+ "messageformat": "Settings",
+ "description": "Nav Tabs > Settings Button > Accessibility Text"
+ },
+ "icu:NavTabs__ItemLabel--Profile": {
+ "messageformat": "Profile",
+ "description": "Nav Tabs > Profile Button > Accessibility Text"
+ },
+ "icu:NavSidebar__BackButtonLabel": {
+ "messageformat": "Back",
+ "description": "Shown in the sidebar header when on a nested panel within the current sidebar, takes the user back to the previous step or default sidebar state"
+ },
"icu:archiveHelperText": {
"messageformat": "These chats are archived and will only appear in the Inbox if new messages are received.",
"description": "Shown at the top of the archived conversations list in the left pane"
@@ -2150,6 +2178,10 @@
"messageformat": "View Safety Number",
"description": "In conversation details, label for button to view safety number, opens safety number modal"
},
+ "icu:ConversationDetails__HeaderButton--Message": {
+ "messageformat": "Message",
+ "description": "In conversation details, label for button to switch to the conversation view in order to draft a message in that converation"
+ },
"icu:SafetyNumberNotification__viewSafetyNumber": {
"messageformat": "View Safety Number",
"description": "In conversation, safety number change notification, label for button to view safety number, opens safety number modal"
@@ -3383,6 +3415,14 @@
"messageformat": "Incoming video call...",
"description": "Shown in both the incoming call bar and notification for an incoming video call"
},
+ "icu:outgoingAudioCall": {
+ "messageformat": "Outgoing voice call",
+ "description": "Shown in the timeline for an outgoing voice call"
+ },
+ "icu:outgoingVideoCall": {
+ "messageformat": "Outgoing video call",
+ "description": "Shown in the timeline for an outgoing video call"
+ },
"icu:incomingGroupCall__ringing-you": {
"messageformat": "{ringer} is calling you",
"description": "Shown in the incoming call bar when someone is ringing you for a group call"
@@ -6595,6 +6635,110 @@
"messageformat": "Send again",
"description": "Button text for the confirmation dialog shown to user when attempting to resend message edit"
},
+ "icu:StoriesTab__MoreActionsLabel": {
+ "messageformat": "More actions",
+ "description": "Stories Tab > More Actions Button (opens context menu) > Accessibility Label"
+ },
+ "icu:CallsTab__HeaderTitle--CallsList": {
+ "messageformat": "Calls",
+ "description": "Calls Tab > Header > Title > On Calls List screen"
+ },
+ "icu:CallsTab__HeaderTitle--NewCall": {
+ "messageformat": "New Call",
+ "description": "Calls Tab > Header > Title > On New Call screen"
+ },
+ "icu:CallsTab__NewCallActionLabel": {
+ "messageformat": "New Call",
+ "description": "Calls Tab > New Call Action Button > Accessibility Label"
+ },
+ "icu:CallsTab__MoreActionsLabel": {
+ "messageformat": "More actions",
+ "description": "Calls Tab > More Actions Button (opens context menu) > Accessibility Label"
+ },
+ "icu:CallsTab__ClearCallHistoryLabel": {
+ "messageformat": "Clear call history",
+ "description": "Calls Tab > More Actions Context Menu > Clear Call History Button Label"
+ },
+ "icu:CallsTab__ConfirmClearCallHistory__Title": {
+ "messageformat": "Clear call history?",
+ "description": "Calls Tab > Confirm Clear Call History Dialog > Title"
+ },
+ "icu:CallsTab__ConfirmClearCallHistory__Body": {
+ "messageformat": "This will permanently delete all call history",
+ "description": "Calls Tab > Confirm Clear Call History Dialog > Body Text"
+ },
+ "icu:CallsTab__ConfirmClearCallHistory__ConfirmButton": {
+ "messageformat": "Clear",
+ "description": "Calls Tab > Confirm Clear Call History Dialog > Confirm Button"
+ },
+ "icu:CallsTab__ToastCallHistoryCleared": {
+ "messageformat": "Call history cleared",
+ "description": "Calls Tab > Clear Call History > Toast"
+ },
+ "icu:CallsTab__EmptyStateText": {
+ "messageformat": "Click to view or start a call",
+ "description": "Calls Tab > When no call is selected > Empty state > Call to action text"
+ },
+ "icu:CallsList__SearchInputPlaceholder": {
+ "messageformat": "Search",
+ "description": "Calls Tab > Calls List > Search Input > Placeholder"
+ },
+ "icu:CallsList__ToggleFilterByMissedLabel": {
+ "messageformat": "Toggle filter by missed",
+ "description": "Calls Tab > Calls List > Toggle search filter by missed > Accessibility label"
+ },
+ "icu:CallsList__ToggleFilterByMissed__RoleDescription": {
+ "messageformat": "Toggle",
+ "description": "Calls Tab > Calls List > Toggle search filter by missed > Accessibility role description ('A toggle button')"
+ },
+ "icu:CallsList__EmptyState--noQuery": {
+ "messageformat": "No recent calls. Get started by calling a friend.",
+ "description": "Calls Tab > Calls List > When no results found > With no search query"
+ },
+ "icu:CallsList__EmptyState--hasQuery": {
+ "messageformat": "No results for “{query}”",
+ "description": "Calls Tab > Calls List > When no results found > With a search query"
+ },
+ "icu:CallsList__ItemCallInfo--Incoming": {
+ "messageformat": "Incoming",
+ "description": "Calls Tab > Calls List > Call Item > Call Status > When call was accepted and was incoming"
+ },
+ "icu:CallsList__ItemCallInfo--Outgoing": {
+ "messageformat": "Outgoing",
+ "description": "Calls Tab > Calls List > Call Item > Call Status > When call was accepted and was outgoing"
+ },
+ "icu:CallsList__ItemCallInfo--Missed": {
+ "messageformat": "Missed",
+ "description": "Calls Tab > Calls List > Call Item > Call Status > When call was missed"
+ },
+ "icu:CallsList__ItemCallInfo--GroupCall": {
+ "messageformat": "Group call",
+ "description": "Calls Tab > Calls List > Call Item > Call Status > When group call is in its default state"
+ },
+ "icu:CallsNewCall__EmptyState--noQuery": {
+ "messageformat": "No recent conversations.",
+ "description": "Calls Tab > New Call > Conversations List > When no results found > With no search query"
+ },
+ "icu:CallsNewCall__EmptyState--hasQuery": {
+ "messageformat": "No results for “{query}”",
+ "description": "Calls Tab > New Call > Conversations List > When no results found > With a search query"
+ },
+ "icu:CallHistory__Description--Default": {
+ "messageformat": "{direction, select, Outgoing {Outgoing} other {Incoming}} {type, select, Audio {voice} Video {video} Group {group} other {}} call",
+ "description": "Call History > Short description of call > When call was not missed or declined (generally accepted)"
+ },
+ "icu:CallHistory__Description--Missed": {
+ "messageformat": "Missed {type, select, Audio {voice} Video {video} Group {group} other {}} call",
+ "description": "Call History > Short description of call > When incoming call was missed"
+ },
+ "icu:CallHistory__Description--Unanswered": {
+ "messageformat": "Unanswered {type, select, Audio {voice} Video {video} Group {group} other {}} call",
+ "description": "Call History > Short description of call > When outgoing call was unanswered"
+ },
+ "icu:CallHistory__Description--Declined": {
+ "messageformat": "Declined {type, select, Audio {voice} Video {video} Group {group} other {}} call",
+ "description": "Call History > Short description of call > When call was declined"
+ },
"icu:WhatsNew__modal-title": {
"messageformat": "What's New",
"description": "Title for the whats new modal"
diff --git a/app/main.ts b/app/main.ts
index b6925ca23769..c0210c5525f7 100644
--- a/app/main.ts
+++ b/app/main.ts
@@ -194,7 +194,11 @@ const defaultWebPrefs = {
getEnvironment() !== Environment.Production ||
!isProduction(app.getVersion()),
spellcheck: false,
- enableBlinkFeatures: 'CSSPseudoDir,CSSLogical',
+ // https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/platform/runtime_enabled_features.json5
+ enableBlinkFeatures: [
+ 'CSSPseudoDir', // status=experimental, needed for RTL (ex: :dir(rtl))
+ 'CSSLogical', // status=experimental, needed for RTL (ex: margin-inline-start)
+ ].join(','),
};
const DISABLE_GPU =
diff --git a/images/icons/v3/chat/chat-fill.svg b/images/icons/v3/chat/chat-fill.svg
new file mode 100644
index 000000000000..ea4410b45f1c
--- /dev/null
+++ b/images/icons/v3/chat/chat-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/images/icons/v3/filter/filter.svg b/images/icons/v3/filter/filter.svg
new file mode 100644
index 000000000000..2fbe7d54e988
--- /dev/null
+++ b/images/icons/v3/filter/filter.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/images/icons/v3/menu/menu.svg b/images/icons/v3/menu/menu.svg
new file mode 100644
index 000000000000..ea40377d8844
--- /dev/null
+++ b/images/icons/v3/menu/menu.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/images/icons/v3/phone/phone-plus-light.svg b/images/icons/v3/phone/phone-plus-light.svg
new file mode 100644
index 000000000000..296632db70e5
--- /dev/null
+++ b/images/icons/v3/phone/phone-plus-light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/images/icons/v3/photo/phone-fill.svg b/images/icons/v3/photo/phone-fill.svg
new file mode 100644
index 000000000000..d64465d12e11
--- /dev/null
+++ b/images/icons/v3/photo/phone-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/images/icons/v3/photo/phone.svg b/images/icons/v3/photo/phone.svg
new file mode 100644
index 000000000000..3bc73836a42b
--- /dev/null
+++ b/images/icons/v3/photo/phone.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/images/icons/v3/stories/stories-fill.svg b/images/icons/v3/stories/stories-fill.svg
new file mode 100644
index 000000000000..6c7002405d3e
--- /dev/null
+++ b/images/icons/v3/stories/stories-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/package.json b/package.json
index 658421557677..c910be6476cf 100644
--- a/package.json
+++ b/package.json
@@ -89,6 +89,7 @@
"@nodert-win10-rs4/windows.data.xml.dom": "0.4.4",
"@nodert-win10-rs4/windows.ui.notifications": "0.4.4",
"@popperjs/core": "2.11.6",
+ "@react-aria/utils": "3.16.0",
"@react-spring/web": "9.5.5",
"@signalapp/better-sqlite3": "8.4.3",
"@signalapp/libsignal-client": "0.29.1",
@@ -151,6 +152,8 @@
"quill": "1.3.7",
"quill-delta": "4.0.1",
"react": "17.0.2",
+ "react-aria": "3.24.0",
+ "react-aria-components": "1.0.0-alpha.3",
"react-blurhash": "0.1.2",
"react-contextmenu": "2.11.0",
"react-dom": "17.0.2",
@@ -178,7 +181,7 @@
"uuid-browser": "3.1.0",
"websocket": "1.0.34",
"@signalapp/windows-dummy-keystroke": "1.0.0",
- "zod": "3.5.1"
+ "zod": "3.21.4"
},
"devDependencies": {
"@babel/core": "7.14.3",
diff --git a/patches/react-aria-components+1.0.0-alpha.3.patch b/patches/react-aria-components+1.0.0-alpha.3.patch
new file mode 100644
index 000000000000..6c18387e1a82
--- /dev/null
+++ b/patches/react-aria-components+1.0.0-alpha.3.patch
@@ -0,0 +1,13 @@
+diff --git a/node_modules/react-aria-components/dist/types.d.ts b/node_modules/react-aria-components/dist/types.d.ts
+index eb908b4..6cd530f 100644
+--- a/node_modules/react-aria-components/dist/types.d.ts
++++ b/node_modules/react-aria-components/dist/types.d.ts
+@@ -44,7 +44,7 @@ interface SlotProps {
+ /** A slot name for the component. Slots allow the component to receive props from a parent component. */
+ slot?: string;
+ }
+-export function useContextProps(props: T & SlotProps, ref: React.ForwardedRef, context: React.Context>): [T, React.RefObject];
++export function useContextProps(props: T & SlotProps, ref: React.ForwardedRef, context: React.Context>): [T, React.RefObject];
+ interface CollectionProps extends Omit, 'children'> {
+ /** The contents of the collection. */
+ children?: ReactNode | ((item: T) => ReactElement);
diff --git a/protos/SignalService.proto b/protos/SignalService.proto
index d8382be35795..61ee94c1c1ad 100644
--- a/protos/SignalService.proto
+++ b/protos/SignalService.proto
@@ -592,9 +592,11 @@ message SyncMessage {
message CallEvent {
enum Type {
- UNKNOWN = 0;
- AUDIO_CALL = 1;
- VIDEO_CALL = 2;
+ UNKNOWN = 0;
+ AUDIO_CALL = 1;
+ VIDEO_CALL = 2;
+ GROUP_CALL = 3;
+ AD_HOC_CALL = 4;
}
enum Direction {
@@ -607,9 +609,10 @@ message SyncMessage {
UNKNOWN = 0;
ACCEPTED = 1;
NOT_ACCEPTED = 2;
+ DELETE = 3;
}
- optional bytes peerUuid = 1;
+ optional bytes peerId = 1;
optional uint64 callId = 2;
optional uint64 timestamp = 3;
optional Type type = 4;
@@ -617,6 +620,15 @@ message SyncMessage {
optional Event event = 6;
}
+ message CallLogEvent {
+ enum Type {
+ CLEAR = 0;
+ }
+
+ optional Type type = 1;
+ optional uint64 timestamp = 2;
+ }
+
optional Sent sent = 1;
optional Contacts contacts = 2;
reserved /* groups */ 3;
@@ -636,6 +648,8 @@ message SyncMessage {
reserved 17; // pniIdentity
optional PniChangeNumber pniChangeNumber = 18;
optional CallEvent callEvent = 19;
+ reserved 20; // callLinkUpdate
+ optional CallLogEvent callLogEvent = 21;
}
message AttachmentPointer {
diff --git a/sticker-creator/src/colors.scss b/sticker-creator/src/colors.scss
index fc3ca03a03cb..a9da725b369d 100644
--- a/sticker-creator/src/colors.scss
+++ b/sticker-creator/src/colors.scss
@@ -26,6 +26,7 @@ $color-black: #000000;
$color-white-alpha-06: rgba($color-white, 0.06);
$color-white-alpha-08: rgba($color-white, 0.08);
$color-white-alpha-12: rgba($color-white, 0.12);
+$color-white-alpha-16: rgba($color-white, 0.16);
$color-white-alpha-20: rgba($color-white, 0.2);
$color-white-alpha-40: rgba($color-white, 0.4);
$color-white-alpha-60: rgba($color-white, 0.6);
diff --git a/stylesheets/_conversation.scss b/stylesheets/_conversation.scss
new file mode 100644
index 000000000000..914e1f6ba62c
--- /dev/null
+++ b/stylesheets/_conversation.scss
@@ -0,0 +1,94 @@
+// Copyright 2015 Signal Messenger, LLC
+// SPDX-License-Identifier: AGPL-3.0-only
+
+@import './mixins';
+
+@keyframes panel--in--ltr {
+ from {
+ // stylelint-disable-next-line declaration-property-value-disallowed-list
+ transform: translateX(500px);
+ }
+
+ to {
+ // stylelint-disable-next-line declaration-property-value-disallowed-list
+ transform: translateX(0);
+ }
+}
+
+@keyframes panel--in--rtl {
+ from {
+ // stylelint-disable-next-line declaration-property-value-disallowed-list
+ transform: translateX(-500px);
+ }
+
+ to {
+ // stylelint-disable-next-line declaration-property-value-disallowed-list
+ transform: translateX(0);
+ }
+}
+
+.conversation {
+ @include light-theme {
+ background-color: $color-white;
+ }
+
+ @include dark-theme {
+ background-color: $color-gray-95;
+ }
+
+ .panel {
+ &:not(.main) {
+ &:dir(ltr) {
+ animation: panel--in--ltr 350ms cubic-bezier(0.17, 0.17, 0, 1);
+ }
+ &:dir(rtl) {
+ animation: panel--in--rtl 350ms cubic-bezier(0.17, 0.17, 0, 1);
+ }
+ }
+
+ &--static {
+ animation: none;
+ }
+
+ &--remove {
+ &:dir(ltr) {
+ // stylelint-disable-next-line declaration-property-value-disallowed-list
+ transform: translateX(100%);
+ }
+ &:dir(rtl) {
+ // stylelint-disable-next-line declaration-property-value-disallowed-list
+ transform: translateX(-100%);
+ }
+ transition: transform 350ms cubic-bezier(0.17, 0.17, 0, 1);
+ }
+ }
+}
+
+// Make sure the main panel is hidden when other panels are in the dom
+.panel + .main.panel {
+ display: none;
+}
+
+.message-detail-wrapper {
+ height: calc(100% - 48px);
+ width: 100%;
+ overflow-y: auto;
+}
+
+.typing-bubble-wrapper {
+ margin-bottom: 20px;
+}
+
+.contact-detail-pane {
+ overflow-y: scroll;
+ padding-top: 40px;
+ padding-bottom: 40px;
+}
+
+.permissions-popup,
+.debug-log-window {
+ .modal {
+ background-color: transparent;
+ padding: 0;
+ }
+}
diff --git a/stylesheets/_mixins.scss b/stylesheets/_mixins.scss
index 2cfb3d595f3f..8d3525c94bd6 100644
--- a/stylesheets/_mixins.scss
+++ b/stylesheets/_mixins.scss
@@ -43,6 +43,14 @@
letter-spacing: -0.34px;
}
+@mixin font-title-medium {
+ @include font-family;
+ font-weight: 600;
+ font-size: 18px;
+ line-height: 25px;
+ letter-spacing: -0.25px;
+}
+
@mixin font-body-1 {
@include font-family;
font-size: 14px;
@@ -858,3 +866,15 @@ $rtl-icon-map: (
top: 50%;
transform: translateY(-50%);
}
+
+@mixin NavTabs__Scroller {
+ @include scrollbar;
+ &::-webkit-scrollbar-thumb {
+ @include light-theme {
+ border-color: $color-gray-04;
+ }
+ @include dark-theme {
+ border-color: $color-gray-80;
+ }
+ }
+}
diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss
index 4fb344e4dacc..b40a4b5a9f15 100644
--- a/stylesheets/_modules.scss
+++ b/stylesheets/_modules.scss
@@ -2445,166 +2445,6 @@ button.ConversationDetails__action-button {
}
}
-// Module: Main Header
-
-.module-main-header {
- -webkit-app-region: var(--draggable-app-region);
-
- height: calc(#{$header-height} + var(--title-bar-drag-area-height));
- width: 100%;
-
- padding-inline: 16px;
- padding-top: var(--title-bar-drag-area-height);
-
- display: flex;
- justify-content: space-between;
- flex-direction: row;
- align-items: center;
-
- .module-left-pane--width-narrow & {
- flex-direction: column;
- height: auto;
- justify-content: center;
- }
-
- & > * {
- margin-inline-end: 12px;
-
- &:last-child {
- margin-inline-end: 0;
- }
- }
-
- &__avatar {
- -webkit-app-region: no-drag;
-
- &--container {
- position: relative;
-
- .module-left-pane--width-narrow & {
- margin-bottom: 20px;
- margin-inline-end: 0;
- margin-top: 12px;
- }
- }
-
- &--badged {
- background: $color-ultramarine;
- border-radius: 100%;
- border: 1px solid $color-white;
- height: 8px;
- width: 8px;
- position: absolute;
- top: 0;
- inset-inline-end: 0;
- }
- }
-
- &__icon-container {
- display: flex;
-
- .module-left-pane--width-narrow & {
- flex-direction: column-reverse;
- }
- }
-
- &__compose-icon {
- -webkit-app-region: no-drag;
- align-items: center;
- background: none;
- border-radius: 4px;
- border: 2px solid transparent;
- display: inline-flex;
- height: 28px;
- justify-content: center;
- width: 28px;
- padding: 0px;
-
- @include keyboard-mode {
- &:focus {
- border-color: $color-ultramarine;
- outline: none;
- }
- }
-
- &::before {
- $icon: '../images/icons/v3/compose/compose.svg';
- width: 20px;
- height: 20px;
- content: '';
-
- @include light-theme {
- @include color-svg($icon, $color-gray-75);
- }
- @include dark-theme {
- @include color-svg($icon, $color-gray-15);
- }
- }
- }
-
- &__stories-badge {
- @include rounded-corners;
- align-items: center;
- background-color: $color-accent-red;
- color: $color-white;
- display: flex;
- font-size: 10px;
- height: 16px;
- justify-content: center;
- min-width: 16px;
- overflow: hidden;
- padding-block: 0;
- padding-inline: 2px;
- position: absolute;
- inset-inline-end: -6px;
- top: -4px;
- user-select: none;
- z-index: $z-index-base;
- }
-
- &__stories-icon {
- -webkit-app-region: no-drag;
- align-items: center;
- background: none;
- border-radius: 4px;
- border: 2px solid transparent;
- display: inline-flex;
- height: 28px;
- justify-content: center;
- margin-inline-end: 12px;
- position: relative;
- width: 28px;
- padding: 0px;
-
- .module-left-pane--width-narrow & {
- margin-inline-end: 0;
- margin-top: 16px;
- margin-bottom: 20px;
- }
-
- @include keyboard-mode {
- &:focus {
- border-color: $color-ultramarine;
- outline: none;
- }
- }
-
- &::before {
- $icon: '../images/icons/v3/stories/stories.svg';
- width: 20px;
- height: 20px;
- content: '';
-
- @include light-theme {
- @include color-svg($icon, $color-gray-75);
- }
- @include dark-theme {
- @include color-svg($icon, $color-gray-15);
- }
- }
- }
-}
-
// Module: Image
.module-image {
@@ -4365,7 +4205,7 @@ button.module-image__border-overlay:focus {
.module-conversation-list {
$normal-row-height: 72px;
- @include scrollbar;
+ @include NavTabs__Scroller;
padding-inline: 10px;
// list tiles in choose-group-members and compose extend to the edge
@@ -5045,33 +4885,18 @@ button.module-image__border-overlay:focus {
// Module: Left Pane
.module-left-pane {
- border-inline-end-style: solid;
- border-inline-end-width: 1px;
- display: inline-flex;
+ display: flex;
flex-direction: column;
height: 100%;
+ width: 100%;
position: relative;
@include light-theme {
$background-color: $color-gray-02;
-
- border-color: $color-gray-15;
- background-color: $background-color;
-
- ::-webkit-scrollbar-thumb {
- border: 2px solid $color-gray-02;
- }
}
@include dark-theme {
$background-color: $color-gray-80;
-
- border-color: $color-gray-65;
- background-color: $background-color;
-
- ::-webkit-scrollbar-thumb {
- border: 2px solid $color-gray-80;
- }
}
}
@@ -5122,13 +4947,11 @@ button.module-image__border-overlay:focus {
user-select: none;
&__contents {
- height: calc(#{$header-height} + var(--title-bar-drag-area-height));
width: 100%;
-
display: inline-flex;
flex-direction: row;
align-items: center;
- padding-top: var(--title-bar-drag-area-height);
+ padding-block: 15px;
&__back-button {
@include button-reset;
@@ -5248,6 +5071,36 @@ button.module-image__border-overlay:focus {
}
}
+.module-left-pane__startComposingIcon {
+ display: block;
+ width: 20px;
+ height: 20px;
+ @include light-theme {
+ @include color-svg(
+ '../images/icons/v3/compose/compose.svg',
+ $color-gray-75
+ );
+ }
+ @include dark-theme {
+ @include color-svg(
+ '../images/icons/v3/compose/compose.svg',
+ $color-gray-15
+ );
+ }
+}
+
+.module-left-pane__moreActionsIcon {
+ display: block;
+ width: 20px;
+ height: 20px;
+ @include light-theme {
+ @include color-svg('../images/icons/v3/more/more.svg', $color-black);
+ }
+ @include dark-theme {
+ @include color-svg('../images/icons/v3/more/more.svg', $color-gray-15);
+ }
+}
+
.module-left-pane__archive-helper-text {
@include font-body-2;
diff --git a/stylesheets/_variables.scss b/stylesheets/_variables.scss
index 5c7d4d5f776f..79ad8f81c6c9 100644
--- a/stylesheets/_variables.scss
+++ b/stylesheets/_variables.scss
@@ -36,6 +36,7 @@ $color-black: #000000;
$color-white-alpha-06: rgba($color-white, 0.06);
$color-white-alpha-08: rgba($color-white, 0.08);
$color-white-alpha-12: rgba($color-white, 0.12);
+$color-white-alpha-16: rgba($color-white, 0.16);
$color-white-alpha-20: rgba($color-white, 0.2);
$color-white-alpha-40: rgba($color-white, 0.4);
$color-white-alpha-60: rgba($color-white, 0.6);
@@ -280,3 +281,9 @@ $z-index-modal-host: 102;
$z-index-above-popup: 103;
$z-index-calling-pip: 104;
$z-index-above-context-menu: 126;
+
+// global navTabs
+$NavTabs__width: 80px;
+// These values are 'block' specific to coordinate with the NavSidebar__Header
+$NavTabs__Item__blockPadding: 2px;
+$NavTabs__ItemButton__blockPadding: 10px;
diff --git a/stylesheets/components/Button.scss b/stylesheets/components/Button.scss
index 9cced7b58bee..07e6f41751f7 100644
--- a/stylesheets/components/Button.scss
+++ b/stylesheets/components/Button.scss
@@ -259,6 +259,10 @@
@include button-icon('../images/icons/v3/phone/phone-compact.svg');
}
+ &--message::before {
+ @include button-icon('../images/icons/v3/chat/chat-compact.svg');
+ }
+
&--muted::before {
@include button-icon('../images/icons/v3/bell/bell-slash-compact.svg');
}
diff --git a/stylesheets/components/CallsTab.scss b/stylesheets/components/CallsTab.scss
new file mode 100644
index 000000000000..a1d69c57629e
--- /dev/null
+++ b/stylesheets/components/CallsTab.scss
@@ -0,0 +1,305 @@
+// Copyright 2023 Signal Messenger, LLC
+// SPDX-License-Identifier: AGPL-3.0-only
+
+.CallsTab {
+ display: flex;
+ width: 100%;
+ height: 100%;
+}
+
+.CallsTab__NewCallActionIcon {
+ display: block;
+ width: 20px;
+ height: 20px;
+ @include light-theme {
+ @include color-svg(
+ '../images/icons/v3/phone/phone-plus-light.svg',
+ $color-black
+ );
+ }
+ @include dark-theme {
+ @include color-svg(
+ '../images/icons/v3/phone/phone-plus-light.svg',
+ $color-gray-15
+ );
+ }
+}
+
+.CallsTab__MoreActionsIcon {
+ display: block;
+ width: 20px;
+ height: 20px;
+ @include light-theme {
+ @include color-svg('../images/icons/v3/more/more.svg', $color-black);
+ }
+ @include dark-theme {
+ @include color-svg('../images/icons/v3/more/more.svg', $color-gray-15);
+ }
+}
+
+.CallsTab__EmptyState {
+ display: flex;
+ width: 100%;
+ height: 100%;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+}
+
+.CallsTab__ConversationCallDetails {
+ display: block;
+ overflow: auto;
+ width: 100%;
+ height: 100%;
+ padding-block: 80px;
+}
+
+.CallsTab__ClearCallHistoryIcon {
+ @include light-theme {
+ @include color-svg(
+ '../images/icons/v3/trash/trash-compact.svg',
+ $color-gray-90
+ );
+ }
+ @include dark-theme {
+ @include color-svg(
+ '../images/icons/v3/trash/trash-compact.svg',
+ $color-white
+ );
+ }
+}
+
+.CallsList__Header {
+ display: flex;
+ gap: 0px;
+}
+
+.CallsList__ToggleFilterByMissed {
+ @include button-reset;
+ flex-shrink: 0;
+ padding: 4px;
+ border-radius: 4px;
+
+ &:not(.CallsList__ToggleFilterByMissed--pressed):hover {
+ @include light-theme {
+ background: $color-gray-20;
+ }
+ @include dark-theme {
+ background: $color-gray-62;
+ }
+ }
+
+ &:focus {
+ outline: none;
+ }
+
+ &:focus-visible {
+ box-shadow: 0 0 0 2px $color-white, 0 0 0 4px $color-ultramarine;
+ }
+
+ &::before {
+ content: '';
+ display: block;
+ width: 20px;
+ height: 20px;
+ @include light-theme {
+ @include color-svg('../images/icons/v3/filter/filter.svg', $color-black);
+ }
+ @include dark-theme {
+ @include color-svg(
+ '../images/icons/v3/filter/filter.svg',
+ $color-gray-15
+ );
+ }
+ }
+}
+
+.CallsList__ToggleFilterByMissed--pressed {
+ border-radius: 9999px;
+ background: $color-accent-blue;
+ &::before {
+ @include color-svg('../images/icons/v3/filter/filter.svg', $color-white);
+ }
+}
+
+.CallsList__ToggleFilterByMissedLabel {
+ @include sr-only;
+}
+
+.CallsList__ListContainer {
+ flex-grow: 1;
+ overflow: hidden;
+}
+
+.CallsList__List {
+ @include NavTabs__Scroller;
+}
+
+.CallsList__List--loading {
+ overflow: hidden !important;
+}
+
+.CallsList__EmptyState {
+ padding-block: 28px;
+ padding-inline: 16px;
+ text-align: center;
+ text-wrap: balance;
+}
+
+.CallsList__ItemIcon {
+ display: block;
+ width: 20px;
+ height: 20px;
+}
+
+.CallsList__ItemIcon--Phone {
+ @include light-theme {
+ @include color-svg('../images/icons/v3/phone/phone.svg', $color-black);
+ }
+ @include dark-theme {
+ @include color-svg('../images/icons/v3/phone/phone.svg', $color-gray-15);
+ }
+}
+
+.CallsList__ItemIcon--Video {
+ @include light-theme {
+ @include color-svg('../images/icons/v3/video/video.svg', $color-black);
+ }
+ @include dark-theme {
+ @include color-svg('../images/icons/v3/video/video.svg', $color-gray-15);
+ }
+}
+
+.CallsList__LoadingAvatar,
+.CallsList__LoadingText {
+ animation: CallsList__LoadingPulse 1.5s ease-in-out infinite;
+ @include light-theme {
+ background-color: $color-gray-05;
+ }
+ @include dark-theme {
+ background-color: $color-gray-75;
+ }
+}
+
+.CallsList__LoadingAvatar {
+ display: block;
+ width: 32px;
+ height: 32px;
+ border-radius: 9999px;
+}
+
+.CallsList__LoadingText {
+ display: inline-block; // ensure uses line-height
+ height: 1em;
+ border-radius: 4px;
+}
+
+.CallsList__LoadingText--title {
+ width: 75%;
+}
+
+.CallsList__LoadingText--subtitle {
+ width: 60%;
+}
+
+.CallsList__ItemTitle {
+ font-weight: bold;
+}
+
+.CallsList__ItemCallInfo--missed {
+ color: $color-accent-red;
+}
+
+.CallsList__Item--selected .CallsList__ItemTile {
+ @include light-theme {
+ background-color: $color-gray-15;
+ }
+ @include dark-theme {
+ background-color: $color-gray-65;
+ }
+}
+
+@keyframes CallsList__LoadingPulse {
+ 0%,
+ 100% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.55;
+ }
+}
+
+.CallsNewCall__ListContainer {
+ flex-grow: 1;
+ overflow: hidden;
+}
+
+.CallsNewCall__List {
+ @include NavTabs__Scroller;
+}
+
+.CallsNewCall__ListHeaderItem {
+ padding-block: 10px;
+ padding-inline: 24px;
+ @include font-body-1-bold;
+}
+
+.CallsNewCall__EmptyState {
+ padding-block: 28px;
+ padding-inline: 16px;
+ text-align: center;
+}
+
+.CallsNewCall__ItemActions {
+ display: flex;
+ gap: 12px;
+ align-items: center;
+}
+
+.CallsNewCall__ItemActionButton {
+ @include button-reset;
+ padding: 4px;
+ border-radius: 4px;
+ &:not(:disabled, [aria-disabled='true']):hover {
+ @include light-theme {
+ background: $color-gray-20;
+ }
+ @include dark-theme {
+ background: $color-gray-62;
+ }
+ }
+ &:focus {
+ outline: none;
+ }
+ &:focus-visible {
+ box-shadow: 0 0 0 2px $color-ultramarine;
+ }
+ &:disabled,
+ &[aria-disabled='true'] {
+ opacity: 0.5;
+ }
+}
+
+.CallsNewCall__ItemIcon {
+ display: block;
+ width: 20px;
+ height: 20px;
+}
+
+.CallsNewCall__ItemIcon--Phone {
+ @include light-theme {
+ @include color-svg('../images/icons/v3/phone/phone.svg', $color-black);
+ }
+ @include dark-theme {
+ @include color-svg('../images/icons/v3/phone/phone.svg', $color-gray-15);
+ }
+}
+
+.CallsNewCall__ItemIcon--Video {
+ @include light-theme {
+ @include color-svg('../images/icons/v3/video/video.svg', $color-black);
+ }
+ @include dark-theme {
+ @include color-svg('../images/icons/v3/video/video.svg', $color-gray-15);
+ }
+}
diff --git a/stylesheets/components/ConversationDetails.scss b/stylesheets/components/ConversationDetails.scss
index 2bc0fe438144..aa6365793e33 100644
--- a/stylesheets/components/ConversationDetails.scss
+++ b/stylesheets/components/ConversationDetails.scss
@@ -512,3 +512,55 @@
}
}
}
+
+.ConversationDetails__CallHistoryGroup__header {
+ @include font-title-2;
+ margin-block: 24px 16px;
+}
+
+.ConversationDetails__CallHistoryGroup__List {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.ConversationDetails__CallHistoryGroup__Item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin: 0;
+ padding-block: 10px;
+ padding-inline: 24px;
+}
+
+.ConversationDetails__CallHistoryGroup__ItemIcon {
+ display: block;
+ width: 20px;
+ height: 20px;
+}
+
+.ConversationDetails__CallHistoryGroup__ItemIcon--Audio {
+ @include light-theme {
+ @include color-svg('../images/icons/v3/phone/phone.svg', $color-black);
+ }
+ @include dark-theme {
+ @include color-svg('../images/icons/v3/phone/phone.svg', $color-gray-15);
+ }
+}
+
+.ConversationDetails__CallHistoryGroup__ItemIcon--Video {
+ @include light-theme {
+ @include color-svg('../images/icons/v3/video/video.svg', $color-black);
+ }
+ @include dark-theme {
+ @include color-svg('../images/icons/v3/video/video.svg', $color-gray-15);
+ }
+}
+
+.ConversationDetails__CallHistoryGroup__ItemLabel {
+ flex: 1;
+}
+
+.ConversationDetails__CallHistoryGroup__ItemTimestamp {
+ flex-shrink: 0;
+}
diff --git a/stylesheets/components/MyStories.scss b/stylesheets/components/MyStories.scss
index 4712f4bc4d2f..a389fe24e0ed 100644
--- a/stylesheets/components/MyStories.scss
+++ b/stylesheets/components/MyStories.scss
@@ -144,7 +144,7 @@
@include rounded-corners;
align-items: center;
background: $color-ultramarine-dawn;
- border: 2px solid $color-gray-80;
+ border: 2px solid;
bottom: -2px;
display: flex;
height: 20px;
@@ -154,6 +154,14 @@
width: 20px;
z-index: $z-index-base;
+ @include light-theme {
+ border-color: $color-gray-04;
+ }
+
+ @include dark-theme {
+ border-color: $color-gray-80;
+ }
+
&::after {
content: '';
@include color-svg(
@@ -166,6 +174,14 @@
}
}
-.StoryListItem__button:hover .MyStories__avatar__add-story {
- border-color: $color-gray-65;
+.StoryListItem__button:hover,
+.StoryListItem__button--active {
+ .MyStories__avatar__add-story {
+ @include light-theme {
+ border-color: $color-gray-15;
+ }
+ @include dark-theme {
+ border-color: $color-gray-65;
+ }
+ }
}
diff --git a/stylesheets/components/NavSidebar.scss b/stylesheets/components/NavSidebar.scss
new file mode 100644
index 000000000000..4462258ce926
--- /dev/null
+++ b/stylesheets/components/NavSidebar.scss
@@ -0,0 +1,207 @@
+// Copyright 2023 Signal Messenger, LLC
+// SPDX-License-Identifier: AGPL-3.0-only
+
+.NavSidebar {
+ position: relative;
+ z-index: 10;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ flex-shrink: 0;
+ padding-top: var(--title-bar-drag-area-height);
+ user-select: none;
+ @include light-theme {
+ background-color: $color-gray-04;
+ border-inline-end: 1px solid $color-black-alpha-16;
+ }
+ @include dark-theme {
+ background-color: $color-gray-80;
+ border-inline-end: 1px solid $color-white-alpha-16;
+ }
+}
+
+.NavSidebar__Header {
+ display: flex;
+ align-items: start;
+ flex-shrink: 0;
+ padding-bottom: 6px;
+
+ .NavTabs__Toggle {
+ width: $NavTabs__width;
+ }
+
+ .NavSidebar--narrow & {
+ flex-direction: column;
+ align-items: center;
+ }
+}
+
+.NavSidebar__HeaderContent {
+ display: flex;
+ width: 100%;
+ flex: 1;
+ align-items: center;
+ justify-content: center;
+ padding-block: calc(
+ $NavTabs__Item__blockPadding + $NavTabs__ItemButton__blockPadding
+ );
+ padding-inline: 24px;
+
+ .NavSidebar--narrow & {
+ padding-inline: 0;
+ }
+}
+
+.NavSidebar__HeaderContent--navTabsCollapsed:not(
+ .NavSidebar__HeaderContent--withBackButton
+ ) {
+ padding-inline-start: 0;
+}
+
+.NavSidebar__HeaderContent--withBackButton {
+ padding-inline-start: 16px;
+}
+
+.NavSidebar__HeaderTitle {
+ flex: 1 1 0%;
+ margin: 0;
+ @include font-title-medium;
+ line-height: 20px;
+
+ .NavSidebar--narrow & {
+ @include sr-only;
+ }
+}
+
+.NavSidebar__HeaderTitle--withBackButton {
+ text-align: center;
+ @include font-body-1-bold;
+}
+
+.NavSidebar__BackButton {
+ @include button-reset();
+ margin-block: -4px;
+ padding: 4px;
+ border-radius: 4px;
+
+ &:hover {
+ @include light-theme {
+ background: $color-gray-20;
+ }
+ @include dark-theme {
+ background: $color-gray-62;
+ }
+ }
+
+ &:focus {
+ outline: none;
+ }
+
+ &:focus-visible {
+ box-shadow: 0 0 0 2px $color-ultramarine;
+ }
+
+ &::before {
+ content: '';
+ display: block;
+ width: 20px;
+ height: 20px;
+ @include light-theme {
+ @include color-svg(
+ '../images/icons/v3/chevron/chevron-left.svg',
+ $color-black
+ );
+ }
+ @include dark-theme {
+ @include color-svg(
+ '../images/icons/v3/chevron/chevron-left.svg',
+ $color-gray-15
+ );
+ }
+ }
+}
+
+.NavSidebar__BackButtonLabel {
+ @include sr-only;
+}
+
+.NavSidebar__Content {
+ flex: 1 1 0%;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+}
+
+.NavSidebar__DragHandle {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ inset-inline-start: 100%;
+ width: 8px;
+ background: transparent;
+ cursor: col-resize;
+
+ &:focus-visible {
+ outline: none;
+ box-shadow: inset 0 0 0 2px $color-ultramarine;
+ }
+}
+
+.NavSidebar__DragHandle--dragging {
+ @include light-theme {
+ background-color: $color-black-alpha-12;
+ }
+ @include dark-theme {
+ background-color: $color-white-alpha-12;
+ }
+}
+
+.NavSidebar__document--draggingHandle {
+ cursor: col-resize;
+}
+
+.NavSidebar__HeaderActions {
+ display: flex;
+ gap: 20px;
+ margin-block: -4px;
+ align-items: center;
+ justify-content: center;
+
+ .NavSidebar--narrow & {
+ flex-direction: column;
+ }
+}
+
+.NavSidebar__ActionButton {
+ @include button-reset();
+ padding: 4px;
+ border-radius: 4px;
+
+ &:hover {
+ @include light-theme {
+ background: $color-gray-20;
+ }
+ @include dark-theme {
+ background: $color-gray-62;
+ }
+ }
+
+ &:focus {
+ outline: none;
+ }
+
+ &:focus-visible {
+ box-shadow: 0 0 0 2px $color-ultramarine;
+ }
+}
+
+.NavSidebar__ActionButtonLabel {
+ @include sr-only;
+}
+
+.NavSidebarSearchHeader {
+ display: flex;
+ margin-inline: 16px;
+ margin-bottom: 8px;
+ gap: 12px;
+}
diff --git a/stylesheets/components/NavTabs.scss b/stylesheets/components/NavTabs.scss
new file mode 100644
index 000000000000..1bc749235409
--- /dev/null
+++ b/stylesheets/components/NavTabs.scss
@@ -0,0 +1,183 @@
+// Copyright 2023 Signal Messenger, LLC
+// SPDX-License-Identifier: AGPL-3.0-only
+
+// This effectively wraps the entire app
+.NavTabs__Container {
+ position: relative;
+ z-index: 0;
+ display: flex;
+ width: 100%;
+ height: 100%;
+}
+
+.NavTabs {
+ display: flex;
+ flex-shrink: 0;
+ flex-direction: column;
+ width: $NavTabs__width;
+ height: 100%;
+ padding-top: var(--title-bar-drag-area-height);
+ @include light-theme {
+ background-color: $color-gray-04;
+ border-inline-end: 1px solid $color-black-alpha-16;
+ }
+ @include dark-theme {
+ background-color: $color-gray-80;
+ border-inline-end: 1px solid $color-white-alpha-16;
+ }
+}
+
+.NavTabs--collapsed {
+ display: none;
+}
+
+// Wraps .NavTabs__ItemButton to make the hitbox larger
+.NavTabs__Item {
+ width: 100%;
+ padding-block: $NavTabs__Item__blockPadding;
+ padding-inline: 10px;
+ border: none;
+ background: transparent;
+ cursor: pointer;
+ &:focus {
+ // Handled by .NavTabs__ItemButton
+ outline: none;
+ }
+}
+
+.NavTabs__ItemButton {
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: $NavTabs__ItemButton__blockPadding;
+ border-radius: 8px;
+ .NavTabs__Item:hover &,
+ .NavTabs__Item:focus-visible & {
+ @include light-theme {
+ background-color: $color-black-alpha-06;
+ }
+ @include dark-theme {
+ background-color: $color-white-alpha-06;
+ }
+ }
+ .NavTabs__Item:focus-visible & {
+ box-shadow: 0 0 0 2px $color-ultramarine;
+ }
+ .NavTabs__Item:active &,
+ .NavTabs__Item[aria-selected='true'] & {
+ @include light-theme {
+ background: $color-gray-20;
+ }
+ @include dark-theme {
+ background: $color-gray-62;
+ }
+ }
+}
+
+.NavTabs__ItemContent {
+ display: inline-flex;
+ position: relative;
+}
+
+.NavTabs__ItemLabel {
+ @include sr-only;
+}
+
+.NavTabs__ItemBadge {
+ @include rounded-corners;
+ align-items: center;
+ background-color: $color-accent-red;
+ color: $color-white;
+ display: flex;
+ font-size: 10px;
+ height: 16px;
+ justify-content: center;
+ min-width: 16px;
+ overflow: hidden;
+ padding-block: 0;
+ padding-inline: 2px;
+ position: absolute;
+ inset-inline-end: -6px;
+ top: -4px;
+ user-select: none;
+ z-index: $z-index-base;
+ word-break: keep-all;
+}
+
+.NavTabs__ItemIcon {
+ display: block;
+ width: 20px;
+ height: 20px;
+}
+
+@mixin NavTabs__Icon($icon) {
+ @include light-theme {
+ @include color-svg($icon, $color-black);
+ }
+ @include dark-theme {
+ @include color-svg($icon, $color-gray-15);
+ }
+}
+
+.NavTabs__ItemIcon--Menu {
+ @include NavTabs__Icon('../images/icons/v3/menu/menu.svg');
+}
+
+.NavTabs__ItemIcon--Settings {
+ @include NavTabs__Icon('../images/icons/v3/settings/settings.svg');
+}
+
+.NavTabs__ItemIcon--Chats {
+ @include NavTabs__Icon('../images/icons/v3/chat/chat.svg');
+ .NavTabs__Item:active &,
+ .NavTabs__Item[aria-selected='true'] & {
+ @include NavTabs__Icon('../images/icons/v3/chat/chat-fill.svg');
+ }
+}
+
+.NavTabs__ItemIcon--Calls {
+ @include NavTabs__Icon('../images/icons/v3/phone/phone.svg');
+ .NavTabs__Item:active &,
+ .NavTabs__Item[aria-selected='true'] & {
+ @include NavTabs__Icon('../images/icons/v3/phone/phone-fill.svg');
+ }
+}
+
+.NavTabs__ItemIcon--Stories {
+ @include NavTabs__Icon('../images/icons/v3/stories/stories.svg');
+ .NavTabs__Item:active &,
+ .NavTabs__Item[aria-selected='true'] & {
+ @include NavTabs__Icon('../images/icons/v3/stories/stories-fill.svg');
+ }
+}
+
+.NavTabs__ItemIconLabel {
+ @include sr-only;
+}
+
+.NavTabs__TabList {
+ flex: 1;
+}
+
+.NavTabs__Misc {
+ padding-bottom: 8px;
+}
+
+.NavTabs__TabPanel {
+ position: relative;
+ display: flex;
+ flex: 1;
+ min-width: 0;
+}
+
+.NavTabs__AvatarBadge {
+ background: $color-ultramarine;
+ border-radius: 100%;
+ border: 1px solid $color-white;
+ height: 8px;
+ width: 8px;
+ position: absolute;
+ top: 0;
+ inset-inline-end: 0;
+}
diff --git a/stylesheets/components/SearchInput.scss b/stylesheets/components/SearchInput.scss
index 158efddf95e5..13a482bc7867 100644
--- a/stylesheets/components/SearchInput.scss
+++ b/stylesheets/components/SearchInput.scss
@@ -3,9 +3,8 @@
.module-SearchInput {
&__container {
- margin-inline: 16px;
- margin-bottom: 8px;
position: relative;
+ flex: 1 0 0;
}
&__icon {
diff --git a/stylesheets/components/Stories.scss b/stylesheets/components/Stories.scss
index 10c485e3f277..b649f4861d89 100644
--- a/stylesheets/components/Stories.scss
+++ b/stylesheets/components/Stories.scss
@@ -1,8 +1,6 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
-
.Stories {
- background: $color-gray-95;
display: flex;
height: var(--window-height);
inset-inline-start: 0;
@@ -11,42 +9,37 @@
user-select: none;
width: 100%;
z-index: $z-index-stories;
+ @include light-theme {
+ background: $color-white;
+ }
+ @include dark-theme {
+ background: $color-gray-95;
+ }
&__pane {
- background: $color-gray-80;
- border-inline-end: 1px solid $color-gray-65;
display: flex;
flex-direction: column;
height: 100%;
width: 380px;
- padding-top: calc(14px + var(--title-bar-drag-area-height));
-
- &__add-story__button {
- @include color-svg('../images/icons/v3/plus/plus.svg', $color-white);
- height: 20px;
- position: absolute;
- inset-inline-end: 64px;
- top: 0px;
- width: 20px;
-
- &:focus {
- @include keyboard-mode {
- background-color: $color-ultramarine;
- }
- }
+ padding-top: calc(2px + var(--title-bar-drag-area-height));
+ @include light-theme {
+ background: $color-gray-04;
+ border-inline-end: 1px solid $color-black-alpha-16;
+ }
+ @include dark-theme {
+ background: $color-gray-80;
+ border-inline-end: 1px solid $color-white-alpha-16;
}
- &__settings__button {
- @include dark-theme {
- @include color-svg('../images/icons/v3/more/more.svg', $color-white);
- }
+ &__add-story__button {
height: 20px;
- margin-inline-start: 20px;
- opacity: 1;
- position: absolute;
- inset-inline-end: 24px;
- top: 0px;
width: 20px;
+ @include light-theme {
+ @include color-svg('../images/icons/v3/plus/plus.svg', $color-black);
+ }
+ @include dark-theme {
+ @include color-svg('../images/icons/v3/plus/plus.svg', $color-gray-15);
+ }
&:focus {
@include keyboard-mode {
@@ -68,55 +61,40 @@
}
&--title {
- @include font-body-1-bold;
- color: $color-gray-05;
- display: flex;
+ @include font-title-medium;
flex: 1;
- justify-content: center;
+ @include light-theme {
+ color: $color-black;
+ }
+ @include dark-theme {
+ color: $color-gray-05;
+ }
}
&--centered .Stories__pane__header--title {
text-align: center;
width: 100%;
}
-
- &--back {
- @include button-reset;
-
- -webkit-app-region: no-drag;
- height: 20px;
- position: absolute;
- width: 20px;
-
- @include color-svg(
- '../images/icons/v3/chevron/chevron-left.svg',
- $color-white
- );
-
- @include keyboard-mode {
- &:hover {
- @include color-svg(
- '../images/icons/v3/chevron/chevron-left.svg',
- $color-ultramarine-light
- );
- }
- }
- }
}
&__list {
- @include scrollbar;
+ @include NavTabs__Scroller;
display: flex;
flex-direction: column;
flex: 1;
overflow-y: overlay;
padding-block: 0;
- padding-inline: 14px;
+ padding-inline: 16px;
&--empty {
@include font-body-1;
align-items: center;
- color: $color-gray-45;
+ @include light-theme() {
+ color: $color-gray-60;
+ }
+ @include dark-theme() {
+ color: $color-gray-45;
+ }
display: flex;
flex: 1;
flex-direction: column;
@@ -125,11 +103,6 @@
}
}
- &__search__container {
- margin-block: 14px 10px;
- margin-inline: 16px;
- }
-
&__placeholder {
align-items: center;
color: $color-gray-45;
@@ -154,33 +127,69 @@
@include button-reset;
@include font-body-1-bold;
align-items: center;
- color: $color-gray-05;
display: flex;
justify-content: space-between;
padding-block: 12px;
padding-inline: 24px;
position: relative;
width: 100%;
+ @include light-theme {
+ color: $color-black;
+ }
+ @include dark-theme {
+ color: $color-gray-05;
+ }
&::after {
- @include color-svg(
- '../images/icons/v3/chevron/chevron-right.svg',
- $color-gray-05
- );
content: '';
height: 16px;
width: 16px;
}
+ &--collapsed {
+ &::after {
+ @include light-theme {
+ @include color-svg(
+ '../images/icons/v3/chevron/chevron-right.svg',
+ $color-black
+ );
+ }
+ @include dark-theme {
+ @include color-svg(
+ '../images/icons/v3/chevron/chevron-right.svg',
+ $color-gray-05
+ );
+ }
+ }
+ }
+
&--expanded {
- // Override color-svg
- :dir(ltr) &::after,
- :dir(rtl) &::after {
- @include color-svg(
- '../images/icons/v3/chevron/chevron-down.svg',
- $color-gray-05
- );
+ &::after {
+ @include light-theme {
+ @include color-svg(
+ '../images/icons/v3/chevron/chevron-down.svg',
+ $color-black
+ );
+ }
+ @include dark-theme {
+ @include color-svg(
+ '../images/icons/v3/chevron/chevron-down.svg',
+ $color-gray-05
+ );
+ }
}
}
}
}
+
+.StoriesTab__MoreActionsIcon {
+ display: block;
+ width: 20px;
+ height: 20px;
+ @include light-theme {
+ @include color-svg('../images/icons/v3/more/more.svg', $color-black);
+ }
+ @include dark-theme {
+ @include color-svg('../images/icons/v3/more/more.svg', $color-gray-15);
+ }
+}
diff --git a/stylesheets/components/StoryListItem.scss b/stylesheets/components/StoryListItem.scss
index 5f608551ad81..f06e4dfdb034 100644
--- a/stylesheets/components/StoryListItem.scss
+++ b/stylesheets/components/StoryListItem.scss
@@ -15,7 +15,12 @@
@include keyboard-mode {
&:focus {
- background: $color-gray-65;
+ @include light-theme {
+ background: $color-gray-15;
+ }
+ @include dark-theme {
+ background: $color-gray-65;
+ }
}
}
@@ -23,7 +28,12 @@
// that has not been closed yet (active)
&:hover,
&--active {
- background: $color-gray-65;
+ @include light-theme {
+ background: $color-gray-15;
+ }
+ @include dark-theme {
+ background: $color-gray-65;
+ }
}
}
@@ -60,16 +70,26 @@
&--title {
@include font-body-1-bold;
- color: $color-gray-05;
display: flex;
align-items: center;
+ @include light-theme {
+ color: $color-black;
+ }
+ @include dark-theme {
+ color: $color-gray-05;
+ }
}
&--timestamp,
&--sending,
&--send_failed {
@include font-body-2;
- color: $color-gray-25;
+ @include light-theme {
+ color: $color-gray-60;
+ }
+ @include dark-theme {
+ color: $color-gray-25;
+ }
}
&--send_failed {
@@ -148,32 +168,31 @@
}
&__icon {
+ @mixin StoryListItem__Icon($path) {
+ @include light-theme {
+ @include color-svg($path, $color-black);
+ }
+ @include dark-theme {
+ @include color-svg($path, $color-white);
+ }
+ }
+
&--chat {
- @include color-svg(
- '../images/icons/v3/open/open-compact.svg',
- $color-white
- );
+ @include StoryListItem__Icon('../images/icons/v3/open/open-compact.svg');
}
&--delete {
- @include color-svg(
- '../images/icons/v3/trash/trash-compact.svg',
- $color-white
+ @include StoryListItem__Icon(
+ '../images/icons/v3/trash/trash-compact.svg'
);
}
&--hide {
- @include color-svg(
- '../images/icons/v3/x/x-circle-compact.svg',
- $color-white
- );
+ @include StoryListItem__Icon('../images/icons/v3/x/x-circle-compact.svg');
}
&--info {
- @include color-svg(
- '../images/icons/v3/info/info-compact.svg',
- $color-white
- );
+ @include StoryListItem__Icon('../images/icons/v3/info/info-compact.svg');
}
}
diff --git a/stylesheets/manifest.scss b/stylesheets/manifest.scss
index 0f306c859bce..b86f31e3a6e3 100644
--- a/stylesheets/manifest.scss
+++ b/stylesheets/manifest.scss
@@ -35,6 +35,7 @@
@import './components/BadgeSustainerInstructionsDialog.scss';
@import './components/BetterAvatarBubble.scss';
@import './components/Button.scss';
+@import './components/CallsTab.scss';
@import './components/CallingAudioIndicator.scss';
@import './components/CallingButton.scss';
@import './components/CallingLobby.scss';
@@ -102,6 +103,8 @@
@import './components/MiniPlayer.scss';
@import './components/Modal.scss';
@import './components/MyStories.scss';
+@import './components/NavSidebar.scss';
+@import './components/NavTabs.scss';
@import './components/OutgoingGiftBadgeModal.scss';
@import './components/PermissionsPopup.scss';
@import './components/PlaybackButton.scss';
diff --git a/ts/Bytes.ts b/ts/Bytes.ts
index d7894cffe8c7..1934cc9eac92 100644
--- a/ts/Bytes.ts
+++ b/ts/Bytes.ts
@@ -3,7 +3,7 @@
import { Bytes } from './context/Bytes';
-const bytes = window.SignalContext?.bytes || new Bytes();
+const bytes = globalThis.window?.SignalContext?.bytes || new Bytes();
export function fromBase64(value: string): Uint8Array {
return bytes.fromBase64(value);
diff --git a/ts/Crypto.ts b/ts/Crypto.ts
index 587a08d7af7f..ceb3273b7046 100644
--- a/ts/Crypto.ts
+++ b/ts/Crypto.ts
@@ -7,13 +7,9 @@ import { HKDF } from '@signalapp/libsignal-client';
import * as Bytes from './Bytes';
import { calculateAgreement, generateKeyPair } from './Curve';
-import * as log from './logging/log';
import { HashType, CipherType } from './types/Crypto';
import { ProfileDecryptError } from './types/errors';
-import { UUID, UUID_BYTE_SIZE } from './types/UUID';
-import type { UUIDStringType } from './types/UUID';
-
-export { uuidToBytes } from './util/uuidToBytes';
+import { getBytesSubarray } from './util/uuidToBytes';
export { HashType, CipherType };
@@ -199,12 +195,16 @@ export function decryptSymmetric(
const iv = getZeroes(IV_LENGTH);
const nonce = getFirstBytes(data, NONCE_LENGTH);
- const ciphertext = getBytes(
+ const ciphertext = getBytesSubarray(
data,
NONCE_LENGTH,
data.byteLength - NONCE_LENGTH - MAC_LENGTH
);
- const theirMac = getBytes(data, data.byteLength - MAC_LENGTH, MAC_LENGTH);
+ const theirMac = getBytesSubarray(
+ data,
+ data.byteLength - MAC_LENGTH,
+ MAC_LENGTH
+ );
const cipherKey = hmacSha256(key, nonce);
const macKey = hmacSha256(key, cipherKey);
@@ -353,52 +353,6 @@ export function getFirstBytes(data: Uint8Array, n: number): Uint8Array {
return data.subarray(0, n);
}
-export function getBytes(
- data: Uint8Array,
- start: number,
- n: number
-): Uint8Array {
- return data.subarray(start, start + n);
-}
-
-export function bytesToUuid(bytes: Uint8Array): undefined | UUIDStringType {
- if (bytes.byteLength !== UUID_BYTE_SIZE) {
- log.warn(
- 'bytesToUuid: received an Uint8Array of invalid length. ' +
- 'Returning undefined'
- );
- return undefined;
- }
-
- const uuids = splitUuids(bytes);
- if (uuids.length === 1) {
- return uuids[0] || undefined;
- }
- return undefined;
-}
-
-export function splitUuids(buffer: Uint8Array): Array {
- const uuids = new Array();
- for (let i = 0; i < buffer.byteLength; i += UUID_BYTE_SIZE) {
- const bytes = getBytes(buffer, i, UUID_BYTE_SIZE);
- const hex = Bytes.toHex(bytes);
- const chunks = [
- hex.substring(0, 8),
- hex.substring(8, 12),
- hex.substring(12, 16),
- hex.substring(16, 20),
- hex.substring(20),
- ];
- const uuid = chunks.join('-');
- if (uuid !== '00000000-0000-0000-0000-000000000000') {
- uuids.push(UUID.cast(uuid));
- } else {
- uuids.push(null);
- }
- }
- return uuids;
-}
-
export function trimForDisplay(padded: Uint8Array): Uint8Array {
let paddingEnd = 0;
for (paddingEnd; paddingEnd < padded.length; paddingEnd += 1) {
@@ -588,7 +542,7 @@ export function decryptProfileName(
// SignalContext APIs
//
-const { crypto } = window.SignalContext;
+const { crypto } = globalThis.window?.SignalContext ?? {};
export function sign(key: Uint8Array, data: Uint8Array): Uint8Array {
return crypto.sign(key, data);
diff --git a/ts/RemoteConfig.ts b/ts/RemoteConfig.ts
index b8f7855972da..47472541929e 100644
--- a/ts/RemoteConfig.ts
+++ b/ts/RemoteConfig.ts
@@ -8,8 +8,8 @@ import * as log from './logging/log';
import type { UUIDStringType } from './types/UUID';
import { parseIntOrThrow } from './util/parseIntOrThrow';
import { SECOND, HOUR } from './util/durations';
-import { uuidToBytes } from './util/uuidToBytes';
import * as Bytes from './Bytes';
+import { uuidToBytes } from './util/uuidToBytes';
import { HashType } from './types/Crypto';
import { getCountryCode } from './types/PhoneNumber';
diff --git a/ts/background.ts b/ts/background.ts
index 1d766e5d03b3..7640926c4a69 100644
--- a/ts/background.ts
+++ b/ts/background.ts
@@ -186,6 +186,11 @@ import { parseRemoteClientExpiration } from './util/parseRemoteClientExpiration'
import { makeLookup } from './util/makeLookup';
import { addGlobalKeyboardShortcuts } from './services/addGlobalKeyboardShortcuts';
import { createEventHandler } from './quill/signal-clipboard/util';
+import { onCallLogEventSync } from './util/onCallLogEventSync';
+import {
+ getCallsHistoryForRedux,
+ loadCallsHistory,
+} from './services/callHistoryLoader';
export function isOverHourIntoPast(timestamp: number): boolean {
return isNumber(timestamp) && isOlderThan(timestamp, HOUR);
@@ -434,6 +439,10 @@ export async function startApp(): Promise {
'callEventSync',
queuedEventListener(onCallEventSync, false)
);
+ messageReceiver.addEventListener(
+ 'callLogEventSync',
+ queuedEventListener(onCallLogEventSync, false)
+ );
});
ourProfileKeyService.initialize(window.storage);
@@ -1121,6 +1130,7 @@ export async function startApp(): Promise {
loadInitialBadgesState(),
loadStories(),
loadDistributionLists(),
+ loadCallsHistory(),
window.textsecure.storage.protocol.hydrateCaches(),
(async () => {
mainWindowStats = await window.SignalContext.getMainWindowStats();
@@ -1174,6 +1184,7 @@ export async function startApp(): Promise {
menuOptions,
stories: getStoriesForRedux(),
storyDistributionLists: getDistributionListsForRedux(),
+ callsHistory: getCallsHistoryForRedux(),
});
const store = window.Signal.State.createStore(initialState);
@@ -1193,6 +1204,10 @@ export async function startApp(): Promise {
store.dispatch
),
badges: bindActionCreators(actionCreators.badges, store.dispatch),
+ callHistory: bindActionCreators(
+ actionCreators.callHistory,
+ store.dispatch
+ ),
calling: bindActionCreators(actionCreators.calling, store.dispatch),
composer: bindActionCreators(actionCreators.composer, store.dispatch),
conversations: bindActionCreators(
diff --git a/ts/components/App.tsx b/ts/components/App.tsx
index a350b76c8477..11e1b983b152 100644
--- a/ts/components/App.tsx
+++ b/ts/components/App.tsx
@@ -25,9 +25,7 @@ type PropsType = {
registerSingleDevice: (number: string, code: string) => Promise;
renderCallManager: () => JSX.Element;
renderGlobalModalContainer: () => JSX.Element;
- isShowingStoriesView: boolean;
i18n: LocalizerType;
- renderStories: (closeView: () => unknown) => JSX.Element;
hasSelectedStoryData: boolean;
renderStoryViewer: (closeView: () => unknown) => JSX.Element;
renderLightbox: () => JSX.Element | null;
@@ -53,7 +51,6 @@ type PropsType = {
titleBarDoubleClick: () => void;
toast?: AnyToast;
scrollToMessage: (conversationId: string, messageId: string) => unknown;
- toggleStoriesView: () => unknown;
viewStory: ViewStoryActionCreatorType;
renderInbox: () => JSX.Element;
};
@@ -69,7 +66,6 @@ export function App({
i18n,
isFullScreen,
isMaximized,
- isShowingStoriesView,
menuOptions,
onUndoArchive,
openFileInFolder,
@@ -81,13 +77,11 @@ export function App({
renderGlobalModalContainer,
renderInbox,
renderLightbox,
- renderStories,
renderStoryViewer,
requestVerification,
theme,
titleBarDoubleClick,
toast,
- toggleStoriesView,
viewStory,
}: PropsType): JSX.Element {
let contents;
@@ -183,7 +177,6 @@ export function App({
{renderGlobalModalContainer()}
{renderCallManager()}
{renderLightbox()}
- {isShowingStoriesView && renderStories(toggleStoriesView)}
{hasSelectedStoryData &&
renderStoryViewer(() => viewStory({ closeViewer: true }))}
diff --git a/ts/components/Avatar.tsx b/ts/components/Avatar.tsx
index e3db4cacb3cd..8493e906f8bf 100644
--- a/ts/components/Avatar.tsx
+++ b/ts/components/Avatar.tsx
@@ -12,6 +12,7 @@ import React, { useEffect, useState } from 'react';
import classNames from 'classnames';
import { noop } from 'lodash';
+import { filterDOMProps } from '@react-aria/utils';
import type { AvatarColorType } from '../types/Colors';
import type { BadgeType } from '../badges/types';
import type { LocalizerType } from '../types/Util';
@@ -239,7 +240,7 @@ export function Avatar({
if (onClick) {
contents = (
-
-
-
+
{hasPendingUpdate && (
-
+ <>
+
+
+ >
)}
);
diff --git a/ts/components/Button.tsx b/ts/components/Button.tsx
index ee0485ed881b..ce39c7bb45f3 100644
--- a/ts/components/Button.tsx
+++ b/ts/components/Button.tsx
@@ -33,6 +33,7 @@ export enum ButtonVariant {
export enum ButtonIconType {
audio = 'audio',
+ message = 'message',
muted = 'muted',
search = 'search',
unmuted = 'unmuted',
diff --git a/ts/components/CallsList.tsx b/ts/components/CallsList.tsx
new file mode 100644
index 000000000000..563bfadea695
--- /dev/null
+++ b/ts/components/CallsList.tsx
@@ -0,0 +1,476 @@
+// Copyright 2023 Signal Messenger, LLC
+// SPDX-License-Identifier: AGPL-3.0-only
+
+import type { ChangeEvent } from 'react';
+import React, {
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from 'react';
+import type { Index, IndexRange, ListRowProps } from 'react-virtualized';
+import { InfiniteLoader, List } from 'react-virtualized';
+import classNames from 'classnames';
+import type { LocalizerType } from '../types/I18N';
+import { ListTile } from './ListTile';
+import { Avatar, AvatarSize } from './Avatar';
+import { SearchInput } from './SearchInput';
+import type {
+ CallHistoryFilterOptions,
+ CallHistoryGroup,
+ CallHistoryPagination,
+} from '../types/CallDisposition';
+import {
+ CallHistoryFilterStatus,
+ CallDirection,
+ CallType,
+ DirectCallStatus,
+ GroupCallStatus,
+ isSameCallHistoryGroup,
+} from '../types/CallDisposition';
+import { formatDateTimeShort } from '../util/timestamp';
+import type { ConversationType } from '../state/ducks/conversations';
+import * as log from '../logging/log';
+import { refMerger } from '../util/refMerger';
+import { drop } from '../util/drop';
+import { strictAssert } from '../util/assert';
+import { UserText } from './UserText';
+import { Intl } from './Intl';
+import { NavSidebarSearchHeader } from './NavSidebar';
+import { SizeObserver } from '../hooks/useSizeObserver';
+import { formatCallHistoryGroup } from '../util/callDisposition';
+
+function Timestamp({
+ i18n,
+ timestamp,
+}: {
+ i18n: LocalizerType;
+ timestamp: number;
+}): JSX.Element {
+ const [now, setNow] = useState(() => Date.now());
+
+ useEffect(() => {
+ const timer = setInterval(() => {
+ setNow(Date.now());
+ }, 1_000);
+
+ return () => {
+ clearInterval(timer);
+ };
+ }, []);
+
+ const dateTime = useMemo(() => {
+ return new Date(timestamp).toISOString();
+ }, [timestamp]);
+
+ const formatted = useMemo(() => {
+ void now; // Use this as a dep so we update
+ return formatDateTimeShort(i18n, timestamp);
+ }, [i18n, timestamp, now]);
+
+ return ;
+}
+
+type SearchResults = Readonly<{
+ count: number;
+ items: ReadonlyArray;
+}>;
+
+type SearchState = Readonly<{
+ state: 'init' | 'pending' | 'rejected' | 'fulfilled';
+ // Note these fields shouldnt be updated until the search is fulfilled or rejected.
+ options: null | { query: string; status: CallHistoryFilterStatus };
+ results: null | SearchResults;
+}>;
+
+const defaultInitState: SearchState = {
+ state: 'init',
+ options: null,
+ results: null,
+};
+
+const defaultPendingState: SearchState = {
+ state: 'pending',
+ options: null,
+ results: {
+ count: 100,
+ items: [],
+ },
+};
+
+type CallsListProps = Readonly<{
+ getCallHistoryGroupsCount: (
+ options: CallHistoryFilterOptions
+ ) => Promise;
+ getCallHistoryGroups: (
+ options: CallHistoryFilterOptions,
+ pagination: CallHistoryPagination
+ ) => Promise>;
+ getConversation: (id: string) => ConversationType | void;
+ i18n: LocalizerType;
+ selectedCallHistoryGroup: CallHistoryGroup | null;
+ onSelectCallHistoryGroup: (
+ conversationId: string,
+ selectedCallHistoryGroup: CallHistoryGroup
+ ) => void;
+}>;
+
+function rowHeight() {
+ return ListTile.heightCompact;
+}
+
+export function CallsList({
+ getCallHistoryGroupsCount,
+ getCallHistoryGroups,
+ getConversation,
+ i18n,
+ selectedCallHistoryGroup,
+ onSelectCallHistoryGroup,
+}: CallsListProps): JSX.Element {
+ const infiniteLoaderRef = useRef(null);
+ const listRef = useRef(null);
+ const [queryInput, setQueryInput] = useState('');
+ const [status, setStatus] = useState(CallHistoryFilterStatus.All);
+ const [searchState, setSearchState] = useState(defaultInitState);
+
+ useEffect(() => {
+ const controller = new AbortController();
+
+ async function search() {
+ const options = {
+ query: queryInput.toLowerCase().normalize().trim(),
+ status,
+ };
+
+ let timer = setTimeout(() => {
+ setSearchState(prevSearchState => {
+ if (prevSearchState.state === 'init') {
+ return defaultPendingState;
+ }
+ return prevSearchState;
+ });
+ timer = setTimeout(() => {
+ // Show loading indicator after a delay
+ setSearchState(defaultPendingState);
+ }, 300);
+ }, 50);
+
+ let results: SearchResults | null = null;
+
+ try {
+ const [count, items] = await Promise.all([
+ getCallHistoryGroupsCount(options),
+ getCallHistoryGroups(options, {
+ offset: 0,
+ limit: 100, // preloaded rows
+ }),
+ ]);
+ results = { count, items };
+ } catch (error) {
+ log.error('CallsList#fetchTotal error fetching', error);
+ }
+
+ // Clear the loading indicator timeout
+ clearTimeout(timer);
+
+ // Ignore old requests
+ if (controller.signal.aborted) {
+ return;
+ }
+
+ // Only commit the new search state once the results are ready
+ setSearchState({
+ state: results == null ? 'rejected' : 'fulfilled',
+ options,
+ results,
+ });
+ infiniteLoaderRef.current?.resetLoadMoreRowsCache(true);
+ listRef.current?.scrollToPosition(0);
+ }
+
+ drop(search());
+
+ return () => {
+ controller.abort();
+ };
+ }, [getCallHistoryGroupsCount, getCallHistoryGroups, queryInput, status]);
+
+ const loadMoreRows = useCallback(
+ async (props: IndexRange) => {
+ const { state, options } = searchState;
+ if (state !== 'fulfilled') {
+ return;
+ }
+ strictAssert(
+ options != null,
+ 'options should never be null when status is fulfilled'
+ );
+
+ let { startIndex, stopIndex } = props;
+
+ if (startIndex > stopIndex) {
+ // flip
+ [startIndex, stopIndex] = [stopIndex, startIndex];
+ }
+
+ const offset = startIndex;
+ const limit = stopIndex - startIndex + 1;
+
+ try {
+ const groups = await getCallHistoryGroups(options, { offset, limit });
+
+ if (searchState.options !== options) {
+ return;
+ }
+
+ setSearchState(prevSearchState => {
+ strictAssert(
+ prevSearchState.results != null,
+ 'results should never be null here'
+ );
+ const newItems = prevSearchState.results.items.slice();
+ newItems.splice(startIndex, stopIndex, ...groups);
+ return {
+ ...prevSearchState,
+ results: {
+ ...prevSearchState.results,
+ items: newItems,
+ },
+ };
+ });
+ } catch (error) {
+ log.error('CallsList#loadMoreRows error fetching', error);
+ }
+ },
+ [getCallHistoryGroups, searchState]
+ );
+
+ const isRowLoaded = useCallback(
+ (props: Index) => {
+ return searchState.results?.items[props.index] != null;
+ },
+ [searchState]
+ );
+
+ const rowRenderer = useCallback(
+ ({ key, index, style }: ListRowProps) => {
+ const item = searchState.results?.items.at(index) ?? null;
+ const conversation = item != null ? getConversation(item.peerId) : null;
+
+ if (
+ searchState.state === 'pending' ||
+ item == null ||
+ conversation == null
+ ) {
+ return (
+