diff --git a/assets/licenses/u2net.txt b/assets/licenses/u2net.txt new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/assets/licenses/u2net.txt @@ -0,0 +1,201 @@ + 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 [yyyy] [name of copyright owner] + + 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. diff --git a/lib/core/bridge/api.dart b/lib/core/bridge/api.dart index 0cb858e5..ccde0ea6 100644 --- a/lib/core/bridge/api.dart +++ b/lib/core/bridge/api.dart @@ -6,6 +6,7 @@ import '../api/server/prekeys.dart'; import '../frb_generated.dart'; import '../services/media_upload.dart'; +import '../services/stickers.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; import 'stories.dart'; @@ -346,6 +347,14 @@ class RustApi { static Future connectionState() => RustLib.instance.api.crateBridgeApiRustApiConnectionState(); + /// Creates a transparent, message-sized sticker from a local image. Model + /// inference and image encoding are CPU-heavy, so neither runs on Tokio's + /// async worker threads. + static Future createSticker({required String imagePath}) => + RustLib.instance.api.crateBridgeApiRustApiCreateSticker( + imagePath: imagePath, + ); + /// Trims fully transparent borders an editor left around a stored image and /// refreshes the preview and content hash derived from it. static Future cropMediaTransparentBorders({required String mediaId}) => diff --git a/lib/core/frb_generated.dart b/lib/core/frb_generated.dart index 5f52fdf0..71640a8f 100644 --- a/lib/core/frb_generated.dart +++ b/lib/core/frb_generated.dart @@ -28,6 +28,7 @@ import 'keys/backup_password_keys.dart'; import 'lib.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; import 'services/media_upload.dart'; +import 'services/stickers.dart'; import 'signal/engine.dart'; import 'user_config.dart'; @@ -88,7 +89,7 @@ class RustLib extends BaseEntrypoint { String get codegenVersion => '2.12.0'; @override - int get rustContentHash => -1336092087; + int get rustContentHash => -1844664049; static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig( @@ -270,6 +271,10 @@ abstract class RustLibApi extends BaseApi { Future crateBridgeApiRustApiConnectionState(); + Future crateBridgeApiRustApiCreateSticker({ + required String imagePath, + }); + Future crateBridgeApiRustApiCropMediaTransparentBorders({ required String mediaId, }); @@ -2489,6 +2494,39 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: [], ); + @override + Future crateBridgeApiRustApiCreateSticker({ + required String imagePath, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(imagePath, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 52, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_sticker_output, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateBridgeApiRustApiCreateStickerConstMeta, + argValues: [imagePath], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateBridgeApiRustApiCreateStickerConstMeta => + const TaskConstMeta( + debugName: "rust_api_create_sticker", + argNames: ["imagePath"], + ); + @override Future crateBridgeApiRustApiCropMediaTransparentBorders({ required String mediaId, @@ -2501,7 +2539,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 52, + funcId: 53, port: port_, ); }, @@ -2532,7 +2570,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 53, + funcId: 54, port: port_, ); }, @@ -2562,7 +2600,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_list_prim_u_8_loose(avatarSvgCompressed, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 54)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 55)!; }, codec: SseCodec( decodeSuccessData: sse_decode_String, @@ -2590,7 +2628,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 55, + funcId: 56, port: port_, ); }, @@ -2621,7 +2659,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 56, + funcId: 57, port: port_, ); }, @@ -2654,7 +2692,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 57, + funcId: 58, port: port_, ); }, @@ -2684,7 +2722,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 58, + funcId: 59, port: port_, ); }, @@ -2715,7 +2753,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 59, + funcId: 60, port: port_, ); }, @@ -2746,7 +2784,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 60, + funcId: 61, port: port_, ); }, @@ -2776,7 +2814,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 61, + funcId: 62, port: port_, ); }, @@ -2809,7 +2847,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 62, + funcId: 63, port: port_, ); }, @@ -2844,7 +2882,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 63, + funcId: 64, port: port_, ); }, @@ -2877,7 +2915,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 64, + funcId: 65, port: port_, ); }, @@ -2909,7 +2947,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 65, + funcId: 66, port: port_, ); }, @@ -2939,7 +2977,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 66, + funcId: 67, port: port_, ); }, @@ -2974,7 +3012,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 67, + funcId: 68, port: port_, ); }, @@ -3004,7 +3042,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 68, + funcId: 69, port: port_, ); }, @@ -3034,7 +3072,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 69, + funcId: 70, port: port_, ); }, @@ -3064,7 +3102,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 70, + funcId: 71, port: port_, ); }, @@ -3105,7 +3143,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 71, + funcId: 72, port: port_, ); }, @@ -3152,7 +3190,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 72, + funcId: 73, port: port_, ); }, @@ -3185,7 +3223,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 73, + funcId: 74, port: port_, ); }, @@ -3218,7 +3256,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 74, + funcId: 75, port: port_, ); }, @@ -3248,7 +3286,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 75, + funcId: 76, port: port_, ); }, @@ -3288,7 +3326,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 76, + funcId: 77, port: port_, ); }, @@ -3321,7 +3359,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 77, + funcId: 78, port: port_, ); }, @@ -3360,7 +3398,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 78, + funcId: 79, port: port_, ); }, @@ -3396,7 +3434,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 79, + funcId: 80, port: port_, ); }, @@ -3431,7 +3469,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 80, + funcId: 81, port: port_, ); }, @@ -3473,7 +3511,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 81, + funcId: 82, port: port_, ); }, @@ -3515,7 +3553,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 82, + funcId: 83, port: port_, ); }, @@ -3545,7 +3583,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 83, + funcId: 84, port: port_, ); }, @@ -3578,7 +3616,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 84, + funcId: 85, port: port_, ); }, @@ -3613,7 +3651,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 85, + funcId: 86, port: port_, ); }, @@ -3643,7 +3681,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 86, + funcId: 87, port: port_, ); }, @@ -3678,7 +3716,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 87, + funcId: 88, port: port_, ); }, @@ -3708,7 +3746,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 88, + funcId: 89, port: port_, ); }, @@ -3741,7 +3779,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 89, + funcId: 90, port: port_, ); }, @@ -3771,7 +3809,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 90, + funcId: 91, port: port_, ); }, @@ -3801,7 +3839,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 91, + funcId: 92, port: port_, ); }, @@ -3831,7 +3869,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 92, + funcId: 93, port: port_, ); }, @@ -3861,7 +3899,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 93, + funcId: 94, port: port_, ); }, @@ -3900,7 +3938,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 94, + funcId: 95, port: port_, ); }, @@ -3935,7 +3973,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 95, + funcId: 96, port: port_, ); }, @@ -3974,7 +4012,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 96, + funcId: 97, port: port_, ); }, @@ -4016,7 +4054,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 97, + funcId: 98, port: port_, ); }, @@ -4047,7 +4085,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 98, + funcId: 99, port: port_, ); }, @@ -4080,7 +4118,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 99, + funcId: 100, port: port_, ); }, @@ -4113,7 +4151,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 100, + funcId: 101, port: port_, ); }, @@ -4148,7 +4186,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 101, + funcId: 102, port: port_, ); }, @@ -4181,7 +4219,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 102, + funcId: 103, port: port_, ); }, @@ -4214,7 +4252,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 103, + funcId: 104, port: port_, ); }, @@ -4247,7 +4285,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 104, + funcId: 105, port: port_, ); }, @@ -4284,7 +4322,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 105, + funcId: 106, port: port_, ); }, @@ -4314,7 +4352,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 106, + funcId: 107, port: port_, ); }, @@ -4344,7 +4382,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 107, + funcId: 108, port: port_, ); }, @@ -4374,7 +4412,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 108, + funcId: 109, port: port_, ); }, @@ -4407,7 +4445,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 109, + funcId: 110, port: port_, ); }, @@ -4438,7 +4476,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 110, + funcId: 111, port: port_, ); }, @@ -4471,7 +4509,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 111, + funcId: 112, port: port_, ); }, @@ -4517,7 +4555,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 112, + funcId: 113, port: port_, ); }, @@ -4573,7 +4611,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 113, + funcId: 114, port: port_, ); }, @@ -4623,7 +4661,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 114, + funcId: 115, port: port_, ); }, @@ -4668,7 +4706,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 115, + funcId: 116, port: port_, ); }, @@ -4703,7 +4741,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 116, + funcId: 117, port: port_, ); }, @@ -4738,7 +4776,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 117, + funcId: 118, port: port_, ); }, @@ -4771,7 +4809,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 118, + funcId: 119, port: port_, ); }, @@ -4808,7 +4846,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 119, + funcId: 120, port: port_, ); }, @@ -4839,7 +4877,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 120, + funcId: 121, port: port_, ); }, @@ -4877,7 +4915,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 121, + funcId: 122, port: port_, ); }, @@ -4912,7 +4950,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 122, + funcId: 123, port: port_, ); }, @@ -4951,7 +4989,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 123, + funcId: 124, port: port_, ); }, @@ -4984,7 +5022,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 124, + funcId: 125, port: port_, ); }, @@ -5015,7 +5053,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 125, + funcId: 126, port: port_, ); }, @@ -5050,7 +5088,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 126, + funcId: 127, port: port_, ); }, @@ -5080,7 +5118,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 127, + funcId: 128, port: port_, ); }, @@ -5113,7 +5151,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 128, + funcId: 129, port: port_, ); }, @@ -5148,7 +5186,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 129, + funcId: 130, port: port_, ); }, @@ -5181,7 +5219,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 130, + funcId: 131, port: port_, ); }, @@ -5212,7 +5250,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 131, + funcId: 132, port: port_, ); }, @@ -5249,7 +5287,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 132, + funcId: 133, port: port_, ); }, @@ -5301,7 +5339,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 133, + funcId: 134, port: port_, ); }, @@ -5354,7 +5392,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 134, + funcId: 135, port: port_, ); }, @@ -5394,7 +5432,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 135, + funcId: 136, port: port_, ); }, @@ -5427,7 +5465,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 136, + funcId: 137, port: port_, ); }, @@ -5460,7 +5498,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 137, + funcId: 138, port: port_, ); }, @@ -5497,7 +5535,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 138, + funcId: 139, port: port_, ); }, @@ -5529,7 +5567,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 139, + funcId: 140, port: port_, ); }, @@ -5562,7 +5600,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 140, + funcId: 141, port: port_, ); }, @@ -5597,7 +5635,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 141, + funcId: 142, port: port_, ); }, @@ -5629,7 +5667,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 142, + funcId: 143, port: port_, ); }, @@ -5667,7 +5705,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 143, + funcId: 144, port: port_, ); }, @@ -5700,7 +5738,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 144, + funcId: 145, port: port_, ); }, @@ -5738,7 +5776,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 145, + funcId: 146, port: port_, ); }, @@ -5775,7 +5813,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 146, + funcId: 147, port: port_, ); }, @@ -5812,7 +5850,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 147, + funcId: 148, port: port_, ); }, @@ -5850,7 +5888,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 148, + funcId: 149, port: port_, ); }, @@ -5888,7 +5926,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 149, + funcId: 150, port: port_, ); }, @@ -5921,7 +5959,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 150, + funcId: 151, port: port_, ); }, @@ -5953,7 +5991,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 151, + funcId: 152, port: port_, ); }, @@ -5988,7 +6026,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 152, + funcId: 153, port: port_, ); }, @@ -6025,7 +6063,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 153, + funcId: 154, port: port_, ); }, @@ -6058,7 +6096,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 154, + funcId: 155, port: port_, ); }, @@ -6090,7 +6128,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 155, + funcId: 156, port: port_, ); }, @@ -6125,7 +6163,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 156, + funcId: 157, port: port_, ); }, @@ -6164,7 +6202,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 157, + funcId: 158, port: port_, ); }, @@ -6201,7 +6239,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 158, + funcId: 159, port: port_, ); }, @@ -6231,7 +6269,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 159, + funcId: 160, port: port_, ); }, @@ -6263,7 +6301,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 160, + funcId: 161, port: port_, ); }, @@ -6298,7 +6336,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 161, + funcId: 162, port: port_, ); }, @@ -6333,7 +6371,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 162, + funcId: 163, port: port_, ); }, @@ -6364,7 +6402,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 163, + funcId: 164, port: port_, ); }, @@ -6402,7 +6440,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 164, + funcId: 165, port: port_, ); }, @@ -6437,7 +6475,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 165, + funcId: 166, port: port_, ); }, @@ -6476,7 +6514,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 166, + funcId: 167, port: port_, ); }, @@ -6511,7 +6549,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 167, + funcId: 168, port: port_, ); }, @@ -6556,7 +6594,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 168, + funcId: 169, port: port_, ); }, @@ -6599,7 +6637,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 169, + funcId: 170, port: port_, ); }, @@ -6631,7 +6669,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 170, + funcId: 171, port: port_, ); }, @@ -6666,7 +6704,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 171, + funcId: 172, port: port_, ); }, @@ -6701,7 +6739,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 172, + funcId: 173, port: port_, ); }, @@ -6736,7 +6774,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 173, + funcId: 174, port: port_, ); }, @@ -6769,7 +6807,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 174, + funcId: 175, )!; }, codec: SseCodec( @@ -6809,7 +6847,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 175, + funcId: 176, port: port_, ); }, @@ -6854,7 +6892,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 176, + funcId: 177, port: port_, ); }, @@ -6884,7 +6922,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 177, + funcId: 178, port: port_, ); }, @@ -6917,7 +6955,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 178, + funcId: 179, port: port_, ); }, @@ -6952,7 +6990,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 179, + funcId: 180, port: port_, ); }, @@ -6991,7 +7029,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 180, + funcId: 181, )!; }, codec: SseCodec( @@ -7996,6 +8034,21 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + StickerOutput dco_decode_sticker_output(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 5) + throw Exception('unexpected arr length: expect 5 but see ${arr.length}'); + return StickerOutput( + webp: dco_decode_list_prim_u_8_strict(arr[0]), + sha256: dco_decode_list_prim_u_8_strict(arr[1]), + contentHash: dco_decode_String(arr[2]), + width: dco_decode_u_32(arr[3]), + height: dco_decode_u_32(arr[4]), + ); + } + @protected StoryAudience dco_decode_story_audience(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -9343,6 +9396,23 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + StickerOutput sse_decode_sticker_output(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_webp = sse_decode_list_prim_u_8_strict(deserializer); + var var_sha256 = sse_decode_list_prim_u_8_strict(deserializer); + var var_contentHash = sse_decode_String(deserializer); + var var_width = sse_decode_u_32(deserializer); + var var_height = sse_decode_u_32(deserializer); + return StickerOutput( + webp: var_webp, + sha256: var_sha256, + contentHash: var_contentHash, + width: var_width, + height: var_height, + ); + } + @protected StoryAudience sse_decode_story_audience(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -10750,6 +10820,16 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_opt_list_prim_u_8_strict(self.blobValue, serializer); } + @protected + void sse_encode_sticker_output(StickerOutput self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_list_prim_u_8_strict(self.webp, serializer); + sse_encode_list_prim_u_8_strict(self.sha256, serializer); + sse_encode_String(self.contentHash, serializer); + sse_encode_u_32(self.width, serializer); + sse_encode_u_32(self.height, serializer); + } + @protected void sse_encode_story_audience(StoryAudience self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs diff --git a/lib/core/frb_generated.io.dart b/lib/core/frb_generated.io.dart index 50424888..d5bd0fda 100644 --- a/lib/core/frb_generated.io.dart +++ b/lib/core/frb_generated.io.dart @@ -27,6 +27,7 @@ import 'keys/backup_password_keys.dart'; import 'lib.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_io.dart'; import 'services/media_upload.dart'; +import 'services/stickers.dart'; import 'signal/engine.dart'; import 'user_config.dart'; @@ -383,6 +384,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected SqlValue dco_decode_sql_value(dynamic raw); + @protected + StickerOutput dco_decode_sticker_output(dynamic raw); + @protected StoryAudience dco_decode_story_audience(dynamic raw); @@ -847,6 +851,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected SqlValue sse_decode_sql_value(SseDeserializer deserializer); + @protected + StickerOutput sse_decode_sticker_output(SseDeserializer deserializer); + @protected StoryAudience sse_decode_story_audience(SseDeserializer deserializer); @@ -1432,6 +1439,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_sql_value(SqlValue self, SseSerializer serializer); + @protected + void sse_encode_sticker_output(StickerOutput self, SseSerializer serializer); + @protected void sse_encode_story_audience(StoryAudience self, SseSerializer serializer); diff --git a/lib/core/frb_generated.web.dart b/lib/core/frb_generated.web.dart index 65a46b6e..b83b111d 100644 --- a/lib/core/frb_generated.web.dart +++ b/lib/core/frb_generated.web.dart @@ -29,6 +29,7 @@ import 'keys/backup_password_keys.dart'; import 'lib.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_web.dart'; import 'services/media_upload.dart'; +import 'services/stickers.dart'; import 'signal/engine.dart'; import 'user_config.dart'; @@ -385,6 +386,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected SqlValue dco_decode_sql_value(dynamic raw); + @protected + StickerOutput dco_decode_sticker_output(dynamic raw); + @protected StoryAudience dco_decode_story_audience(dynamic raw); @@ -849,6 +853,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected SqlValue sse_decode_sql_value(SseDeserializer deserializer); + @protected + StickerOutput sse_decode_sticker_output(SseDeserializer deserializer); + @protected StoryAudience sse_decode_story_audience(SseDeserializer deserializer); @@ -1434,6 +1441,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_sql_value(SqlValue self, SseSerializer serializer); + @protected + void sse_encode_sticker_output(StickerOutput self, SseSerializer serializer); + @protected void sse_encode_story_audience(StoryAudience self, SseSerializer serializer); diff --git a/lib/core/services/stickers.dart b/lib/core/services/stickers.dart new file mode 100644 index 00000000..e972e71c --- /dev/null +++ b/lib/core/services/stickers.dart @@ -0,0 +1,42 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.12.0. + +// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import + +import '../frb_generated.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; + +class StickerOutput { + final Uint8List webp; + final Uint8List sha256; + final String contentHash; + final int width; + final int height; + + const StickerOutput({ + required this.webp, + required this.sha256, + required this.contentHash, + required this.width, + required this.height, + }); + + @override + int get hashCode => + webp.hashCode ^ + sha256.hashCode ^ + contentHash.hashCode ^ + width.hashCode ^ + height.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is StickerOutput && + runtimeType == other.runtimeType && + webp == other.webp && + sha256 == other.sha256 && + contentHash == other.contentHash && + width == other.width && + height == other.height; +} diff --git a/lib/main.dart b/lib/main.dart index d595c1ef..75d10356 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'package:camera/camera.dart'; import 'package:drift/native.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:mutex/mutex.dart'; @@ -94,6 +95,12 @@ class StartupResult { void main() { SentryWidgetsFlutterBinding.ensureInitialized(); + LicenseRegistry.addLicense(() async* { + yield LicenseEntryWithLineBreaks( + ['U²-Net-P'], + await rootBundle.loadString('assets/licenses/u2net.txt'), + ); + }); // `App` used to set this as it mounted, which was at the same moment the // engine started. It now mounts only once startup finishes, and the API can diff --git a/lib/src/database/daos/messages.dao.dart b/lib/src/database/daos/messages.dao.dart index 6cfe34c9..691daba7 100644 --- a/lib/src/database/daos/messages.dao.dart +++ b/lib/src/database/daos/messages.dao.dart @@ -401,6 +401,7 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { isDeletedFromSender: Value(true), content: Value(null), mediaId: Value(null), + additionalMessageData: Value(null), ), ); } diff --git a/lib/src/database/daos/stickers.dao.dart b/lib/src/database/daos/stickers.dao.dart new file mode 100644 index 00000000..8d78639a --- /dev/null +++ b/lib/src/database/daos/stickers.dao.dart @@ -0,0 +1,81 @@ +import 'package:convert/convert.dart'; +import 'package:crypto/crypto.dart'; +import 'package:drift/drift.dart'; +import 'package:twonly/src/database/tables/stickers.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; + +part 'stickers.dao.g.dart'; + +@DriftAccessor(tables: [Stickers]) +class StickersDao extends DatabaseAccessor with _$StickersDaoMixin { + // ignore: matching_super_parameters + StickersDao(super.db); + + Stream> watchAll() { + return (select(stickers)..orderBy([ + (sticker) => OrderingTerm.desc(sticker.usageCount), + (sticker) => OrderingTerm.desc(sticker.lastUsedAt), + (sticker) => OrderingTerm.desc(sticker.createdAt), + ])) + .watch(); + } + + Future getByHash(String contentHash) { + return (select(stickers) + ..where((sticker) => sticker.contentHash.equals(contentHash))) + .getSingleOrNull(); + } + + /// Saves verified sticker bytes. The digest is always recomputed here so a + /// malformed received message can never create a duplicate under a false id. + Future save({ + required Uint8List webp, + required int width, + required int height, + String? expectedHash, + }) async { + if (webp.isEmpty || webp.length > 128 * 1024) { + throw ArgumentError.value(webp.length, 'webp', 'invalid sticker size'); + } + if (width < 1 || width > 300 || height < 1 || height > 300) { + throw ArgumentError('invalid sticker dimensions: ${width}x$height'); + } + final contentHash = hex.encode(sha256.convert(webp).bytes); + if (expectedHash != null && expectedHash != contentHash) { + throw ArgumentError('sticker digest does not match its bytes'); + } + final now = DateTime.now(); + return transaction(() async { + if (await getByHash(contentHash) != null) return false; + await into(stickers).insert( + StickersCompanion.insert( + contentHash: contentHash, + webp: webp, + width: width, + height: height, + createdAt: Value(now), + lastUsedAt: Value(now), + ), + ); + return true; + }); + } + + Future recordUse(String contentHash) async { + await customUpdate( + 'UPDATE stickers SET usage_count = usage_count + 1, ' + 'last_used_at = ? WHERE content_hash = ?', + variables: [ + Variable(DateTime.now()), + Variable(contentHash), + ], + updates: {stickers}, + ); + } + + Future remove(String contentHash) { + return (delete( + stickers, + )..where((sticker) => sticker.contentHash.equals(contentHash))).go(); + } +} diff --git a/lib/src/database/daos/stickers.dao.g.dart b/lib/src/database/daos/stickers.dao.g.dart new file mode 100644 index 00000000..8662b5c9 --- /dev/null +++ b/lib/src/database/daos/stickers.dao.g.dart @@ -0,0 +1,16 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'stickers.dao.dart'; + +// ignore_for_file: type=lint +mixin _$StickersDaoMixin on DatabaseAccessor { + $StickersTable get stickers => attachedDatabase.stickers; + StickersDaoManager get managers => StickersDaoManager(this); +} + +class StickersDaoManager { + final _$StickersDaoMixin _db; + StickersDaoManager(this._db); + $$StickersTableTableManager get stickers => + $$StickersTableTableManager(_db.attachedDatabase, _db.stickers); +} diff --git a/lib/src/database/tables/stickers.table.dart b/lib/src/database/tables/stickers.table.dart new file mode 100644 index 00000000..35b5c9b6 --- /dev/null +++ b/lib/src/database/tables/stickers.table.dart @@ -0,0 +1,15 @@ +import 'package:drift/drift.dart'; + +@DataClassName('LocalSticker') +class Stickers extends Table { + TextColumn get contentHash => text().withLength(min: 64, max: 64)(); + BlobColumn get webp => blob()(); + IntColumn get width => integer()(); + IntColumn get height => integer()(); + IntColumn get usageCount => integer().withDefault(const Constant(0))(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get lastUsedAt => dateTime().withDefault(currentDateAndTime)(); + + @override + Set get primaryKey => {contentHash}; +} diff --git a/lib/src/database/twonly.db.dart b/lib/src/database/twonly.db.dart index 5d835d9d..69f831fd 100644 --- a/lib/src/database/twonly.db.dart +++ b/lib/src/database/twonly.db.dart @@ -9,6 +9,7 @@ import 'package:twonly/src/database/daos/mediafiles.dao.dart'; import 'package:twonly/src/database/daos/messages.dao.dart'; import 'package:twonly/src/database/daos/reactions.dao.dart'; import 'package:twonly/src/database/daos/receipts.dao.dart'; +import 'package:twonly/src/database/daos/stickers.dao.dart'; import 'package:twonly/src/database/daos/stories.dao.dart'; import 'package:twonly/src/database/daos/user_discovery.dao.dart'; import 'package:twonly/src/database/rust_change_notifier.dart'; @@ -20,6 +21,7 @@ import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/tables/reactions.table.dart'; import 'package:twonly/src/database/tables/receipts.table.dart'; +import 'package:twonly/src/database/tables/stickers.table.dart'; import 'package:twonly/src/database/tables/user_discovery.table.dart'; import 'package:twonly/src/database/tables/webxdc.table.dart'; import 'package:twonly/src/database/twonly.db.steps.dart'; @@ -53,6 +55,7 @@ part 'twonly.db.g.dart'; WebxdcApps, WebxdcInstances, WebxdcUpdates, + Stickers, ], daos: [ MessagesDao, @@ -65,6 +68,7 @@ part 'twonly.db.g.dart'; KeyVerificationDao, ContactGroupsDao, StoriesDao, + StickersDao, ], ) class TwonlyDB extends _$TwonlyDB { diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart index 99242268..1d1e57c0 100644 --- a/lib/src/database/twonly.db.g.dart +++ b/lib/src/database/twonly.db.g.dart @@ -14969,6 +14969,479 @@ class WebxdcUpdatesCompanion extends UpdateCompanion { } } +class $StickersTable extends Stickers + with TableInfo<$StickersTable, LocalSticker> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $StickersTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _contentHashMeta = const VerificationMeta( + 'contentHash', + ); + @override + late final GeneratedColumn contentHash = GeneratedColumn( + 'content_hash', + aliasedName, + false, + additionalChecks: GeneratedColumn.checkTextLength( + minTextLength: 64, + maxTextLength: 64, + ), + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _webpMeta = const VerificationMeta('webp'); + @override + late final GeneratedColumn webp = GeneratedColumn( + 'webp', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + ); + static const VerificationMeta _widthMeta = const VerificationMeta('width'); + @override + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + static const VerificationMeta _heightMeta = const VerificationMeta('height'); + @override + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + static const VerificationMeta _usageCountMeta = const VerificationMeta( + 'usageCount', + ); + @override + late final GeneratedColumn usageCount = GeneratedColumn( + 'usage_count', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0), + ); + static const VerificationMeta _createdAtMeta = const VerificationMeta( + 'createdAt', + ); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + static const VerificationMeta _lastUsedAtMeta = const VerificationMeta( + 'lastUsedAt', + ); + @override + late final GeneratedColumn lastUsedAt = GeneratedColumn( + 'last_used_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + @override + List get $columns => [ + contentHash, + webp, + width, + height, + usageCount, + createdAt, + lastUsedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'stickers'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('content_hash')) { + context.handle( + _contentHashMeta, + contentHash.isAcceptableOrUnknown( + data['content_hash']!, + _contentHashMeta, + ), + ); + } else if (isInserting) { + context.missing(_contentHashMeta); + } + if (data.containsKey('webp')) { + context.handle( + _webpMeta, + webp.isAcceptableOrUnknown(data['webp']!, _webpMeta), + ); + } else if (isInserting) { + context.missing(_webpMeta); + } + if (data.containsKey('width')) { + context.handle( + _widthMeta, + width.isAcceptableOrUnknown(data['width']!, _widthMeta), + ); + } else if (isInserting) { + context.missing(_widthMeta); + } + if (data.containsKey('height')) { + context.handle( + _heightMeta, + height.isAcceptableOrUnknown(data['height']!, _heightMeta), + ); + } else if (isInserting) { + context.missing(_heightMeta); + } + if (data.containsKey('usage_count')) { + context.handle( + _usageCountMeta, + usageCount.isAcceptableOrUnknown(data['usage_count']!, _usageCountMeta), + ); + } + if (data.containsKey('created_at')) { + context.handle( + _createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), + ); + } + if (data.containsKey('last_used_at')) { + context.handle( + _lastUsedAtMeta, + lastUsedAt.isAcceptableOrUnknown( + data['last_used_at']!, + _lastUsedAtMeta, + ), + ); + } + return context; + } + + @override + Set get $primaryKey => {contentHash}; + @override + LocalSticker map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalSticker( + contentHash: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}content_hash'], + )!, + webp: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}webp'], + )!, + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + )!, + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + )!, + usageCount: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}usage_count'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + lastUsedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}last_used_at'], + )!, + ); + } + + @override + $StickersTable createAlias(String alias) { + return $StickersTable(attachedDatabase, alias); + } +} + +class LocalSticker extends DataClass implements Insertable { + final String contentHash; + final Uint8List webp; + final int width; + final int height; + final int usageCount; + final DateTime createdAt; + final DateTime lastUsedAt; + const LocalSticker({ + required this.contentHash, + required this.webp, + required this.width, + required this.height, + required this.usageCount, + required this.createdAt, + required this.lastUsedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['content_hash'] = Variable(contentHash); + map['webp'] = Variable(webp); + map['width'] = Variable(width); + map['height'] = Variable(height); + map['usage_count'] = Variable(usageCount); + map['created_at'] = Variable(createdAt); + map['last_used_at'] = Variable(lastUsedAt); + return map; + } + + StickersCompanion toCompanion(bool nullToAbsent) { + return StickersCompanion( + contentHash: Value(contentHash), + webp: Value(webp), + width: Value(width), + height: Value(height), + usageCount: Value(usageCount), + createdAt: Value(createdAt), + lastUsedAt: Value(lastUsedAt), + ); + } + + factory LocalSticker.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalSticker( + contentHash: serializer.fromJson(json['contentHash']), + webp: serializer.fromJson(json['webp']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + usageCount: serializer.fromJson(json['usageCount']), + createdAt: serializer.fromJson(json['createdAt']), + lastUsedAt: serializer.fromJson(json['lastUsedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'contentHash': serializer.toJson(contentHash), + 'webp': serializer.toJson(webp), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + 'usageCount': serializer.toJson(usageCount), + 'createdAt': serializer.toJson(createdAt), + 'lastUsedAt': serializer.toJson(lastUsedAt), + }; + } + + LocalSticker copyWith({ + String? contentHash, + Uint8List? webp, + int? width, + int? height, + int? usageCount, + DateTime? createdAt, + DateTime? lastUsedAt, + }) => LocalSticker( + contentHash: contentHash ?? this.contentHash, + webp: webp ?? this.webp, + width: width ?? this.width, + height: height ?? this.height, + usageCount: usageCount ?? this.usageCount, + createdAt: createdAt ?? this.createdAt, + lastUsedAt: lastUsedAt ?? this.lastUsedAt, + ); + LocalSticker copyWithCompanion(StickersCompanion data) { + return LocalSticker( + contentHash: data.contentHash.present + ? data.contentHash.value + : this.contentHash, + webp: data.webp.present ? data.webp.value : this.webp, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + usageCount: data.usageCount.present + ? data.usageCount.value + : this.usageCount, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + lastUsedAt: data.lastUsedAt.present + ? data.lastUsedAt.value + : this.lastUsedAt, + ); + } + + @override + String toString() { + return (StringBuffer('LocalSticker(') + ..write('contentHash: $contentHash, ') + ..write('webp: $webp, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('usageCount: $usageCount, ') + ..write('createdAt: $createdAt, ') + ..write('lastUsedAt: $lastUsedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + contentHash, + $driftBlobEquality.hash(webp), + width, + height, + usageCount, + createdAt, + lastUsedAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalSticker && + other.contentHash == this.contentHash && + $driftBlobEquality.equals(other.webp, this.webp) && + other.width == this.width && + other.height == this.height && + other.usageCount == this.usageCount && + other.createdAt == this.createdAt && + other.lastUsedAt == this.lastUsedAt); +} + +class StickersCompanion extends UpdateCompanion { + final Value contentHash; + final Value webp; + final Value width; + final Value height; + final Value usageCount; + final Value createdAt; + final Value lastUsedAt; + final Value rowid; + const StickersCompanion({ + this.contentHash = const Value.absent(), + this.webp = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.usageCount = const Value.absent(), + this.createdAt = const Value.absent(), + this.lastUsedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + StickersCompanion.insert({ + required String contentHash, + required Uint8List webp, + required int width, + required int height, + this.usageCount = const Value.absent(), + this.createdAt = const Value.absent(), + this.lastUsedAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : contentHash = Value(contentHash), + webp = Value(webp), + width = Value(width), + height = Value(height); + static Insertable custom({ + Expression? contentHash, + Expression? webp, + Expression? width, + Expression? height, + Expression? usageCount, + Expression? createdAt, + Expression? lastUsedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (contentHash != null) 'content_hash': contentHash, + if (webp != null) 'webp': webp, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (usageCount != null) 'usage_count': usageCount, + if (createdAt != null) 'created_at': createdAt, + if (lastUsedAt != null) 'last_used_at': lastUsedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + StickersCompanion copyWith({ + Value? contentHash, + Value? webp, + Value? width, + Value? height, + Value? usageCount, + Value? createdAt, + Value? lastUsedAt, + Value? rowid, + }) { + return StickersCompanion( + contentHash: contentHash ?? this.contentHash, + webp: webp ?? this.webp, + width: width ?? this.width, + height: height ?? this.height, + usageCount: usageCount ?? this.usageCount, + createdAt: createdAt ?? this.createdAt, + lastUsedAt: lastUsedAt ?? this.lastUsedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (contentHash.present) { + map['content_hash'] = Variable(contentHash.value); + } + if (webp.present) { + map['webp'] = Variable(webp.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (usageCount.present) { + map['usage_count'] = Variable(usageCount.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (lastUsedAt.present) { + map['last_used_at'] = Variable(lastUsedAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('StickersCompanion(') + ..write('contentHash: $contentHash, ') + ..write('webp: $webp, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('usageCount: $usageCount, ') + ..write('createdAt: $createdAt, ') + ..write('lastUsedAt: $lastUsedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + abstract class _$TwonlyDB extends GeneratedDatabase { _$TwonlyDB(QueryExecutor e) : super(e); $TwonlyDBManager get managers => $TwonlyDBManager(this); @@ -15010,6 +15483,7 @@ abstract class _$TwonlyDB extends GeneratedDatabase { this, ); late final $WebxdcUpdatesTable webxdcUpdates = $WebxdcUpdatesTable(this); + late final $StickersTable stickers = $StickersTable(this); late final Index idxMessagesGroupIdCreatedAt = Index( 'idx_messages_group_id_created_at', 'CREATE INDEX idx_messages_group_id_created_at ON messages (group_id, created_at)', @@ -15034,6 +15508,7 @@ abstract class _$TwonlyDB extends GeneratedDatabase { this as TwonlyDB, ); late final StoriesDao storiesDao = StoriesDao(this as TwonlyDB); + late final StickersDao stickersDao = StickersDao(this as TwonlyDB); @override Iterable> get allTables => allSchemaEntities.whereType>(); @@ -15062,6 +15537,7 @@ abstract class _$TwonlyDB extends GeneratedDatabase { webxdcApps, webxdcInstances, webxdcUpdates, + stickers, idxMessagesGroupIdCreatedAt, idxReceiptsMessageId, ]; @@ -27736,6 +28212,256 @@ typedef $$WebxdcUpdatesTableProcessedTableManager = WebxdcUpdate, PrefetchHooks Function() >; +typedef $$StickersTableCreateCompanionBuilder = + StickersCompanion Function({ + required String contentHash, + required Uint8List webp, + required int width, + required int height, + Value usageCount, + Value createdAt, + Value lastUsedAt, + Value rowid, + }); +typedef $$StickersTableUpdateCompanionBuilder = + StickersCompanion Function({ + Value contentHash, + Value webp, + Value width, + Value height, + Value usageCount, + Value createdAt, + Value lastUsedAt, + Value rowid, + }); + +class $$StickersTableFilterComposer + extends Composer<_$TwonlyDB, $StickersTable> { + $$StickersTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get contentHash => $composableBuilder( + column: $table.contentHash, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get webp => $composableBuilder( + column: $table.webp, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get width => $composableBuilder( + column: $table.width, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get height => $composableBuilder( + column: $table.height, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get usageCount => $composableBuilder( + column: $table.usageCount, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get lastUsedAt => $composableBuilder( + column: $table.lastUsedAt, + builder: (column) => ColumnFilters(column), + ); +} + +class $$StickersTableOrderingComposer + extends Composer<_$TwonlyDB, $StickersTable> { + $$StickersTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get contentHash => $composableBuilder( + column: $table.contentHash, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get webp => $composableBuilder( + column: $table.webp, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get width => $composableBuilder( + column: $table.width, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get height => $composableBuilder( + column: $table.height, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get usageCount => $composableBuilder( + column: $table.usageCount, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get lastUsedAt => $composableBuilder( + column: $table.lastUsedAt, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$StickersTableAnnotationComposer + extends Composer<_$TwonlyDB, $StickersTable> { + $$StickersTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get contentHash => $composableBuilder( + column: $table.contentHash, + builder: (column) => column, + ); + + GeneratedColumn get webp => + $composableBuilder(column: $table.webp, builder: (column) => column); + + GeneratedColumn get width => + $composableBuilder(column: $table.width, builder: (column) => column); + + GeneratedColumn get height => + $composableBuilder(column: $table.height, builder: (column) => column); + + GeneratedColumn get usageCount => $composableBuilder( + column: $table.usageCount, + builder: (column) => column, + ); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + GeneratedColumn get lastUsedAt => $composableBuilder( + column: $table.lastUsedAt, + builder: (column) => column, + ); +} + +class $$StickersTableTableManager + extends + RootTableManager< + _$TwonlyDB, + $StickersTable, + LocalSticker, + $$StickersTableFilterComposer, + $$StickersTableOrderingComposer, + $$StickersTableAnnotationComposer, + $$StickersTableCreateCompanionBuilder, + $$StickersTableUpdateCompanionBuilder, + ( + LocalSticker, + BaseReferences<_$TwonlyDB, $StickersTable, LocalSticker>, + ), + LocalSticker, + PrefetchHooks Function() + > { + $$StickersTableTableManager(_$TwonlyDB db, $StickersTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$StickersTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$StickersTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$StickersTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value contentHash = const Value.absent(), + Value webp = const Value.absent(), + Value width = const Value.absent(), + Value height = const Value.absent(), + Value usageCount = const Value.absent(), + Value createdAt = const Value.absent(), + Value lastUsedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => StickersCompanion( + contentHash: contentHash, + webp: webp, + width: width, + height: height, + usageCount: usageCount, + createdAt: createdAt, + lastUsedAt: lastUsedAt, + rowid: rowid, + ), + createCompanionCallback: + ({ + required String contentHash, + required Uint8List webp, + required int width, + required int height, + Value usageCount = const Value.absent(), + Value createdAt = const Value.absent(), + Value lastUsedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => StickersCompanion.insert( + contentHash: contentHash, + webp: webp, + width: width, + height: height, + usageCount: usageCount, + createdAt: createdAt, + lastUsedAt: lastUsedAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable<$StickersTable, LocalSticker>(table), + BaseReferences<_$TwonlyDB, $StickersTable, LocalSticker>( + db, + table, + e, + ), + ), + ) + .toList(), + prefetchHooksCallback: null, + ), + ); +} + +typedef $$StickersTableProcessedTableManager = + ProcessedTableManager< + _$TwonlyDB, + $StickersTable, + LocalSticker, + $$StickersTableFilterComposer, + $$StickersTableOrderingComposer, + $$StickersTableAnnotationComposer, + $$StickersTableCreateCompanionBuilder, + $$StickersTableUpdateCompanionBuilder, + (LocalSticker, BaseReferences<_$TwonlyDB, $StickersTable, LocalSticker>), + LocalSticker, + PrefetchHooks Function() + >; class $TwonlyDBManager { final _$TwonlyDB _db; @@ -27802,4 +28528,6 @@ class $TwonlyDBManager { $$WebxdcInstancesTableTableManager(_db, _db.webxdcInstances); $$WebxdcUpdatesTableTableManager get webxdcUpdates => $$WebxdcUpdatesTableTableManager(_db, _db.webxdcUpdates); + $$StickersTableTableManager get stickers => + $$StickersTableTableManager(_db, _db.stickers); } diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index b727c44a..26c2f64e 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -4995,6 +4995,78 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'{count, plural, =0{No views yet} =1{1 view} other{{count} views}}'** String storyViews(int count); + + /// No description provided for @sticker. + /// + /// In en, this message translates to: + /// **'Sticker'** + String get sticker; + + /// No description provided for @stickers. + /// + /// In en, this message translates to: + /// **'Stickers'** + String get stickers; + + /// No description provided for @createSticker. + /// + /// In en, this message translates to: + /// **'Create sticker'** + String get createSticker; + + /// No description provided for @noStickersYet. + /// + /// In en, this message translates to: + /// **'Create your first sticker from a photo.'** + String get noStickersYet; + + /// No description provided for @stickerAdded. + /// + /// In en, this message translates to: + /// **'Sticker saved'** + String get stickerAdded; + + /// No description provided for @stickerAlreadySaved. + /// + /// In en, this message translates to: + /// **'This sticker is already saved'** + String get stickerAlreadySaved; + + /// No description provided for @stickerCreateFailed. + /// + /// In en, this message translates to: + /// **'The sticker could not be created.'** + String get stickerCreateFailed; + + /// No description provided for @stickerSaveFailed. + /// + /// In en, this message translates to: + /// **'The sticker could not be saved.'** + String get stickerSaveFailed; + + /// No description provided for @notificationSticker. + /// + /// In en, this message translates to: + /// **'sent a sticker{inGroup}.'** + String notificationSticker(Object inGroup); + + /// No description provided for @storeSticker. + /// + /// In en, this message translates to: + /// **'Save sticker'** + String get storeSticker; + + /// No description provided for @removeSticker. + /// + /// In en, this message translates to: + /// **'Remove from collection'** + String get removeSticker; + + /// No description provided for @deleteSticker. + /// + /// In en, this message translates to: + /// **'Delete sticker'** + String get deleteSticker; } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_ar.dart b/lib/src/localization/generated/app_localizations_ar.dart index 79157185..013e91b5 100644 --- a/lib/src/localization/generated/app_localizations_ar.dart +++ b/lib/src/localization/generated/app_localizations_ar.dart @@ -2944,4 +2944,42 @@ class AppLocalizationsAr extends AppLocalizations { ); return '$_temp0'; } + + @override + String get sticker => 'ملصق'; + + @override + String get stickers => 'الملصقات'; + + @override + String get createSticker => 'إنشاء ملصق'; + + @override + String get noStickersYet => 'أنشئ ملصقك الأول من صورة.'; + + @override + String get stickerAdded => 'تم حفظ الملصق'; + + @override + String get stickerAlreadySaved => 'هذا الملصق محفوظ بالفعل'; + + @override + String get stickerCreateFailed => 'تعذّر إنشاء الملصق.'; + + @override + String get stickerSaveFailed => 'تعذّر حفظ الملصق.'; + + @override + String notificationSticker(Object inGroup) { + return 'أرسل ملصقًا$inGroup.'; + } + + @override + String get storeSticker => 'حفظ الملصق'; + + @override + String get removeSticker => 'إزالة من المجموعة'; + + @override + String get deleteSticker => 'حذف الملصق'; } diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 7ea93672..58b3bc05 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -2945,4 +2945,43 @@ class AppLocalizationsDe extends AppLocalizations { ); return '$_temp0'; } + + @override + String get sticker => 'Sticker'; + + @override + String get stickers => 'Sticker'; + + @override + String get createSticker => 'Sticker erstellen'; + + @override + String get noStickersYet => 'Erstelle deinen ersten Sticker aus einem Foto.'; + + @override + String get stickerAdded => 'Sticker gespeichert'; + + @override + String get stickerAlreadySaved => 'Dieser Sticker ist bereits gespeichert'; + + @override + String get stickerCreateFailed => 'Der Sticker konnte nicht erstellt werden.'; + + @override + String get stickerSaveFailed => + 'Der Sticker konnte nicht gespeichert werden.'; + + @override + String notificationSticker(Object inGroup) { + return 'hat einen Sticker gesendet$inGroup.'; + } + + @override + String get storeSticker => 'Sticker speichern'; + + @override + String get removeSticker => 'Aus Sammlung entfernen'; + + @override + String get deleteSticker => 'Sticker löschen'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index a675552e..cc8dfae6 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -2916,4 +2916,42 @@ class AppLocalizationsEn extends AppLocalizations { ); return '$_temp0'; } + + @override + String get sticker => 'Sticker'; + + @override + String get stickers => 'Stickers'; + + @override + String get createSticker => 'Create sticker'; + + @override + String get noStickersYet => 'Create your first sticker from a photo.'; + + @override + String get stickerAdded => 'Sticker saved'; + + @override + String get stickerAlreadySaved => 'This sticker is already saved'; + + @override + String get stickerCreateFailed => 'The sticker could not be created.'; + + @override + String get stickerSaveFailed => 'The sticker could not be saved.'; + + @override + String notificationSticker(Object inGroup) { + return 'sent a sticker$inGroup.'; + } + + @override + String get storeSticker => 'Save sticker'; + + @override + String get removeSticker => 'Remove from collection'; + + @override + String get deleteSticker => 'Delete sticker'; } diff --git a/lib/src/localization/translations/ar.arb b/lib/src/localization/translations/ar.arb index e3044a26..056d2b26 100644 --- a/lib/src/localization/translations/ar.arb +++ b/lib/src/localization/translations/ar.arb @@ -836,5 +836,17 @@ "type": "int" } } - } + }, + "sticker": "ملصق", + "stickers": "الملصقات", + "createSticker": "إنشاء ملصق", + "noStickersYet": "أنشئ ملصقك الأول من صورة.", + "stickerAdded": "تم حفظ الملصق", + "stickerAlreadySaved": "هذا الملصق محفوظ بالفعل", + "stickerCreateFailed": "تعذّر إنشاء الملصق.", + "stickerSaveFailed": "تعذّر حفظ الملصق.", + "notificationSticker": "أرسل ملصقًا{inGroup}.", + "storeSticker": "حفظ الملصق", + "removeSticker": "إزالة من المجموعة", + "deleteSticker": "حذف الملصق" } diff --git a/lib/src/localization/translations/de.arb b/lib/src/localization/translations/de.arb index 6f155f15..38bdac88 100644 --- a/lib/src/localization/translations/de.arb +++ b/lib/src/localization/translations/de.arb @@ -1149,5 +1149,17 @@ "type": "int" } } - } + }, + "sticker": "Sticker", + "stickers": "Sticker", + "createSticker": "Sticker erstellen", + "noStickersYet": "Erstelle deinen ersten Sticker aus einem Foto.", + "stickerAdded": "Sticker gespeichert", + "stickerAlreadySaved": "Dieser Sticker ist bereits gespeichert", + "stickerCreateFailed": "Der Sticker konnte nicht erstellt werden.", + "stickerSaveFailed": "Der Sticker konnte nicht gespeichert werden.", + "notificationSticker": "hat einen Sticker gesendet{inGroup}.", + "storeSticker": "Sticker speichern", + "removeSticker": "Aus Sammlung entfernen", + "deleteSticker": "Sticker löschen" } diff --git a/lib/src/localization/translations/en.arb b/lib/src/localization/translations/en.arb index 037b892b..7bb04a51 100644 --- a/lib/src/localization/translations/en.arb +++ b/lib/src/localization/translations/en.arb @@ -1159,5 +1159,17 @@ "type": "int" } } - } + }, + "sticker": "Sticker", + "stickers": "Stickers", + "createSticker": "Create sticker", + "noStickersYet": "Create your first sticker from a photo.", + "stickerAdded": "Sticker saved", + "stickerAlreadySaved": "This sticker is already saved", + "stickerCreateFailed": "The sticker could not be created.", + "stickerSaveFailed": "The sticker could not be saved.", + "notificationSticker": "sent a sticker{inGroup}.", + "storeSticker": "Save sticker", + "removeSticker": "Remove from collection", + "deleteSticker": "Delete sticker" } diff --git a/lib/src/model/protobuf/client/generated/data.pb.dart b/lib/src/model/protobuf/client/generated/data.pb.dart index 8634c355..5ac21a00 100644 --- a/lib/src/model/protobuf/client/generated/data.pb.dart +++ b/lib/src/model/protobuf/client/generated/data.pb.dart @@ -111,6 +111,7 @@ class AdditionalMessageData extends $pb.GeneratedMessage { WebxdcOrigin? webxdcOrigin, WebxdcSyncChunk? webxdcSync, WebxdcSyncRequest? webxdcSyncRequest, + StickerData? sticker, }) { final result = create(); if (type != null) result.type = type; @@ -124,6 +125,7 @@ class AdditionalMessageData extends $pb.GeneratedMessage { if (webxdcOrigin != null) result.webxdcOrigin = webxdcOrigin; if (webxdcSync != null) result.webxdcSync = webxdcSync; if (webxdcSyncRequest != null) result.webxdcSyncRequest = webxdcSyncRequest; + if (sticker != null) result.sticker = sticker; return result; } @@ -156,6 +158,8 @@ class AdditionalMessageData extends $pb.GeneratedMessage { subBuilder: WebxdcSyncChunk.create) ..aOM(10, _omitFieldNames ? '' : 'webxdcSyncRequest', subBuilder: WebxdcSyncRequest.create) + ..aOM(11, _omitFieldNames ? '' : 'sticker', + subBuilder: StickerData.create) ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -271,6 +275,123 @@ class AdditionalMessageData extends $pb.GeneratedMessage { void clearWebxdcSyncRequest() => $_clearField(10); @$pb.TagNumber(10) WebxdcSyncRequest ensureWebxdcSyncRequest() => $_ensure(9); + + @$pb.TagNumber(11) + StickerData get sticker => $_getN(10); + @$pb.TagNumber(11) + set sticker(StickerData value) => $_setField(11, value); + @$pb.TagNumber(11) + $core.bool hasSticker() => $_has(10); + @$pb.TagNumber(11) + void clearSticker() => $_clearField(11); + @$pb.TagNumber(11) + StickerData ensureSticker() => $_ensure(10); +} + +/// A small, self-contained sticker. Keeping the bytes beside the text message +/// makes it available immediately and preserves end-to-end encryption without +/// introducing a second media download lifecycle. +class StickerData extends $pb.GeneratedMessage { + factory StickerData({ + $core.int? version, + $core.List<$core.int>? webp, + $core.List<$core.int>? sha256, + $core.int? width, + $core.int? height, + }) { + final result = create(); + if (version != null) result.version = version; + if (webp != null) result.webp = webp; + if (sha256 != null) result.sha256 = sha256; + if (width != null) result.width = width; + if (height != null) result.height = height; + return result; + } + + StickerData._(); + + factory StickerData.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory StickerData.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'StickerData', + createEmptyInstance: create) + ..aI(1, _omitFieldNames ? '' : 'version', fieldType: $pb.PbFieldType.OU3) + ..a<$core.List<$core.int>>( + 2, _omitFieldNames ? '' : 'webp', $pb.PbFieldType.OY) + ..a<$core.List<$core.int>>( + 3, _omitFieldNames ? '' : 'sha256', $pb.PbFieldType.OY) + ..aI(4, _omitFieldNames ? '' : 'width', fieldType: $pb.PbFieldType.OU3) + ..aI(5, _omitFieldNames ? '' : 'height', fieldType: $pb.PbFieldType.OU3) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + StickerData clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + StickerData copyWith(void Function(StickerData) updates) => + super.copyWith((message) => updates(message as StickerData)) + as StickerData; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static StickerData create() => StickerData._(); + @$core.override + StickerData createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static StickerData getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static StickerData? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get version => $_getIZ(0); + @$pb.TagNumber(1) + set version($core.int value) => $_setUnsignedInt32(0, value); + @$pb.TagNumber(1) + $core.bool hasVersion() => $_has(0); + @$pb.TagNumber(1) + void clearVersion() => $_clearField(1); + + @$pb.TagNumber(2) + $core.List<$core.int> get webp => $_getN(1); + @$pb.TagNumber(2) + set webp($core.List<$core.int> value) => $_setBytes(1, value); + @$pb.TagNumber(2) + $core.bool hasWebp() => $_has(1); + @$pb.TagNumber(2) + void clearWebp() => $_clearField(2); + + @$pb.TagNumber(3) + $core.List<$core.int> get sha256 => $_getN(2); + @$pb.TagNumber(3) + set sha256($core.List<$core.int> value) => $_setBytes(2, value); + @$pb.TagNumber(3) + $core.bool hasSha256() => $_has(2); + @$pb.TagNumber(3) + void clearSha256() => $_clearField(3); + + @$pb.TagNumber(4) + $core.int get width => $_getIZ(3); + @$pb.TagNumber(4) + set width($core.int value) => $_setUnsignedInt32(3, value); + @$pb.TagNumber(4) + $core.bool hasWidth() => $_has(3); + @$pb.TagNumber(4) + void clearWidth() => $_clearField(4); + + @$pb.TagNumber(5) + $core.int get height => $_getIZ(4); + @$pb.TagNumber(5) + set height($core.int value) => $_setUnsignedInt32(4, value); + @$pb.TagNumber(5) + $core.bool hasHeight() => $_has(4); + @$pb.TagNumber(5) + void clearHeight() => $_clearField(5); } /// A peer-to-peer transfer of existing one-time app state. The encoded payload diff --git a/lib/src/model/protobuf/client/generated/data.pbenum.dart b/lib/src/model/protobuf/client/generated/data.pbenum.dart index 0bf236a8..3d7db1f4 100644 --- a/lib/src/model/protobuf/client/generated/data.pbenum.dart +++ b/lib/src/model/protobuf/client/generated/data.pbenum.dart @@ -35,6 +35,8 @@ class AdditionalMessageData_Type extends $pb.ProtobufEnum { static const AdditionalMessageData_Type WEBXDC_SYNC_REQUEST = AdditionalMessageData_Type._( 8, _omitEnumNames ? '' : 'WEBXDC_SYNC_REQUEST'); + static const AdditionalMessageData_Type STICKER = + AdditionalMessageData_Type._(9, _omitEnumNames ? '' : 'STICKER'); static const $core.List values = [ @@ -47,10 +49,11 @@ class AdditionalMessageData_Type extends $pb.ProtobufEnum { WEBXDC_SENT, WEBXDC_SYNC, WEBXDC_SYNC_REQUEST, + STICKER, ]; static final $core.List _byValue = - $pb.ProtobufEnum.$_initByValueList(values, 8); + $pb.ProtobufEnum.$_initByValueList(values, 9); static AdditionalMessageData_Type? valueOf($core.int value) => value < 0 || value >= _byValue.length ? null : _byValue[value]; diff --git a/lib/src/model/protobuf/client/generated/data.pbjson.dart b/lib/src/model/protobuf/client/generated/data.pbjson.dart index b742fe42..012f8e53 100644 --- a/lib/src/model/protobuf/client/generated/data.pbjson.dart +++ b/lib/src/model/protobuf/client/generated/data.pbjson.dart @@ -126,6 +126,16 @@ const AdditionalMessageData$json = { '10': 'webxdcSyncRequest', '17': true }, + { + '1': 'sticker', + '3': 11, + '4': 1, + '5': 11, + '6': '.StickerData', + '9': 8, + '10': 'sticker', + '17': true + }, ], '4': [AdditionalMessageData_Type$json], '8': [ @@ -137,6 +147,7 @@ const AdditionalMessageData$json = { {'1': '_webxdc_origin'}, {'1': '_webxdc_sync'}, {'1': '_webxdc_sync_request'}, + {'1': '_sticker'}, ], }; @@ -153,6 +164,7 @@ const AdditionalMessageData_Type$json = { {'1': 'WEBXDC_SENT', '2': 6}, {'1': 'WEBXDC_SYNC', '2': 7}, {'1': 'WEBXDC_SYNC_REQUEST', '2': 8}, + {'1': 'STICKER', '2': 9}, ], }; @@ -167,13 +179,32 @@ final $typed_data.Uint8List additionalMessageDataDescriptor = $convert.base64Dec 'ZUgEUgx3ZWJ4ZGNVcGRhdGWIAQESNwoNd2VieGRjX29yaWdpbhgIIAEoCzINLldlYnhkY09yaW' 'dpbkgFUgx3ZWJ4ZGNPcmlnaW6IAQESNgoLd2VieGRjX3N5bmMYCSABKAsyEC5XZWJ4ZGNTeW5j' 'Q2h1bmtIBlIKd2VieGRjU3luY4gBARJHChN3ZWJ4ZGNfc3luY19yZXF1ZXN0GAogASgLMhIuV2' - 'VieGRjU3luY1JlcXVlc3RIB1IRd2VieGRjU3luY1JlcXVlc3SIAQEirAEKBFR5cGUSCAoETElO' - 'SxAAEgwKCENPTlRBQ1RTEAESGgoWUkVTVE9SRURfRkxBTUVfQ09VTlRFUhACEhIKDkFTS19BQk' - '9VVF9VU0VSEAMSDgoKV0VCWERDX0FQUBAEEhEKDVdFQlhEQ19VUERBVEUQBRIPCgtXRUJYRENf' - 'U0VOVBAGEg8KC1dFQlhEQ19TWU5DEAcSFwoTV0VCWERDX1NZTkNfUkVRVUVTVBAIQgcKBV9saW' - '5rQhkKF19yZXN0b3JlZF9mbGFtZV9jb3VudGVyQhQKEl9hc2tfYWJvdXRfdXNlcl9pZEINCgtf' - 'd2VieGRjX2FwcEIQCg5fd2VieGRjX3VwZGF0ZUIQCg5fd2VieGRjX29yaWdpbkIOCgxfd2VieG' - 'RjX3N5bmNCFgoUX3dlYnhkY19zeW5jX3JlcXVlc3Q='); + 'VieGRjU3luY1JlcXVlc3RIB1IRd2VieGRjU3luY1JlcXVlc3SIAQESKwoHc3RpY2tlchgLIAEo' + 'CzIMLlN0aWNrZXJEYXRhSAhSB3N0aWNrZXKIAQEiuQEKBFR5cGUSCAoETElOSxAAEgwKCENPTl' + 'RBQ1RTEAESGgoWUkVTVE9SRURfRkxBTUVfQ09VTlRFUhACEhIKDkFTS19BQk9VVF9VU0VSEAMS' + 'DgoKV0VCWERDX0FQUBAEEhEKDVdFQlhEQ19VUERBVEUQBRIPCgtXRUJYRENfU0VOVBAGEg8KC1' + 'dFQlhEQ19TWU5DEAcSFwoTV0VCWERDX1NZTkNfUkVRVUVTVBAIEgsKB1NUSUNLRVIQCUIHCgVf' + 'bGlua0IZChdfcmVzdG9yZWRfZmxhbWVfY291bnRlckIUChJfYXNrX2Fib3V0X3VzZXJfaWRCDQ' + 'oLX3dlYnhkY19hcHBCEAoOX3dlYnhkY191cGRhdGVCEAoOX3dlYnhkY19vcmlnaW5CDgoMX3dl' + 'YnhkY19zeW5jQhYKFF93ZWJ4ZGNfc3luY19yZXF1ZXN0QgoKCF9zdGlja2Vy'); + +@$core.Deprecated('Use stickerDataDescriptor instead') +const StickerData$json = { + '1': 'StickerData', + '2': [ + {'1': 'version', '3': 1, '4': 1, '5': 13, '10': 'version'}, + {'1': 'webp', '3': 2, '4': 1, '5': 12, '10': 'webp'}, + {'1': 'sha256', '3': 3, '4': 1, '5': 12, '10': 'sha256'}, + {'1': 'width', '3': 4, '4': 1, '5': 13, '10': 'width'}, + {'1': 'height', '3': 5, '4': 1, '5': 13, '10': 'height'}, + ], +}; + +/// Descriptor for `StickerData`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List stickerDataDescriptor = $convert.base64Decode( + 'CgtTdGlja2VyRGF0YRIYCgd2ZXJzaW9uGAEgASgNUgd2ZXJzaW9uEhIKBHdlYnAYAiABKAxSBH' + 'dlYnASFgoGc2hhMjU2GAMgASgMUgZzaGEyNTYSFAoFd2lkdGgYBCABKA1SBXdpZHRoEhYKBmhl' + 'aWdodBgFIAEoDVIGaGVpZ2h0'); @$core.Deprecated('Use webxdcSyncChunkDescriptor instead') const WebxdcSyncChunk$json = { diff --git a/lib/src/services/notifications/native.notifications.dart b/lib/src/services/notifications/native.notifications.dart index 57e0c8f4..f82c30b7 100644 --- a/lib/src/services/notifications/native.notifications.dart +++ b/lib/src/services/notifications/native.notifications.dart @@ -44,6 +44,7 @@ class NativeNotificationService { static bool opensConversation(String? kind) => kind == 'text' || kind == 'response' || + kind == 'sticker' || kind == 'webxdc' || kind == 'story_reply' || kind == 'story_reaction'; diff --git a/lib/src/services/stickers/sticker.service.dart b/lib/src/services/stickers/sticker.service.dart new file mode 100644 index 00000000..da94c0f4 --- /dev/null +++ b/lib/src/services/stickers/sticker.service.dart @@ -0,0 +1,104 @@ +import 'dart:typed_data'; + +import 'package:convert/convert.dart'; +import 'package:crypto/crypto.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart'; + +class StickerService { + const StickerService._(); + + static const fallbackText = '🖼️'; + + static StickerData? decodeAdditional(Uint8List? bytes) { + if (bytes == null) return null; + try { + final additional = AdditionalMessageData.fromBuffer(bytes); + if (additional.type != AdditionalMessageData_Type.STICKER || + !additional.hasSticker()) { + return null; + } + final sticker = additional.sticker; + if (sticker.version != 1 || + sticker.webp.isEmpty || + sticker.webp.length > 128 * 1024 || + sticker.sha256.length != 32 || + sticker.width < 1 || + sticker.width > 300 || + sticker.height < 1 || + sticker.height > 300) { + return null; + } + final digest = sha256.convert(sticker.webp).bytes; + for (var index = 0; index < digest.length; index++) { + if (digest[index] != sticker.sha256[index]) return null; + } + return sticker; + } on Object { + return null; + } + } + + static StickerData fromLocal(LocalSticker sticker) { + return StickerData( + version: 1, + webp: sticker.webp, + sha256: hex.decode(sticker.contentHash), + width: sticker.width, + height: sticker.height, + ); + } + + static Uint8List encodeAdditional(LocalSticker sticker) { + return AdditionalMessageData( + type: AdditionalMessageData_Type.STICKER, + sticker: fromLocal(sticker), + ).writeToBuffer(); + } + + static Future createFromPath(String imagePath) async { + final output = await RustApi.createSticker(imagePath: imagePath); + await twonlyDB.stickersDao.save( + webp: output.webp, + width: output.width, + height: output.height, + expectedHash: output.contentHash, + ); + final sticker = await twonlyDB.stickersDao.getByHash(output.contentHash); + if (sticker == null) { + throw StateError('created sticker was not stored'); + } + return sticker; + } + + static Future createFromGallery() async { + final image = await ImagePicker().pickImage(source: ImageSource.gallery); + if (image == null) return null; + return createFromPath(image.path); + } + + static Future saveReceived(StickerData sticker) { + return twonlyDB.stickersDao.save( + webp: Uint8List.fromList(sticker.webp), + width: sticker.width, + height: sticker.height, + expectedHash: hex.encode(sticker.sha256), + ); + } + + static Future send({ + required String groupId, + required LocalSticker sticker, + String? quoteMessageId, + }) async { + await RustApi.insertAndSendText( + groupId: groupId, + text: fallbackText, + quoteMessageId: quoteMessageId, + additionalMessageData: encodeAdditional(sticker), + ); + await twonlyDB.stickersDao.recordUse(sticker.contentHash); + } +} diff --git a/lib/src/visual/components/emoji_picker.bottom.dart b/lib/src/visual/components/emoji_picker.bottom.dart index 1f12ba35..ba873552 100755 --- a/lib/src/visual/components/emoji_picker.bottom.dart +++ b/lib/src/visual/components/emoji_picker.bottom.dart @@ -1,12 +1,23 @@ import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:twonly/locator.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/emoji_picker/emoji_picker.dart'; +import 'package:twonly/src/visual/components/sticker_picker.dart'; import 'package:twonly/src/visual/views/camera/share_image_editor_components/layer_data.dart'; -class EmojiPickerBottom extends StatelessWidget { - const EmojiPickerBottom({super.key}); +class EmojiPickerBottom extends StatefulWidget { + const EmojiPickerBottom({this.allowStickers = false, super.key}); + + final bool allowStickers; + + @override + State createState() => _EmojiPickerBottomState(); +} + +class _EmojiPickerBottomState extends State { + bool _showStickers = false; @override Widget build(BuildContext context) { @@ -41,6 +52,32 @@ class EmojiPickerBottom extends StatelessWidget { ), Expanded( child: EmojiPicker( + alternateView: _showStickers + ? StickerPicker( + selectCreatedSticker: true, + onEmojiPressed: () { + setState(() => _showStickers = false); + }, + onStickerSelected: (sticker) { + unawaitedRustCall( + twonlyDB.stickersDao.recordUse( + sticker.contentHash, + ), + 'recordStickerUse', + ); + Navigator.pop( + context, + StickerLayerData( + key: GlobalKey(), + webp: sticker.webp, + contentHash: sticker.contentHash, + width: sticker.width, + height: sticker.height, + ), + ); + }, + ) + : null, onEmojiSelected: (category, emoji) { Navigator.pop( context, @@ -55,7 +92,9 @@ class EmojiPickerBottom extends StatelessWidget { height: 400, locale: Localizations.localeOf(context), checkPlatformCompatibility: false, - resizeConfig: const ResizeConfig(showDragHandle: false), + resizeConfig: const ResizeConfig( + showDragHandle: false, + ), emojiTextStyle: TextStyle( fontSize: 24 * (Platform.isIOS ? 1.2 : 1), fontFamilyFallback: Platform.isAndroid @@ -81,6 +120,11 @@ class EmojiPickerBottom extends StatelessWidget { backgroundColor: context.color.surfaceContainer, buttonColor: context.color.surfaceContainer, buttonIconColor: context.color.secondary, + stickerButtonLabel: context.lang.sticker, + showStickerButton: widget.allowStickers, + onStickerButtonPressed: widget.allowStickers + ? () => setState(() => _showStickers = true) + : null, ), ), ), diff --git a/lib/src/visual/components/emoji_picker/bottom_action_bar.dart b/lib/src/visual/components/emoji_picker/bottom_action_bar.dart index a37581ed..341b96cd 100644 --- a/lib/src/visual/components/emoji_picker/bottom_action_bar.dart +++ b/lib/src/visual/components/emoji_picker/bottom_action_bar.dart @@ -4,8 +4,8 @@ * Copyright (c) 2024 Stefan Humm */ -import 'package:twonly/src/visual/components/emoji_picker/emoji_picker.dart'; import 'package:flutter/material.dart'; +import 'package:twonly/src/visual/components/emoji_picker/emoji_picker.dart'; /// Template class for custom implementation abstract class BottomActionBar extends StatefulWidget { @@ -36,6 +36,7 @@ typedef BottomActionBarBuilder = ); /// Configuration for the bottom action bar. +@immutable class BottomActionBarConfig { /// Constructor. const BottomActionBarConfig({ @@ -69,7 +70,7 @@ class BottomActionBarConfig { final BottomActionBarBuilder? customBottomActionBar; @override - bool operator ==(other) { + bool operator ==(Object other) { return other is BottomActionBarConfig && other.enabled == enabled && other.showBackspaceButton == showBackspaceButton && @@ -119,72 +120,17 @@ class DefaultBottomActionBar extends BottomActionBar { class _DefaultBottomActionBarState extends State { @override Widget build(BuildContext context) { - return Container( - color: widget.config.bottomActionBarConfig.backgroundColor, - child: Row( - children: [ - SizedBox(width: 48, child: _buildSearchViewButton()), - Expanded( - child: Center( - child: FittedBox( - fit: BoxFit.scaleDown, - child: _buildPickerTabs(), - ), - ), - ), - SizedBox(width: 48, child: _buildBackspaceButton()), - ], - ), - ); - } - - Widget _buildPickerTabs() { final actionBarConfig = widget.config.bottomActionBarConfig; - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - _buildPickerTab( - label: actionBarConfig.emojiButtonLabel, - selected: true, - ), - if (actionBarConfig.showStickerButton) ...[ - const SizedBox(width: 4), - _buildPickerTab( - label: actionBarConfig.stickerButtonLabel, - onTap: actionBarConfig.onStickerButtonPressed, - ), - ], - ], - ); - } - - Widget _buildPickerTab({ - required String label, - bool selected = false, - VoidCallback? onTap, - }) { - final actionBarConfig = widget.config.bottomActionBarConfig; - final selectedColor = - actionBarConfig.selectedTabColor ?? - actionBarConfig.buttonIconColor.withValues(alpha: 0.14); - return Material( - color: selected ? selectedColor : Colors.transparent, - borderRadius: BorderRadius.circular(16), - clipBehavior: Clip.antiAlias, - child: InkWell( - onTap: onTap, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7), - child: Text( - label, - style: TextStyle( - color: actionBarConfig.buttonIconColor, - fontSize: 13, - fontWeight: selected ? FontWeight.w600 : FontWeight.w400, - ), - ), - ), - ), + return PickerBottomBar( + backgroundColor: actionBarConfig.backgroundColor, + buttonIconColor: actionBarConfig.buttonIconColor, + selectedTabColor: actionBarConfig.selectedTabColor, + emojiButtonLabel: actionBarConfig.emojiButtonLabel, + stickerButtonLabel: actionBarConfig.stickerButtonLabel, + showStickerButton: actionBarConfig.showStickerButton, + onStickerButtonPressed: actionBarConfig.onStickerButtonPressed, + leading: _buildSearchViewButton(), + trailing: _buildBackspaceButton(), ); } @@ -214,3 +160,121 @@ class _DefaultBottomActionBarState extends State { ); } } + +/// Shared shell for emoji and sticker mode. Reserving the same 48 logical +/// pixels on both sides keeps the tabs perfectly stationary while switching. +class PickerBottomBar extends StatelessWidget { + const PickerBottomBar({ + required this.backgroundColor, + required this.buttonIconColor, + required this.emojiButtonLabel, + required this.stickerButtonLabel, + required this.showStickerButton, + this.selectedTabColor, + this.stickerSelected = false, + this.onEmojiButtonPressed, + this.onStickerButtonPressed, + this.leading, + this.trailing, + super.key, + }); + + final Color? backgroundColor; + final Color buttonIconColor; + final Color? selectedTabColor; + final String emojiButtonLabel; + final String stickerButtonLabel; + final bool showStickerButton; + final bool stickerSelected; + final VoidCallback? onEmojiButtonPressed; + final VoidCallback? onStickerButtonPressed; + final Widget? leading; + final Widget? trailing; + + @override + Widget build(BuildContext context) { + return ColoredBox( + color: backgroundColor ?? Colors.transparent, + child: SizedBox( + height: 48, + child: Row( + children: [ + SizedBox(width: 48, child: leading), + Expanded( + child: Center( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + _PickerTab( + label: emojiButtonLabel, + selected: !stickerSelected, + foreground: buttonIconColor, + selectedColor: selectedTabColor, + onTap: onEmojiButtonPressed, + ), + if (showStickerButton) ...[ + const SizedBox(width: 4), + _PickerTab( + label: stickerButtonLabel, + selected: stickerSelected, + foreground: buttonIconColor, + selectedColor: selectedTabColor, + onTap: onStickerButtonPressed, + ), + ], + ], + ), + ), + ), + ), + SizedBox(width: 48, child: trailing), + ], + ), + ), + ); + } +} + +class _PickerTab extends StatelessWidget { + const _PickerTab({ + required this.label, + required this.selected, + required this.foreground, + this.selectedColor, + this.onTap, + }); + + final String label; + final bool selected; + final Color foreground; + final Color? selectedColor; + final VoidCallback? onTap; + + @override + Widget build(BuildContext context) { + return Material( + color: selected + ? selectedColor ?? foreground.withValues(alpha: 0.14) + : Colors.transparent, + borderRadius: BorderRadius.circular(16), + clipBehavior: Clip.antiAlias, + child: InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7), + child: Text( + label, + textAlign: TextAlign.center, + style: TextStyle( + color: foreground, + fontSize: 13, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ); + } +} diff --git a/lib/src/visual/components/emoji_picker/emoji_picker_widget.dart b/lib/src/visual/components/emoji_picker/emoji_picker_widget.dart index 059fd94a..3013eeb6 100644 --- a/lib/src/visual/components/emoji_picker/emoji_picker_widget.dart +++ b/lib/src/visual/components/emoji_picker/emoji_picker_widget.dart @@ -134,11 +134,15 @@ class EmojiPicker extends StatefulWidget { this.onCategoryChanged, this.config = const Config(), this.customWidget, + this.alternateView, }); /// Custom widget final EmojiViewBuilder? customWidget; + /// Alternate picker content, sharing the same resize handle and height. + final Widget? alternateView; + /// If you provide the [TextEditingController] that is linked to a /// [TextField] this widget handles inserting and deleting for you /// automatically. @@ -260,6 +264,9 @@ class EmojiPickerState extends State { @override Widget build(BuildContext context) { + if (widget.alternateView != null) { + return _buildPickerSurface(widget.alternateView!); + } if (!_loaded) { return widget.config.emojiViewConfig.loadingIndicator; } diff --git a/lib/src/visual/components/sticker_picker.dart b/lib/src/visual/components/sticker_picker.dart new file mode 100644 index 00000000..e3d68e23 --- /dev/null +++ b/lib/src/visual/components/sticker_picker.dart @@ -0,0 +1,136 @@ +import 'package:flutter/material.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/stickers/sticker.service.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/components/emoji_picker/bottom_action_bar.dart'; +import 'package:twonly/src/visual/components/sticker_preview.dialog.dart'; +import 'package:twonly/src/visual/loader/three_rotating_dots.loader.dart'; + +class StickerPicker extends StatefulWidget { + const StickerPicker({ + required this.onEmojiPressed, + required this.onStickerSelected, + this.selectCreatedSticker = false, + super.key, + }); + + final VoidCallback onEmojiPressed; + final ValueChanged onStickerSelected; + final bool selectCreatedSticker; + + @override + State createState() => _StickerPickerState(); +} + +class _StickerPickerState extends State { + late final Stream> _stickers = twonlyDB.stickersDao + .watchAll(); + bool _creating = false; + + Future _createSticker() async { + if (_creating) return; + setState(() => _creating = true); + try { + final sticker = await StickerService.createFromGallery(); + if (!mounted || sticker == null) return; + if (widget.selectCreatedSticker) widget.onStickerSelected(sticker); + } on Object { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.lang.stickerCreateFailed)), + ); + } finally { + if (mounted) setState(() => _creating = false); + } + } + + @override + Widget build(BuildContext context) { + final color = context.color; + return ColoredBox( + color: color.surfaceContainer, + child: Column( + children: [ + PickerBottomBar( + backgroundColor: color.surfaceContainer, + buttonIconColor: color.secondary, + emojiButtonLabel: 'Emoji', + stickerButtonLabel: context.lang.sticker, + showStickerButton: true, + stickerSelected: true, + onEmojiButtonPressed: widget.onEmojiPressed, + ), + Expanded( + child: StreamBuilder>( + stream: _stickers, + builder: (context, snapshot) { + if (!snapshot.hasData && !snapshot.hasError) { + return const Center(child: ThreeRotatingDots(size: 26)); + } + final stickers = snapshot.data ?? const []; + return GridView.builder( + padding: const EdgeInsets.all(12), + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 104, + mainAxisSpacing: 8, + crossAxisSpacing: 8, + ), + itemCount: stickers.length + 1, + itemBuilder: (context, index) { + if (index == 0) { + return Semantics( + button: true, + label: context.lang.createSticker, + child: Material( + color: color.surfaceContainerHigh, + borderRadius: BorderRadius.circular(16), + clipBehavior: Clip.antiAlias, + child: InkWell( + onTap: _creating ? null : _createSticker, + child: Center( + child: _creating + ? const ThreeRotatingDots(size: 26) + : Icon( + Icons.add_photo_alternate_outlined, + color: color.primary, + size: 32, + ), + ), + ), + ), + ); + } + final sticker = stickers[index - 1]; + return Semantics( + button: true, + label: context.lang.sticker, + child: InkWell( + key: ValueKey(sticker.contentHash), + borderRadius: BorderRadius.circular(12), + onTap: () => widget.onStickerSelected(sticker), + onLongPress: () => showStickerPreview( + context, + StickerService.fromLocal(sticker), + collectionOnly: true, + ), + child: Padding( + padding: const EdgeInsets.all(5), + child: Image.memory( + sticker.webp, + fit: BoxFit.contain, + gaplessPlayback: true, + ), + ), + ), + ); + }, + ); + }, + ), + ), + ], + ), + ); + } +} diff --git a/lib/src/visual/components/sticker_preview.dialog.dart b/lib/src/visual/components/sticker_preview.dialog.dart new file mode 100644 index 00000000..64d1251f --- /dev/null +++ b/lib/src/visual/components/sticker_preview.dialog.dart @@ -0,0 +1,137 @@ +import 'dart:typed_data'; + +import 'package:convert/convert.dart'; +import 'package:flutter/material.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart'; +import 'package:twonly/src/services/stickers/sticker.service.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/elements/my_button.element.dart'; +import 'package:twonly/src/visual/loader/three_rotating_dots.loader.dart'; + +Future showStickerPreview( + BuildContext context, + StickerData sticker, { + bool collectionOnly = false, +}) { + return showDialog( + context: context, + builder: (context) => _StickerPreviewDialog( + sticker: sticker, + collectionOnly: collectionOnly, + ), + ); +} + +class _StickerPreviewDialog extends StatefulWidget { + const _StickerPreviewDialog({ + required this.sticker, + required this.collectionOnly, + }); + + final StickerData sticker; + final bool collectionOnly; + + @override + State<_StickerPreviewDialog> createState() => _StickerPreviewDialogState(); +} + +class _StickerPreviewDialogState extends State<_StickerPreviewDialog> { + late final Uint8List _webp = Uint8List.fromList(widget.sticker.webp); + late final String _contentHash = hex.encode(widget.sticker.sha256); + bool _stored = false; + bool _busy = true; + String? _error; + + @override + void initState() { + super.initState(); + _loadStoredState(); + } + + Future _loadStoredState() async { + try { + final local = await twonlyDB.stickersDao.getByHash(_contentHash); + if (!mounted) return; + setState(() { + _stored = local != null; + _busy = false; + }); + } on Object { + if (!mounted) return; + setState(() { + _error = context.lang.stickerSaveFailed; + _busy = false; + }); + } + } + + Future _performAction() async { + if (_busy) return; + setState(() { + _busy = true; + _error = null; + }); + try { + if (_stored || widget.collectionOnly) { + await twonlyDB.stickersDao.remove(_contentHash); + } else { + await StickerService.saveReceived(widget.sticker); + } + if (!mounted) return; + Navigator.of(context).pop(); + } on Object { + if (!mounted) return; + setState(() { + _busy = false; + _error = context.lang.stickerSaveFailed; + }); + } + } + + @override + Widget build(BuildContext context) { + return Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 320), + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 240, + height: 240, + child: Image.memory(_webp, fit: BoxFit.contain), + ), + const SizedBox(height: 24), + if (_error != null) ...[ + Text(_error!, style: TextStyle(color: context.color.error)), + const SizedBox(height: 12), + ], + MyButton( + variant: _stored || widget.collectionOnly + ? MyButtonVariant.error + : MyButtonVariant.primary, + onPressed: _busy ? null : _performAction, + child: _busy + ? const ThreeRotatingDots(size: 24) + : Text( + widget.collectionOnly + ? context.lang.deleteSticker + : _stored + ? context.lang.removeSticker + : context.lang.storeSticker, + ), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(context.lang.cancel), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/visual/views/camera/share_image_editor_components/editor_layer_stack.dart b/lib/src/visual/views/camera/share_image_editor_components/editor_layer_stack.dart index ace3a603..f8851217 100644 --- a/lib/src/visual/views/camera/share_image_editor_components/editor_layer_stack.dart +++ b/lib/src/visual/views/camera/share_image_editor_components/editor_layer_stack.dart @@ -8,7 +8,7 @@ import 'package:twonly/src/visual/views/camera/share_image_editor_components/lay /// /// The stack always starts with the [BackgroundLayerData] (the photo itself) /// followed by the [FilterLayerData]; everything the user adds (text, drawings, -/// emojis, link previews) is stacked on top of those two. +/// emojis, stickers, link previews) is stacked on top of those two. /// /// Layers are not removed immediately when the user deletes them. They are /// flagged via [Layer.isDeleted] by the layer widgets and only collected later, diff --git a/lib/src/visual/views/camera/share_image_editor_components/editor_side_toolbar.dart b/lib/src/visual/views/camera/share_image_editor_components/editor_side_toolbar.dart index 8088cdf6..af0219b7 100644 --- a/lib/src/visual/views/camera/share_image_editor_components/editor_side_toolbar.dart +++ b/lib/src/visual/views/camera/share_image_editor_components/editor_side_toolbar.dart @@ -108,7 +108,8 @@ class EditorSideToolbar extends StatelessWidget { final layer = await showModalBottomSheet( context: context, backgroundColor: context.color.surface, - builder: (context) => const EmojiPickerBottom(), + builder: (context) => + const EmojiPickerBottom(allowStickers: true), ); if (layer == null) return; layerStack.add(layer); diff --git a/lib/src/visual/views/camera/share_image_editor_components/layer_data.dart b/lib/src/visual/views/camera/share_image_editor_components/layer_data.dart index 96153d07..49c75135 100755 --- a/lib/src/visual/views/camera/share_image_editor_components/layer_data.dart +++ b/lib/src/visual/views/camera/share_image_editor_components/layer_data.dart @@ -1,3 +1,5 @@ +import 'dart:typed_data'; + import 'package:flutter/material.dart'; import 'package:hand_signature/signature.dart'; import 'package:twonly/src/visual/views/camera/share_image_editor_components/image_item.dart'; @@ -69,6 +71,28 @@ class EmojiLayerData extends Layer { double size; } +class StickerLayerData extends Layer { + StickerLayerData({ + required super.key, + required this.webp, + required this.contentHash, + required this.width, + required this.height, + this.size = 160, + super.offset, + super.opacity, + super.rotation, + super.scale, + super.isEditing, + }); + + final Uint8List webp; + final String contentHash; + final int width; + final int height; + double size; +} + class TextLayerData extends Layer { TextLayerData({ required super.key, diff --git a/lib/src/visual/views/camera/share_image_editor_components/layers/sticker.layer.dart b/lib/src/visual/views/camera/share_image_editor_components/layers/sticker.layer.dart new file mode 100644 index 00000000..8e2ca525 --- /dev/null +++ b/lib/src/visual/views/camera/share_image_editor_components/layers/sticker.layer.dart @@ -0,0 +1,162 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/views/camera/share_image_editor_components/action_button.dart'; +import 'package:twonly/src/visual/views/camera/share_image_editor_components/layer_data.dart'; + +class StickerLayer extends StatefulWidget { + const StickerLayer({required this.layerData, this.onUpdate, super.key}); + + final StickerLayerData layerData; + final VoidCallback? onUpdate; + + @override + State createState() => _StickerLayerState(); +} + +class _StickerLayerState extends State { + double initialRotation = 0; + Offset initialOffset = Offset.zero; + Offset initialFocalPoint = Offset.zero; + double initialSize = 1; + bool deleteLayer = false; + bool twoPointersWereDown = false; + final GlobalKey outlineKey = GlobalKey(); + final GlobalKey stickerKey = GlobalKey(); + int pointers = 0; + bool display = false; + + @override + void initState() { + super.initState(); + if (widget.layerData.offset.dy == 0) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + setState(() { + widget.layerData.offset = Offset( + MediaQuery.sizeOf(context).width / 2 - 110, + MediaQuery.sizeOf(context).height / 2 - 210, + ); + display = true; + }); + }); + } else { + display = true; + } + } + + @override + Widget build(BuildContext context) { + if (!display || widget.layerData.isDeleted) return const SizedBox.shrink(); + return Stack( + key: outlineKey, + children: [ + Positioned( + left: widget.layerData.offset.dx, + top: widget.layerData.offset.dy, + child: Listener( + onPointerDown: (_) => setState(() => pointers++), + onPointerUp: (_) { + setState(() { + pointers = pointers > 0 ? pointers - 1 : 0; + if (pointers == 0) twoPointersWereDown = false; + if (deleteLayer) { + widget.layerData.isDeleted = true; + widget.onUpdate?.call(); + } + }); + }, + child: GestureDetector( + onScaleStart: (details) { + initialSize = widget.layerData.size; + initialRotation = widget.layerData.rotation; + initialOffset = widget.layerData.offset; + initialFocalPoint = details.focalPoint; + }, + onScaleUpdate: (details) async { + if (twoPointersWereDown && details.pointerCount != 2) return; + final outline = + outlineKey.currentContext?.findRenderObject() as RenderBox?; + final sticker = + stickerKey.currentContext?.findRenderObject() as RenderBox?; + if (outline == null || sticker == null) return; + final center = + widget.layerData.offset + + Offset(sticker.size.width / 2, sticker.size.height / 2); + final overDelete = + center.dy > outline.size.height - 80 && + center.dx > MediaQuery.sizeOf(context).width / 2 - 30 && + center.dx < MediaQuery.sizeOf(context).width / 2 + 20; + if (overDelete && !deleteLayer) { + await HapticFeedback.heavyImpact(); + } + if (!mounted) return; + setState(() { + deleteLayer = overDelete; + twoPointersWereDown = details.pointerCount >= 2; + widget.layerData.size = (initialSize * details.scale).clamp( + 42.0, + 420.0, + ); + widget.layerData.rotation = + initialRotation + details.rotation; + widget.layerData.offset = + initialOffset + (details.focalPoint - initialFocalPoint); + }); + }, + child: Transform.rotate( + angle: widget.layerData.rotation, + key: stickerKey, + child: Padding( + padding: const EdgeInsets.all(36), + child: _StickerImage(layerData: widget.layerData), + ), + ), + ), + ), + ), + if (pointers > 0) + Positioned( + left: 0, + right: 0, + bottom: 20, + child: Center( + child: ActionButton( + FontAwesomeIcons.trashCan, + tooltipText: '', + color: deleteLayer + ? context.appColor(AppColor.recording) + : context.appColor(AppColor.mediaForeground), + ), + ), + ), + ], + ); + } +} + +class _StickerImage extends StatelessWidget { + const _StickerImage({required this.layerData}); + + final StickerLayerData layerData; + + @override + Widget build(BuildContext context) { + final aspect = layerData.width / layerData.height; + final width = aspect >= 1 ? layerData.size : layerData.size * aspect; + final height = aspect >= 1 ? layerData.size / aspect : layerData.size; + return SizedBox( + width: width, + height: height, + child: Opacity( + opacity: layerData.opacity, + child: Image.memory( + layerData.webp, + fit: BoxFit.contain, + gaplessPlayback: true, + ), + ), + ); + } +} diff --git a/lib/src/visual/views/camera/share_image_editor_components/layers_viewer.dart b/lib/src/visual/views/camera/share_image_editor_components/layers_viewer.dart index 581535a3..786332d7 100644 --- a/lib/src/visual/views/camera/share_image_editor_components/layers_viewer.dart +++ b/lib/src/visual/views/camera/share_image_editor_components/layers_viewer.dart @@ -5,6 +5,7 @@ import 'package:twonly/src/visual/views/camera/share_image_editor_components/lay import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/emoji.layer.dart'; import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/filter.layer.dart'; import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/link_preview.layer.dart'; +import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/sticker.layer.dart'; import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/text.layer.dart'; /// View stacked layers (unbounded height, width) @@ -43,6 +44,7 @@ class LayersViewer extends StatelessWidget { .where( (layerItem) => layerItem is EmojiLayerData || + layerItem is StickerLayerData || layerItem is DrawLayerData || layerItem is LinkPreviewLayerData || layerItem is TextLayerData, @@ -54,6 +56,12 @@ class LayersViewer extends StatelessWidget { layerData: layerItem, onUpdate: onUpdate, ); + } else if (layerItem is StickerLayerData) { + return StickerLayer( + key: layerItem.key, + layerData: layerItem, + onUpdate: onUpdate, + ); } else if (layerItem is DrawLayerData) { return DrawLayer( key: layerItem.key, diff --git a/lib/src/visual/views/chats/chat_messages_components/chat_list_entry.dart b/lib/src/visual/views/chats/chat_messages_components/chat_list_entry.dart index a9871f9e..4c19564a 100644 --- a/lib/src/visual/views/chats/chat_messages_components/chat_list_entry.dart +++ b/lib/src/visual/views/chats/chat_messages_components/chat_list_entry.dart @@ -10,6 +10,7 @@ import 'package:twonly/src/database/tables/messages.table.dart' import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/memory_item.model.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; +import 'package:twonly/src/services/stickers/sticker.service.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; @@ -19,6 +20,7 @@ import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/c import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/chat_contacts.entry.dart'; import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/chat_flame_restored.entry.dart'; import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/chat_media_entry.dart'; +import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/chat_sticker.entry.dart'; import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/chat_text_entry.dart'; import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/chat_unknown.entry.dart'; import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/chat_webxdc.entry.dart'; @@ -146,9 +148,12 @@ class _ChatListEntryState extends State { BubbleInfo info, ) { if (widget.message.type == MessageType.text.name) { - // Only a message a webxdc app produced -- text handed to a chat, or an - // announcement an app made in one -- carries data alongside its text, so - // this is enough to route it without decoding anything here. + final sticker = StickerService.decodeAdditional( + widget.message.additionalMessageData, + ); + if (sticker != null) { + return ChatStickerEntry(message: widget.message, sticker: sticker); + } if (widget.message.additionalMessageData != null) { return ChatWebxdcMessageEntry( message: widget.message, diff --git a/lib/src/visual/views/chats/chat_messages_components/entries/chat_sticker.entry.dart b/lib/src/visual/views/chats/chat_messages_components/entries/chat_sticker.entry.dart new file mode 100644 index 00000000..b66409ff --- /dev/null +++ b/lib/src/visual/views/chats/chat_messages_components/entries/chat_sticker.entry.dart @@ -0,0 +1,73 @@ +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/components/sticker_preview.dialog.dart'; +import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/friendly_message_time.comp.dart'; + +class ChatStickerEntry extends StatelessWidget { + const ChatStickerEntry({ + required this.message, + required this.sticker, + super.key, + }); + + final Message message; + final StickerData sticker; + + @override + Widget build(BuildContext context) { + const maxEdge = 190.0; + final maxDimension = sticker.width > sticker.height + ? sticker.width + : sticker.height; + final fittedScale = maxEdge / maxDimension.clamp(1, 300).toDouble(); + final width = (sticker.width * fittedScale).clamp(48.0, maxEdge); + final height = (sticker.height * fittedScale).clamp(48.0, maxEdge); + return Semantics( + button: true, + label: context.lang.sticker, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => showStickerPreview(context, sticker), + child: Stack( + alignment: Alignment.bottomRight, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 14), + child: SizedBox( + width: width, + height: height, + child: Image.memory( + Uint8List.fromList(sticker.webp), + fit: BoxFit.contain, + gaplessPlayback: true, + errorBuilder: (context, error, stackTrace) => Icon( + Icons.broken_image_outlined, + color: context.color.onSurfaceVariant, + size: 44, + ), + ), + ), + ), + DecoratedBox( + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.52), + borderRadius: BorderRadius.circular(10), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2), + child: FriendlyMessageTime( + message: message, + color: Colors.white, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/src/visual/views/chats/chat_messages_components/entries/friendly_message_time.comp.dart b/lib/src/visual/views/chats/chat_messages_components/entries/friendly_message_time.comp.dart index 2d8dbe62..7c687281 100644 --- a/lib/src/visual/views/chats/chat_messages_components/entries/friendly_message_time.comp.dart +++ b/lib/src/visual/views/chats/chat_messages_components/entries/friendly_message_time.comp.dart @@ -22,7 +22,7 @@ class FriendlyMessageTime extends StatelessWidget { Widget build(BuildContext context) { final statusIcon = _buildStatusIcon( context, - context.appColor(AppColor.chatMessageMeta), + color ?? context.appColor(AppColor.chatMessageMeta), ); return Padding( diff --git a/lib/src/visual/views/chats/chat_messages_components/message_context_menu.dart b/lib/src/visual/views/chats/chat_messages_components/message_context_menu.dart index 818cb85a..9d8f7546 100644 --- a/lib/src/visual/views/chats/chat_messages_components/message_context_menu.dart +++ b/lib/src/visual/views/chats/chat_messages_components/message_context_menu.dart @@ -7,17 +7,20 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/locator.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/memory_item.model.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart' as pb; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; +import 'package:twonly/src/services/stickers/sticker.service.dart'; import 'package:twonly/src/services/webxdc/webxdc.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/emoji_picker.bottom.dart'; import 'package:twonly/src/visual/context_menu/context_menu.helper.dart'; import 'package:twonly/src/visual/elements/my_button.element.dart'; +import 'package:twonly/src/visual/loader/three_rotating_dots.loader.dart'; import 'package:twonly/src/visual/views/camera/share_image_editor_components/layer_data.dart'; import 'package:twonly/src/visual/views/chats/chat_messages_components/chat_list_entry.dart'; import 'package:twonly/src/visual/views/chats/message_info.view.dart'; @@ -40,6 +43,51 @@ class MessageContextMenu extends StatelessWidget { final MediaFileService? mediaFileService; final VoidCallback onResponseTriggered; + Future createStickerFromMedia(BuildContext context) async { + final media = mediaFileService; + if (media == null) return; + final candidates = [media.storedPath, media.tempPath, media.originalPath]; + final source = candidates.where((file) => file.existsSync()).firstOrNull; + if (source == null) { + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.lang.stickerCreateFailed)), + ); + return; + } + final navigator = Navigator.of(context); + final loadingRoute = DialogRoute( + context: context, + barrierDismissible: false, + builder: (context) => const PopScope( + canPop: false, + child: Dialog( + child: Padding( + padding: EdgeInsets.all(32), + child: ThreeRotatingDots(size: 32), + ), + ), + ), + ); + navigator.push(loadingRoute); + String result; + try { + await StickerService.createFromPath(source.path); + if (!context.mounted) return; + result = context.lang.stickerAdded; + } on Object { + if (!context.mounted) return; + result = context.lang.stickerCreateFailed; + } finally { + if (loadingRoute.isActive) navigator.removeRoute(loadingRoute); + } + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(result)), + ); + } + } + Future reopenMediaFile(BuildContext context) async { if (message.senderId == null) { final isAuth = await authenticateUser( @@ -107,6 +155,8 @@ class MessageContextMenu extends StatelessWidget { // `late` so the ancestor lookup only runs if the menu is actually // opened, rather than once per row on every rebuild. late final navigator = Navigator.of(context); + final isSticker = + StickerService.decodeAdditional(message.additionalMessageData) != null; return ContextMenu( items: () => [ if (!message.isDeletedFromSender) @@ -150,6 +200,14 @@ class MessageContextMenu extends StatelessWidget { onTap: () => reopenMediaFile(navigator.context), icon: FontAwesomeIcons.clockRotateLeft, ), + if (!message.isDeletedFromSender && + mediaFileService?.mediaFile.type == MediaType.image && + (mediaFileService?.mediaFile.stored ?? false)) + ContextMenuItem( + title: context.lang.createSticker, + onTap: () => createStickerFromMedia(navigator.context), + icon: FontAwesomeIcons.wandMagicSparkles, + ), if (!message.isDeletedFromSender) ContextMenuItem( title: context.lang.reply, @@ -160,7 +218,8 @@ class MessageContextMenu extends StatelessWidget { ), if (!message.isDeletedFromSender && message.senderId == null && - message.type == MessageType.text.name) + message.type == MessageType.text.name && + !isSticker) ContextMenuItem( title: context.lang.edit, onTap: () async { @@ -168,7 +227,7 @@ class MessageContextMenu extends StatelessWidget { }, icon: FontAwesomeIcons.pencil, ), - if (message.content != null) + if (message.content != null && !isSticker) ContextMenuItem( title: context.lang.copy, onTap: () async { diff --git a/lib/src/visual/views/chats/chat_messages_components/message_input.dart b/lib/src/visual/views/chats/chat_messages_components/message_input.dart index 1ff2f00f..0a87c3df 100644 --- a/lib/src/visual/views/chats/chat_messages_components/message_input.dart +++ b/lib/src/visual/views/chats/chat_messages_components/message_input.dart @@ -13,8 +13,10 @@ import 'package:twonly/locator.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; +import 'package:twonly/src/services/stickers/sticker.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/emoji_picker/emoji_picker.dart'; +import 'package:twonly/src/visual/components/sticker_picker.dart'; import 'package:twonly/src/visual/views/camera/camera_send_to.view.dart'; import 'package:twonly/src/visual/views/chats/chat_messages_components/bottom_sheets/share_additional.bottom_sheet.dart'; import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/chat_audio_entry.dart'; @@ -70,6 +72,7 @@ class _MessageInputState extends State late final RecorderController recorderController; final bool isApple = Platform.isIOS; bool _emojiShowing = false; + bool _stickerShowing = false; bool _switchingToSystemKeyboard = false; double _emojiPickerHeight = 300; bool _showSparks = false; @@ -112,6 +115,19 @@ class _MessageInputState extends State ); } + void _sendSticker(LocalSticker sticker) { + final quoteMessageId = widget.quotesMessage?.messageId; + widget.onMessageSend(); + unawaitedRustCall( + StickerService.send( + groupId: widget.group.groupId, + sticker: sticker, + quoteMessageId: quoteMessageId, + ), + 'sendSticker', + ); + } + @override void initState() { super.initState(); @@ -246,6 +262,7 @@ class _MessageInputState extends State _emojiPickerHeight = keyboardHeight; } _emojiShowing = true; + _stickerShowing = false; }); widget.onEmojiVisibilityChanged(true); widget.textFieldFocus.unfocus(); @@ -756,6 +773,14 @@ class _MessageInputState extends State Offstage( offstage: !_emojiShowing, child: EmojiPicker( + alternateView: _stickerShowing + ? StickerPicker( + onEmojiPressed: () { + setState(() => _stickerShowing = false); + }, + onStickerSelected: _sendSticker, + ) + : null, textEditingController: _textFieldController, onEmojiSelected: (category, emoji) { setState(() {}); @@ -787,6 +812,10 @@ class _MessageInputState extends State backgroundColor: context.color.surfaceContainer, buttonColor: context.color.surfaceContainer, buttonIconColor: context.color.secondary, + stickerButtonLabel: context.lang.sticker, + onStickerButtonPressed: () { + setState(() => _stickerShowing = true); + }, ), ), ), diff --git a/lib/src/visual/views/chats/chat_messages_components/response_container.dart b/lib/src/visual/views/chats/chat_messages_components/response_container.dart index 367e1141..65e3f821 100644 --- a/lib/src/visual/views/chats/chat_messages_components/response_container.dart +++ b/lib/src/visual/views/chats/chat_messages_components/response_container.dart @@ -1,3 +1,5 @@ +import 'dart:typed_data'; + import 'package:clock/clock.dart'; import 'package:flutter/material.dart'; import 'package:twonly/locator.dart'; @@ -8,6 +10,7 @@ import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; +import 'package:twonly/src/services/stickers/sticker.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/views/chats/chat_messages.view.dart'; @@ -191,7 +194,12 @@ class _ResponsePreviewState extends State { if (_message != null) { if (_message!.type == MessageType.text.name) { - if (_message!.content != null) { + final sticker = StickerService.decodeAdditional( + _message!.additionalMessageData, + ); + if (sticker != null) { + subtitle = context.lang.sticker; + } else if (_message!.content != null) { subtitle = truncateString(_message!.content!); } } @@ -248,6 +256,9 @@ class _ResponsePreviewState extends State { _mediaService!.mediaFile.type != MediaType.audio; Widget? imageWidget; + final quotedSticker = StickerService.decodeAdditional( + _message?.additionalMessageData, + ); if (hasImage) { final isVideo = _mediaService!.mediaFile.type == MediaType.video; // A story nobody saved is still in its temporary file. @@ -269,6 +280,16 @@ class _ResponsePreviewState extends State { ), ), ); + } else if (quotedSticker != null) { + imageWidget = Container( + height: 40, + width: 40, + margin: const EdgeInsets.only(left: 8), + child: Image.memory( + Uint8List.fromList(quotedSticker.webp), + fit: BoxFit.contain, + ), + ); } return Container( diff --git a/lib/src/visual/views/settings/help/credits.view.dart b/lib/src/visual/views/settings/help/credits.view.dart index 94ad3d52..759f3ed8 100644 --- a/lib/src/visual/views/settings/help/credits.view.dart +++ b/lib/src/visual/views/settings/help/credits.view.dart @@ -85,6 +85,11 @@ class _CreditsViewState extends State { title: 'Avatar Icons', url: 'https://github.com/RoadTripMoustache/avatar_maker', ), + const UrlListTitle( + title: 'Sticker background removal: U²-Net-P', + subtitle: 'Apache-2.0 · Xuebin Qin and contributors', + url: 'https://github.com/xuebinqin/U-2-Net', + ), const Divider(), const ListTile( title: Center( diff --git a/pubspec.yaml b/pubspec.yaml index 00b53d0e..02cf8a0e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -211,6 +211,7 @@ flutter: assets: # Add assets from the images directory to the application. - assets/images/ + - assets/licenses/ - assets/icons/ - assets/icons/verification_badge_numeric/ - assets/animated_icons/ diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 8ce53916..6cf84b1f 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -33,7 +33,7 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "cipher 0.4.4", "cpufeatures 0.2.17", ] @@ -150,12 +150,68 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" +[[package]] +name = "anymap3" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5dfbc6d8d2675589ccbe4d0fd61df2419075625f8c1a62325e718e2b0049f9" + [[package]] name = "apple-native-keyring-store" version = "1.0.1" @@ -248,7 +304,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" dependencies = [ "addr2line", - "cfg-if", + "cfg-if 1.0.4", "libc", "miniz_oxide", "object", @@ -282,6 +338,25 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "bit-set" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56d87354e4229f54a44f7bf2435906a4656dba36026ab6eaca629a2c436a691c" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5727b15fa97d4f4fee0a3b7c3d550ed0269f54329207b86388de918604e31269" +dependencies = [ + "borsh", + "serde", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -359,6 +434,45 @@ dependencies = [ "syn 3.0.3", ] +[[package]] +name = "boow" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4505c91a2ef58b8ff2b8c579cd580e4f94949828b4f9a888666cecf08d4124" +dependencies = [ + "cfg-if 0.1.10", +] + +[[package]] +name = "borsh" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "553c5d846a6ba5150c65e3b1b8ec073bcf1abc20f9b7220de384a4443ea4e20a" +dependencies = [ + "borsh-derive", + "bytes", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12cdfe656708a01f89b451a7d36466e6fe6c414de0aa18fc54f864f6f9ca9f56" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "box_drawing" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea27d8d5fd867b17523bf6788b1175fa9867f34669d057e9adaf76e27bcea44b" + [[package]] name = "build-target" version = "0.4.0" @@ -428,9 +542,15 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ - "nom", + "nom 7.1.3", ] +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + [[package]] name = "cfg-if" version = "1.0.4" @@ -449,7 +569,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "cipher 0.4.4", "cpufeatures 0.2.17", ] @@ -460,7 +580,7 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "cpufeatures 0.3.0", "rand_core 0.10.1", ] @@ -526,6 +646,46 @@ dependencies = [ "libloading", ] +[[package]] +name = "clap" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "clap_lex" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" + [[package]] name = "cmov" version = "0.5.4" @@ -547,6 +707,18 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "colorous" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e18bf7a165bf7028fde98609a0f1e8f7498d762a212598e6c891f6893556ec" + [[package]] name = "combine" version = "4.6.7" @@ -563,7 +735,7 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "wasm-bindgen", ] @@ -671,7 +843,7 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", ] [[package]] @@ -714,6 +886,12 @@ version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.7" @@ -768,7 +946,7 @@ version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "cpufeatures 0.3.0", "curve25519-dalek-derive", "digest 0.11.3", @@ -839,7 +1017,7 @@ version = "5.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "hashbrown 0.14.5", "lock_api", "once_cell", @@ -889,6 +1067,17 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "derive-new" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cdc8d50f426189eef89dac62fabfa0abb27d5cc008f25bf4156a0203325becc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "derive-where" version = "1.6.1" @@ -972,12 +1161,24 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "downcast-rs" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" + [[package]] name = "dyn-clone" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "dyn-hash" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fdab65db9274e0168143841eb8f864a0a21f8b1b8d2ba6812bbe6024346e99e" + [[package]] name = "either" version = "1.17.0" @@ -1028,6 +1229,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + [[package]] name = "errno" version = "0.3.14" @@ -1044,7 +1256,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "windows-sys 0.61.2", ] @@ -1099,6 +1311,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if 1.0.4", + "libc", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -1119,6 +1341,7 @@ checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", + "zlib-rs", ] [[package]] @@ -1344,7 +1567,7 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "js-sys", "libc", "wasi", @@ -1357,7 +1580,7 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "libc", "r-efi 5.3.0", "wasip2", @@ -1369,7 +1592,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "js-sys", "libc", "r-efi 6.0.0", @@ -1419,6 +1642,18 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if 1.0.4", + "crunchy", + "num-traits", + "zerocopy", +] + [[package]] name = "hash32" version = "0.2.1" @@ -1915,6 +2150,15 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "inventory" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" +dependencies = [ + "rustversion", +] + [[package]] name = "ipnet" version = "2.12.1" @@ -1932,6 +2176,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.13.0" @@ -1972,7 +2222,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" dependencies = [ "cesu8", - "cfg-if", + "cfg-if 1.0.4", "combine", "jni-sys 0.3.1", "log", @@ -2025,7 +2275,7 @@ version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "futures-util", "wasm-bindgen", ] @@ -2175,7 +2425,7 @@ version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "windows-link", ] @@ -2209,7 +2459,7 @@ name = "libsignal-debug" version = "0.97.5" source = "git+https://github.com/signalapp/libsignal?rev=44a6dd8fc9f9d6903b32842b80b3894a75678418#44a6dd8fc9f9d6903b32842b80b3894a75678418" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", ] [[package]] @@ -2306,6 +2556,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + [[package]] name = "matchers" version = "0.2.0" @@ -2315,13 +2571,23 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "matrixmultiply" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f607c237553f086e7043417a51df26b2eb899d3caff94e6a67592ff992fedc7" +dependencies = [ + "autocfg", + "rawpointer", +] + [[package]] name = "md-5" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "digest 0.10.7", ] @@ -2331,7 +2597,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "digest 0.11.3", ] @@ -2341,6 +2607,31 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "memmap2" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" +dependencies = [ + "libc", +] + +[[package]] +name = "memo-map" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5449c8c750f1a07ea702bbd212bd999fceece9b3d1508b17023b3e174583124b" + +[[package]] +name = "minijinja" +version = "2.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86886cf6dbf4e614b19c9a1eec9775f021869d7eadde0fc73921a81b90c9b4c9" +dependencies = [ + "memo-map", + "serde", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -2401,6 +2692,35 @@ dependencies = [ "tempfile", ] +[[package]] +name = "ndarray" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520080814a7a6b4a6e9070823bb24b4531daac8c4627e08ba5de8c5ef2f2752d" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "portable-atomic", + "portable-atomic-util", + "rawpointer", +] + +[[package]] +name = "ndarray-npy" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58e8a348bca0075000d999d750420d74434fd0d3e0993b456554f885e7657a11" +dependencies = [ + "byteorder", + "ndarray", + "num-complex", + "num-traits", + "py_literal", + "zip 6.0.0", +] + [[package]] name = "ndk-context" version = "0.1.1" @@ -2417,6 +2737,24 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "nom-language" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2de2bc5b451bfedaef92c90b8939a8fff5770bdcc1fafd6239d086aab8fa6b29" +dependencies = [ + "nom 8.0.0", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -2436,6 +2774,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-integer" version = "0.1.46" @@ -2445,6 +2792,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2452,6 +2810,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -2501,6 +2860,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "opaque-debug" version = "0.3.1" @@ -2514,7 +2879,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" dependencies = [ "bitflags 2.13.1", - "cfg-if", + "cfg-if 1.0.4", "foreign-types", "libc", "openssl-macros", @@ -2593,7 +2958,7 @@ version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "libc", "redox_syscall", "smallvec", @@ -2628,6 +2993,48 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pest" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45d3aca230fad2e6f6317ca0a72724338c4960cb97168a85cdee66df4a9a21a8" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "284b60557f2c4a2e72ad3f2d34d42685a2fa4a6a61d0d2a10c0ae2a5e916c2cf" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9d1f08a115309ee99268cf85e5228e0e56aa9caf8841ec12866b6be07c3109" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pest_meta" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed93ba1a9ffcca32130a5188701c81c0c49cf00d4b7c5007d5148951d743adcb" +dependencies = [ + "pest", +] + [[package]] name = "petgraph" version = "0.8.3" @@ -2700,7 +3107,7 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "cpufeatures 0.2.17", "opaque-debug", "universal-hash 0.5.1", @@ -2724,6 +3131,15 @@ version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" +[[package]] +name = "portable-atomic-util" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" +dependencies = [ + "portable-atomic", +] + [[package]] name = "postcard" version = "1.1.3" @@ -2785,6 +3201,15 @@ dependencies = [ "syn 3.0.3", ] +[[package]] +name = "primal-check" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0d895b311e3af9902528fbb8f928688abbd95872819320517cc24ca6b2bd08" +dependencies = [ + "num-integer", +] + [[package]] name = "proc-macro-crate" version = "3.5.0" @@ -2882,6 +3307,19 @@ version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" +[[package]] +name = "py_literal" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "102df7a3d46db9d3891f178dcc826dc270a6746277a9ae6436f8d29fd490a8e1" +dependencies = [ + "num-bigint", + "num-complex", + "num-traits", + "pest", + "pest_derive", +] + [[package]] name = "quick-error" version = "2.0.1" @@ -3041,6 +3479,16 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" +[[package]] +name = "rand_distr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d431c2703ccf129de4d45253c03f49ebb22b97d6ad79ee3ecfc7e3f4862c1d8" +dependencies = [ + "num-traits", + "rand 0.10.2", +] + [[package]] name = "rand_pcg" version = "0.10.2" @@ -3050,6 +3498,12 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + [[package]] name = "rayon" version = "1.12.0" @@ -3176,7 +3630,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", - "cfg-if", + "cfg-if 1.0.4", "getrandom 0.2.17", "libc", "untrusted", @@ -3238,11 +3692,12 @@ dependencies = [ "tokio-util", "tracing", "tracing-subscriber", + "tract", "uuid", "walkdir", "webp", "zeroize", - "zip", + "zip 2.4.2", ] [[package]] @@ -3266,6 +3721,20 @@ dependencies = [ "semver", ] +[[package]] +name = "rustfft" +version = "6.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21db5f9893e91f41798c88680037dba611ca6674703c1a18601b01a72c8adb89" +dependencies = [ + "num-complex", + "num-integer", + "num-traits", + "primal-check", + "strength_reduce", + "transpose", +] + [[package]] name = "rustix" version = "1.1.4" @@ -3362,7 +3831,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f874456e72520ff1375a06c588eaf074b0f01f9e9e1aada45bd9b7954a6e42c" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "cipher 0.5.2", ] @@ -3396,7 +3865,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87af57419b594aa23fa95f09f0e06d80d84ba01c26148c43844cad6ff4485f0" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "pbkdf2", "salsa20", "sha2 0.11.0", @@ -3492,7 +3961,7 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "cpufeatures 0.2.17", "digest 0.10.7", ] @@ -3503,7 +3972,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "cpufeatures 0.3.0", "digest 0.11.3", ] @@ -3514,7 +3983,7 @@ version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "cpufeatures 0.2.17", "digest 0.10.7", ] @@ -3525,7 +3994,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "cpufeatures 0.3.0", "digest 0.11.3", ] @@ -3701,7 +4170,7 @@ checksum = "05b44e85bf579a8eeb4ceaa77a3a523baf2bf0e9bac7e40f405d537b5d2d5ccb" dependencies = [ "base64", "bytes", - "cfg-if", + "cfg-if 1.0.4", "chrono", "crc", "crossbeam-queue", @@ -3747,7 +4216,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb8d96de5fdc85a5c4ec813432b523ec637e80ba98f046555f75f7908ddac7c3" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "dotenvy", "either", "heck", @@ -3884,6 +4353,12 @@ dependencies = [ "tungstenite", ] +[[package]] +name = "strength_reduce" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" + [[package]] name = "strict-num" version = "0.1.1" @@ -3893,6 +4368,16 @@ dependencies = [ "float-cmp", ] +[[package]] +name = "string-interner" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad3df9b59e2eded8d825c7c4363ad339a20fb6bc0b9a4778560f518f59910b15" +dependencies = [ + "hashbrown 0.16.1", + "serde", +] + [[package]] name = "stringprep" version = "0.1.5" @@ -3968,6 +4453,17 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "tempfile" version = "3.27.0" @@ -4036,7 +4532,7 @@ version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", ] [[package]] @@ -4057,7 +4553,7 @@ dependencies = [ "arrayref", "arrayvec", "bytemuck", - "cfg-if", + "cfg-if 1.0.4", "log", "png 0.17.16", "tiny-skia-path", @@ -4327,6 +4823,238 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "tract" +version = "0.23.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758e0c728b19841c7b137e9d81ea1bfbeba6fba3ab710b3ca8157c08b99daeea" +dependencies = [ + "anyhow", + "boow", + "erased-serde", + "half", + "serde_json", + "tract-api", + "tract-core", + "tract-extra", + "tract-libcli", + "tract-nnef", + "tract-onnx", + "tract-onnx-opl", + "tract-pulse-opl", +] + +[[package]] +name = "tract-api" +version = "0.23.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e23ba86825d721cca45c0656dcbf62286b4feec82d2443aa014b836a823c6e3" +dependencies = [ + "anyhow", + "boow", + "half", + "serde", + "serde_json", +] + +[[package]] +name = "tract-core" +version = "0.23.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a5a9e07e863fc28355436c5a48fe859784832094ba1ff410357396ae87ff1e3" +dependencies = [ + "anyhow", + "anymap3", + "bit-set", + "derive-new", + "downcast-rs", + "dyn-clone", + "erased-serde", + "inventory", + "lazy_static", + "log", + "maplit", + "ndarray", + "num-complex", + "num-integer", + "num-traits", + "pastey", + "rustfft", + "serde", + "smallvec", + "tract-data", + "tract-linalg", +] + +[[package]] +name = "tract-data" +version = "0.23.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33bafcf1d3dfa404c3271dd08304b162deb8fcfe9536290fb97e578c32d5dd5" +dependencies = [ + "anyhow", + "downcast-rs", + "dyn-clone", + "dyn-hash", + "half", + "inventory", + "itertools 0.15.0", + "lazy_static", + "libm", + "maplit", + "ndarray", + "nom 8.0.0", + "nom-language", + "num-integer", + "num-traits", + "parking_lot", + "smallvec", + "string-interner", +] + +[[package]] +name = "tract-extra" +version = "0.23.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a05c86ece0c47880c7726e08dabb72d68e9cba0cd36d44bc0aa90e2291a05db" +dependencies = [ + "tract-nnef", +] + +[[package]] +name = "tract-hir" +version = "0.23.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a52e3ec1f3beb16d552806386e123cf5632d4206154695a80aaecdb6ee2c5128" +dependencies = [ + "derive-new", + "log", + "tract-core", +] + +[[package]] +name = "tract-libcli" +version = "0.23.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "885205d0df68d3248fd6247d91474143aec09390cd713b1ea4c4835728ce4651" +dependencies = [ + "box_drawing", + "clap", + "colorous", + "lazy_static", + "log", + "ndarray-npy", + "nu-ansi-term", + "py_literal", + "rand 0.10.2", + "serde", + "serde_json", + "tract-core", + "tract-hir", +] + +[[package]] +name = "tract-linalg" +version = "0.23.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cc37ab328b95cef8eef98324122e9f956fd3ca197f0892e09ff90fe495fa222" +dependencies = [ + "byteorder", + "cc", + "derive-new", + "downcast-rs", + "dyn-clone", + "dyn-hash", + "half", + "inventory", + "lazy_static", + "libc", + "log", + "minijinja", + "num-traits", + "pastey", + "tract-data", + "walkdir", +] + +[[package]] +name = "tract-nnef" +version = "0.23.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d95e30d7e6c2f73516212367a5b49354c105b52285e8919ec439c84556f54b3" +dependencies = [ + "byteorder", + "erased-serde", + "log", + "nom 8.0.0", + "nom-language", + "serde", + "serde_json", + "simd-adler32", + "tar", + "tract-core", + "walkdir", +] + +[[package]] +name = "tract-onnx" +version = "0.23.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a316674683d04c6070146badf57a7b8b9d99d4e38f586cbe3214e553d70bc2d" +dependencies = [ + "bytes", + "derive-new", + "log", + "memmap2", + "nom 8.0.0", + "nom-language", + "num-integer", + "num-rational", + "num-traits", + "prost", + "smallvec", + "tract-extra", + "tract-hir", + "tract-nnef", + "tract-onnx-opl", +] + +[[package]] +name = "tract-onnx-opl" +version = "0.23.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "091e126c52d608214f8784f468c7d0be44459e7a369fb9a26f92aa593a78f4a5" +dependencies = [ + "getrandom 0.4.3", + "log", + "rand 0.10.2", + "rand_distr", + "rustfft", + "tract-extra", + "tract-nnef", +] + +[[package]] +name = "tract-pulse-opl" +version = "0.23.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17fb7c893409929cffc5fa72d09ff9d035c33b7ac98481c5e7fded58322e4f30" +dependencies = [ + "downcast-rs", + "lazy_static", + "tract-nnef", +] + +[[package]] +name = "transpose" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e" +dependencies = [ + "num-integer", + "strength_reduce", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -4362,12 +5090,24 @@ dependencies = [ "utf-8", ] +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + [[package]] name = "typenum" version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "unicode-bidi" version = "0.3.18" @@ -4496,6 +5236,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.24.0" @@ -4565,7 +5311,7 @@ version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "once_cell", "rustversion", "wasm-bindgen-macro", @@ -4913,6 +5659,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + [[package]] name = "xmlwriter" version = "0.1.0" @@ -5053,6 +5809,26 @@ dependencies = [ "zopfli", ] +[[package]] +name = "zip" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2a05c7c36fde6c09b08576c9f7fb4cda705990f73b58fe011abf7dfb24168b" +dependencies = [ + "arbitrary", + "crc32fast", + "flate2", + "indexmap", + "memchr", + "zopfli", +] + +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" + [[package]] name = "zmij" version = "1.0.23" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 96c76532..3aa9a559 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -78,6 +78,7 @@ bon = "3.9.3" stream-tungstenite = "0.6.1" image = { version = "0.25.10", default-features = false, features = ["png", "jpeg", "gif", "bmp", "webp"] } webp = "0.3.1" +tract = { version = "0.23.8", default-features = false, features = ["onnx"] } [target.'cfg(target_os = "ios")'.dependencies] # iOS backend: Requires the 'protected' feature for Data Protection Keychain apple-native-keyring-store = { version = "1", features = ["protected"] } diff --git a/rust/models/README.md b/rust/models/README.md new file mode 100644 index 00000000..5b4e83a5 --- /dev/null +++ b/rust/models/README.md @@ -0,0 +1,11 @@ +# Bundled sticker segmentation model + +`u2netp.onnx` is the compact U²-Net portrait/object segmentation model used to +create stickers entirely on device. It is bundled for the first release; a +later model manager can replace the bytes after verifying the same digest. + +- Source: +- Architecture/source project: +- License: Apache-2.0 +- License text distributed with the app: `assets/licenses/u2net.txt` +- SHA-256: `309c8469258dda742793dce0ebea8e6dd393174f89934733ecc8b14c76f4ddd8` diff --git a/rust/models/u2netp.onnx b/rust/models/u2netp.onnx new file mode 100644 index 00000000..4f21f9af Binary files /dev/null and b/rust/models/u2netp.onnx differ diff --git a/rust/src/api/messages/incoming/text_message.rs b/rust/src/api/messages/incoming/text_message.rs index c8be6458..8b8b1040 100644 --- a/rust/src/api/messages/incoming/text_message.rs +++ b/rust/src/api/messages/incoming/text_message.rs @@ -22,6 +22,19 @@ pub(crate) async fn handle_text_message( let timestamp = milliseconds_to_seconds(message.timestamp); + let additional_message_data = match message.additional_message_data.as_deref() { + Some(data) => match crate::services::stickers::validate_additional_if_sticker(data) { + Ok(()) => Some(data), + Err(error) => { + // Keep the fallback text so an invalid attachment cannot make + // a reliable-mailbox message retry forever. + tracing::warn!(%error, "discarding invalid sticker attachment"); + None + } + }, + None => None, + }; + NewMessage::builder() .group_id(group_id) .message_id(&message.sender_message_id) @@ -30,7 +43,7 @@ pub(crate) async fn handle_text_message( .sender_id(from_user_id) .content(&message.text) .maybe_quotes_message_id(message.quote_message_id.as_deref()) - .maybe_additional_message_data(message.additional_message_data.as_deref()) + .maybe_additional_message_data(additional_message_data) .ack_by_server(current_time().timestamp()) .build() .insert(t) diff --git a/rust/src/api/proto/client/data.proto b/rust/src/api/proto/client/data.proto index 9502e6af..d132dcda 100644 --- a/rust/src/api/proto/client/data.proto +++ b/rust/src/api/proto/client/data.proto @@ -17,6 +17,7 @@ message AdditionalMessageData { WEBXDC_SENT = 6; WEBXDC_SYNC = 7; WEBXDC_SYNC_REQUEST = 8; + STICKER = 9; } Type type = 1; @@ -29,6 +30,18 @@ message AdditionalMessageData { optional WebxdcOrigin webxdc_origin = 8; optional WebxdcSyncChunk webxdc_sync = 9; optional WebxdcSyncRequest webxdc_sync_request = 10; + optional StickerData sticker = 11; +} + +// A small, self-contained sticker. Keeping the bytes beside the text message +// makes it available immediately and preserves end-to-end encryption without +// introducing a second media download lifecycle. +message StickerData { + uint32 version = 1; + bytes webp = 2; + bytes sha256 = 3; + uint32 width = 4; + uint32 height = 5; } // A peer-to-peer transfer of existing one-time app state. The encoded payload diff --git a/rust/src/bridge/api.rs b/rust/src/bridge/api.rs index c0c393cd..efe9580d 100644 --- a/rust/src/bridge/api.rs +++ b/rust/src/bridge/api.rs @@ -14,10 +14,14 @@ use crate::services::contacts::ContactService; use crate::services::media_upload::{MediaSizeReport, MediaUploadService}; use crate::services::messages::MessageService; use crate::services::outbox_dispatch::OutboxDispatchService; +use crate::services::stickers; use crate::services::stories::StoryAudience; use crate::user_config::UserConfig; use flutter_rust_bridge::frb; use std::collections::HashMap; +use std::path::Path; + +pub use crate::services::stickers::StickerOutput; #[frb(ignore)] pub enum ServerResult { @@ -157,6 +161,17 @@ pub struct FrbMemoriesUploadUrls { pub struct RustApi {} impl RustApi { + /// Creates a transparent, message-sized sticker from a local image. Model + /// inference and image encoding are CPU-heavy, so neither runs on Tokio's + /// async worker threads. + pub async fn create_sticker(image_path: String) -> Result { + tokio::task::spawn_blocking(move || stickers::create(Path::new(&image_path))) + .await + .map_err(|error| { + TwonlyError::Generic(format!("sticker creation task failed: {error}")) + })? + } + #[frb(sync)] pub fn decode_avatar_svg(avatar_svg_compressed: Vec) -> String { avatars::decode_avatar_svg(avatar_svg_compressed) diff --git a/rust/src/database/app/migrations/0027_stickers.sql b/rust/src/database/app/migrations/0027_stickers.sql new file mode 100644 index 00000000..0e354c2b --- /dev/null +++ b/rust/src/database/app/migrations/0027_stickers.sql @@ -0,0 +1,10 @@ +CREATE TABLE stickers ( + content_hash TEXT NOT NULL PRIMARY KEY CHECK(length(content_hash) = 64), + webp BLOB NOT NULL CHECK(length(webp) BETWEEN 1 AND 131072), + width INTEGER NOT NULL CHECK(width BETWEEN 1 AND 300), + height INTEGER NOT NULL CHECK(height BETWEEN 1 AND 300), + created_at INTEGER NOT NULL DEFAULT (CAST(strftime('%s', 'now') AS INTEGER)), + last_used_at INTEGER NOT NULL DEFAULT (CAST(strftime('%s', 'now') AS INTEGER)) +); + +CREATE INDEX stickers_last_used_at ON stickers(last_used_at DESC); diff --git a/rust/src/database/app/migrations/0028_sticker_usage.sql b/rust/src/database/app/migrations/0028_sticker_usage.sql new file mode 100644 index 00000000..8ad901d2 --- /dev/null +++ b/rust/src/database/app/migrations/0028_sticker_usage.sql @@ -0,0 +1,2 @@ +ALTER TABLE stickers ADD COLUMN usage_count INTEGER NOT NULL DEFAULT 0 CHECK(usage_count >= 0); +CREATE INDEX stickers_usage ON stickers(usage_count DESC, last_used_at DESC); diff --git a/rust/src/database/app/mod.rs b/rust/src/database/app/mod.rs index 8537438a..ea39b97e 100644 --- a/rust/src/database/app/mod.rs +++ b/rust/src/database/app/mod.rs @@ -16,7 +16,7 @@ mod legacy_import; pub mod tables; pub const APP_DATABASE_FILE: &str = "app_db.sqlite"; -pub const APP_SCHEMA_VERSION: i64 = 11; +pub const APP_SCHEMA_VERSION: i64 = 13; /// User-owned application tables in the current Rust schema. Rust-only outbox /// tables are deliberately absent because they are reconstructed locally. diff --git a/rust/src/database/app/tables/message.rs b/rust/src/database/app/tables/message.rs index 0ead973b..da8216a3 100644 --- a/rust/src/database/app/tables/message.rs +++ b/rust/src/database/app/tables/message.rs @@ -116,7 +116,8 @@ impl Message { .await?; sqlx::query!( r#"UPDATE messages - SET is_deleted_from_sender = 1, content = NULL, media_id = NULL, modified_at = ? + SET is_deleted_from_sender = 1, content = NULL, media_id = NULL, + additional_message_data = NULL, modified_at = ? WHERE message_id = ? AND sender_id = ?"#, timestamp, message_id, diff --git a/rust/src/frb_generated.rs b/rust/src/frb_generated.rs index 1057fb23..4270374f 100644 --- a/rust/src/frb_generated.rs +++ b/rust/src/frb_generated.rs @@ -38,7 +38,7 @@ flutter_rust_bridge::frb_generated_boilerplate!( default_rust_auto_opaque = RustAutoOpaqueMoi, ); pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0"; -pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1336092087; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1844664049; // Section: executor @@ -1880,6 +1880,43 @@ fn wire__crate__bridge__api__rust_api_connection_state_impl( }, ) } +fn wire__crate__bridge__api__rust_api_create_sticker_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "rust_api_create_sticker", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_image_path = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = + crate::bridge::api::RustApi::create_sticker(api_image_path).await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__bridge__api__rust_api_crop_media_transparent_borders_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -7730,6 +7767,24 @@ impl SseDecode for crate::database::app::SqlValue { } } +impl SseDecode for crate::services::stickers::StickerOutput { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_webp = >::sse_decode(deserializer); + let mut var_sha256 = >::sse_decode(deserializer); + let mut var_contentHash = ::sse_decode(deserializer); + let mut var_width = ::sse_decode(deserializer); + let mut var_height = ::sse_decode(deserializer); + return crate::services::stickers::StickerOutput { + webp: var_webp, + sha256: var_sha256, + content_hash: var_contentHash, + width: var_width, + height: var_height, + }; + } +} + impl SseDecode for crate::bridge::stories::StoryAudience { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -8134,132 +8189,133 @@ fn pde_ffi_dispatcher_primary_impl( 49 => wire__crate__bridge__api__rust_api_confirm_memories_upload_impl(port, ptr, rust_vec_len, data_len), 50 => wire__crate__bridge__api__rust_api_connect_impl(port, ptr, rust_vec_len, data_len), 51 => wire__crate__bridge__api__rust_api_connection_state_impl(port, ptr, rust_vec_len, data_len), -52 => wire__crate__bridge__api__rust_api_crop_media_transparent_borders_impl(port, ptr, rust_vec_len, data_len), -53 => wire__crate__bridge__api__rust_api_current_user_avatar_path_impl(port, ptr, rust_vec_len, data_len), -55 => wire__crate__bridge__api__rust_api_delete_account_impl(port, ptr, rust_vec_len, data_len), -56 => wire__crate__bridge__api__rust_api_delete_memory_impl(port, ptr, rust_vec_len, data_len), -57 => wire__crate__bridge__api__rust_api_delete_widget_media_impl(port, ptr, rust_vec_len, data_len), -58 => wire__crate__bridge__api__rust_api_disable_memories_backup_impl(port, ptr, rust_vec_len, data_len), -59 => wire__crate__bridge__api__rust_api_download_done_impl(port, ptr, rust_vec_len, data_len), -60 => wire__crate__bridge__api__rust_api_download_media_impl(port, ptr, rust_vec_len, data_len), -61 => wire__crate__bridge__api__rust_api_download_pending_media_impl(port, ptr, rust_vec_len, data_len), -62 => wire__crate__bridge__api__rust_api_ensure_avatar_png_impl(port, ptr, rust_vec_len, data_len), -63 => wire__crate__bridge__api__rust_api_establish_signal_session_impl(port, ptr, rust_vec_len, data_len), -64 => wire__crate__bridge__api__rust_api_events_impl(port, ptr, rust_vec_len, data_len), -65 => wire__crate__bridge__api__rust_api_finish_started_media_uploads_impl(port, ptr, rust_vec_len, data_len), -66 => wire__crate__bridge__api__rust_api_force_ipa_check_impl(port, ptr, rust_vec_len, data_len), -67 => wire__crate__bridge__api__rust_api_get_memories_url_impl(port, ptr, rust_vec_len, data_len), -68 => wire__crate__bridge__api__rust_api_get_memories_usage_impl(port, ptr, rust_vec_len, data_len), -69 => wire__crate__bridge__api__rust_api_get_plan_balance_impl(port, ptr, rust_vec_len, data_len), -70 => wire__crate__bridge__api__rust_api_get_proof_of_work_impl(port, ptr, rust_vec_len, data_len), -71 => wire__crate__bridge__api__rust_api_get_server_key_for_passwordless_recovery_impl(port, ptr, rust_vec_len, data_len), -72 => wire__crate__bridge__api__rust_api_get_user_by_id_impl(port, ptr, rust_vec_len, data_len), -73 => wire__crate__bridge__api__rust_api_get_user_data_impl(port, ptr, rust_vec_len, data_len), -74 => wire__crate__bridge__api__rust_api_get_user_id_from_username_impl(port, ptr, rust_vec_len, data_len), -75 => wire__crate__bridge__api__rust_api_hand_outbox_to_os_impl(port, ptr, rust_vec_len, data_len), -76 => wire__crate__bridge__api__rust_api_initialize_media_upload_impl(port, ptr, rust_vec_len, data_len), -77 => wire__crate__bridge__api__rust_api_initialize_memory_location_impl(port, ptr, rust_vec_len, data_len), -78 => wire__crate__bridge__api__rust_api_insert_and_send_additional_data_impl(port, ptr, rust_vec_len, data_len), -79 => wire__crate__bridge__api__rust_api_insert_and_send_ask_about_user_impl(port, ptr, rust_vec_len, data_len), -80 => wire__crate__bridge__api__rust_api_insert_and_send_contact_share_impl(port, ptr, rust_vec_len, data_len), -81 => wire__crate__bridge__api__rust_api_insert_and_send_text_impl(port, ptr, rust_vec_len, data_len), -82 => wire__crate__bridge__api__rust_api_ipa_purchase_impl(port, ptr, rust_vec_len, data_len), -83 => wire__crate__bridge__api__rust_api_load_plan_balance_impl(port, ptr, rust_vec_len, data_len), -84 => wire__crate__bridge__api__rust_api_media_size_limit_report_impl(port, ptr, rust_vec_len, data_len), -85 => wire__crate__bridge__api__rust_api_media_step_finished_impl(port, ptr, rust_vec_len, data_len), -86 => wire__crate__bridge__api__rust_api_notification_badge_count_impl(port, ptr, rust_vec_len, data_len), -87 => wire__crate__bridge__api__rust_api_notify_messages_opened_impl(port, ptr, rust_vec_len, data_len), -88 => wire__crate__bridge__api__rust_api_perform_passwordless_recovery_heartbeat_impl(port, ptr, rust_vec_len, data_len), -89 => wire__crate__bridge__api__rust_api_prerender_media_impl(port, ptr, rust_vec_len, data_len), -90 => wire__crate__bridge__api__rust_api_prewarm_memory_location_impl(port, ptr, rust_vec_len, data_len), -91 => wire__crate__bridge__api__rust_api_purge_media_temp_folder_impl(port, ptr, rust_vec_len, data_len), -92 => wire__crate__bridge__api__rust_api_purge_widget_media_impl(port, ptr, rust_vec_len, data_len), -93 => wire__crate__bridge__api__rust_api_refresh_widget_manifest_impl(port, ptr, rust_vec_len, data_len), -94 => wire__crate__bridge__api__rust_api_register_impl(port, ptr, rust_vec_len, data_len), -95 => wire__crate__bridge__api__rust_api_register_home_widget_impl(port, ptr, rust_vec_len, data_len), -96 => wire__crate__bridge__api__rust_api_register_passwordless_notification_impl(port, ptr, rust_vec_len, data_len), -97 => wire__crate__bridge__api__rust_api_register_passwordless_recovery_impl(port, ptr, rust_vec_len, data_len), -98 => wire__crate__bridge__api__rust_api_reload_configuration_impl(port, ptr, rust_vec_len, data_len), -99 => wire__crate__bridge__api__rust_api_remove_additional_user_impl(port, ptr, rust_vec_len, data_len), -100 => wire__crate__bridge__api__rust_api_remove_media_files_impl(port, ptr, rust_vec_len, data_len), -101 => wire__crate__bridge__api__rust_api_report_user_impl(port, ptr, rust_vec_len, data_len), -102 => wire__crate__bridge__api__rust_api_request_binary_impl(port, ptr, rust_vec_len, data_len), -103 => wire__crate__bridge__api__rust_api_request_contact_by_username_impl(port, ptr, rust_vec_len, data_len), -104 => wire__crate__bridge__api__rust_api_request_media_reupload_impl(port, ptr, rust_vec_len, data_len), -105 => wire__crate__bridge__api__rust_api_request_memories_upload_impl(port, ptr, rust_vec_len, data_len), -106 => wire__crate__bridge__api__rust_api_retransmit_all_messages_impl(port, ptr, rust_vec_len, data_len), -107 => wire__crate__bridge__api__rust_api_retry_pending_media_reuploads_impl(port, ptr, rust_vec_len, data_len), -108 => wire__crate__bridge__api__rust_api_reupload_pending_media_impl(port, ptr, rust_vec_len, data_len), -109 => wire__crate__bridge__api__rust_api_save_media_to_gallery_impl(port, ptr, rust_vec_len, data_len), -110 => wire__crate__bridge__api__rust_api_send_binary_impl(port, ptr, rust_vec_len, data_len), -111 => wire__crate__bridge__api__rust_api_send_contact_profile_impl(port, ptr, rust_vec_len, data_len), -112 => wire__crate__bridge__api__rust_api_send_encrypted_content_impl(port, ptr, rust_vec_len, data_len), -113 => wire__crate__bridge__api__rust_api_send_encrypted_content_to_group_impl(port, ptr, rust_vec_len, data_len), -114 => wire__crate__bridge__api__rust_api_send_media_to_groups_impl(port, ptr, rust_vec_len, data_len), -115 => wire__crate__bridge__api__rust_api_send_queued_message_impl(port, ptr, rust_vec_len, data_len), -116 => wire__crate__bridge__api__rust_api_send_text_message_impl(port, ptr, rust_vec_len, data_len), -117 => wire__crate__bridge__api__rust_api_send_typing_impl(port, ptr, rust_vec_len, data_len), -118 => wire__crate__bridge__api__rust_api_set_background_impl(port, ptr, rust_vec_len, data_len), -119 => wire__crate__bridge__api__rust_api_set_home_widget_groups_impl(port, ptr, rust_vec_len, data_len), -120 => wire__crate__bridge__api__rust_api_set_login_token_impl(port, ptr, rust_vec_len, data_len), -121 => wire__crate__bridge__api__rust_api_set_media_display_limit_impl(port, ptr, rust_vec_len, data_len), -122 => wire__crate__bridge__api__rust_api_set_media_requires_authentication_impl(port, ptr, rust_vec_len, data_len), -123 => wire__crate__bridge__api__rust_api_set_media_trim_impl(port, ptr, rust_vec_len, data_len), -124 => wire__crate__bridge__api__rust_api_set_network_available_impl(port, ptr, rust_vec_len, data_len), -125 => wire__crate__bridge__api__rust_api_store_media_impl(port, ptr, rust_vec_len, data_len), -126 => wire__crate__bridge__api__rust_api_submit_recovery_share_impl(port, ptr, rust_vec_len, data_len), -127 => wire__crate__bridge__api__rust_api_sync_widget_permissions_impl(port, ptr, rust_vec_len, data_len), -128 => wire__crate__bridge__api__rust_api_toggle_media_remove_audio_impl(port, ptr, rust_vec_len, data_len), -129 => wire__crate__bridge__api__rust_api_try_request_contact_by_id_impl(port, ptr, rust_vec_len, data_len), -130 => wire__crate__bridge__api__rust_api_unregister_home_widget_impl(port, ptr, rust_vec_len, data_len), -131 => wire__crate__bridge__api__rust_api_update_fcm_token_impl(port, ptr, rust_vec_len, data_len), -132 => wire__crate__bridge__api__rust_api_update_signed_pre_key_impl(port, ptr, rust_vec_len, data_len), -133 => wire__crate__bridge__api__rust_api_upload_pqc_pre_keys_impl(port, ptr, rust_vec_len, data_len), -134 => wire__crate__bridge__wrapper__app_database__rust_app_database_changes_impl(port, ptr, rust_vec_len, data_len), -135 => wire__crate__bridge__wrapper__app_database__rust_app_database_execute_impl(port, ptr, rust_vec_len, data_len), -136 => wire__crate__bridge__wrapper__app_database__rust_app_database_legacy_import_complete_impl(port, ptr, rust_vec_len, data_len), -137 => wire__crate__bridge__wrapper__app_database__rust_app_database_migrate_legacy_database_impl(port, ptr, rust_vec_len, data_len), -138 => wire__crate__bridge__wrapper__app_database__rust_app_database_select_impl(port, ptr, rust_vec_len, data_len), -139 => wire__crate__bridge__wrapper__backup__rust_backup_archive_create_backup_archive_impl(port, ptr, rust_vec_len, data_len), -140 => wire__crate__bridge__wrapper__backup__rust_backup_archive_get_backup_download_token_impl(port, ptr, rust_vec_len, data_len), -141 => wire__crate__bridge__wrapper__backup__rust_backup_archive_restore_backup_archive_impl(port, ptr, rust_vec_len, data_len), -142 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_id_impl(port, ptr, rust_vec_len, data_len), -143 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), -144 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_identity_backup_bytes_impl(port, ptr, rust_vec_len, data_len), -145 => wire__crate__bridge__wrapper__backup__rust_backup_identity_import_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), -146 => wire__crate__bridge__wrapper__backup__rust_backup_identity_restore_identity_backup_impl(port, ptr, rust_vec_len, data_len), -147 => wire__crate__bridge__wrapper__backup__rust_backup_identity_set_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), -148 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_decrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len), -149 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_encrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len), -150 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_signal_identity_impl(port, ptr, rust_vec_len, data_len), -151 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_user_id_impl(port, ptr, rust_vec_len, data_len), -152 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_serialized_impl(port, ptr, rust_vec_len, data_len), -153 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_identity_impl(port, ptr, rust_vec_len, data_len), -154 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_local_credentials_impl(port, ptr, rust_vec_len, data_len), -155 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_serialize_impl(port, ptr, rust_vec_len, data_len), -156 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_set_user_id_impl(port, ptr, rust_vec_len, data_len), -157 => wire__crate__bridge__wrapper__signal__rust_signal_decrypt_impl(port, ptr, rust_vec_len, data_len), -158 => wire__crate__bridge__wrapper__signal__rust_signal_encrypt_impl(port, ptr, rust_vec_len, data_len), -159 => wire__crate__bridge__wrapper__signal__rust_signal_generate_bundle_impl(port, ptr, rust_vec_len, data_len), -160 => wire__crate__bridge__wrapper__signal__rust_signal_generate_pqc_prekeys_impl(port, ptr, rust_vec_len, data_len), -161 => wire__crate__bridge__wrapper__signal__rust_signal_get_contact_public_key_impl(port, ptr, rust_vec_len, data_len), -162 => wire__crate__bridge__wrapper__signal__rust_signal_get_safety_number_impl(port, ptr, rust_vec_len, data_len), -163 => wire__crate__bridge__wrapper__signal__rust_signal_get_user_public_key_impl(port, ptr, rust_vec_len, data_len), -164 => wire__crate__bridge__wrapper__signal__rust_signal_process_prekey_bundle_impl(port, ptr, rust_vec_len, data_len), -165 => wire__crate__bridge__wrapper__signal__rust_signal_reset_contact_session_impl(port, ptr, rust_vec_len, data_len), -166 => wire__crate__bridge__wrapper__rust_utils_generate_shares_impl(port, ptr, rust_vec_len, data_len), -167 => wire__crate__bridge__wrapper__rust_utils_recover_secret_impl(port, ptr, rust_vec_len, data_len), -168 => wire__crate__bridge__webxdc__send_update_impl(port, ptr, rust_vec_len, data_len), -169 => wire__crate__bridge__webxdc__serve_impl(port, ptr, rust_vec_len, data_len), -170 => wire__crate__bridge__stories__story_audience_chats_impl(port, ptr, rust_vec_len, data_len), -171 => wire__crate__bridge__groups__update_chat_deletion_time_impl(port, ptr, rust_vec_len, data_len), -172 => wire__crate__bridge__groups__update_group_name_impl(port, ptr, rust_vec_len, data_len), -173 => wire__crate__bridge__webxdc__updates_after_impl(port, ptr, rust_vec_len, data_len), -175 => wire__crate__bridge__user_config__user_config_api_create_impl(port, ptr, rust_vec_len, data_len), -176 => wire__crate__bridge__user_config__user_config_api_import_json_impl(port, ptr, rust_vec_len, data_len), -177 => wire__crate__bridge__user_config__user_config_api_load_impl(port, ptr, rust_vec_len, data_len), -178 => wire__crate__bridge__user_config__user_config_api_save_impl(port, ptr, rust_vec_len, data_len), -179 => wire__crate__bridge__user_config__user_config_api_update_impl(port, ptr, rust_vec_len, data_len), +52 => wire__crate__bridge__api__rust_api_create_sticker_impl(port, ptr, rust_vec_len, data_len), +53 => wire__crate__bridge__api__rust_api_crop_media_transparent_borders_impl(port, ptr, rust_vec_len, data_len), +54 => wire__crate__bridge__api__rust_api_current_user_avatar_path_impl(port, ptr, rust_vec_len, data_len), +56 => wire__crate__bridge__api__rust_api_delete_account_impl(port, ptr, rust_vec_len, data_len), +57 => wire__crate__bridge__api__rust_api_delete_memory_impl(port, ptr, rust_vec_len, data_len), +58 => wire__crate__bridge__api__rust_api_delete_widget_media_impl(port, ptr, rust_vec_len, data_len), +59 => wire__crate__bridge__api__rust_api_disable_memories_backup_impl(port, ptr, rust_vec_len, data_len), +60 => wire__crate__bridge__api__rust_api_download_done_impl(port, ptr, rust_vec_len, data_len), +61 => wire__crate__bridge__api__rust_api_download_media_impl(port, ptr, rust_vec_len, data_len), +62 => wire__crate__bridge__api__rust_api_download_pending_media_impl(port, ptr, rust_vec_len, data_len), +63 => wire__crate__bridge__api__rust_api_ensure_avatar_png_impl(port, ptr, rust_vec_len, data_len), +64 => wire__crate__bridge__api__rust_api_establish_signal_session_impl(port, ptr, rust_vec_len, data_len), +65 => wire__crate__bridge__api__rust_api_events_impl(port, ptr, rust_vec_len, data_len), +66 => wire__crate__bridge__api__rust_api_finish_started_media_uploads_impl(port, ptr, rust_vec_len, data_len), +67 => wire__crate__bridge__api__rust_api_force_ipa_check_impl(port, ptr, rust_vec_len, data_len), +68 => wire__crate__bridge__api__rust_api_get_memories_url_impl(port, ptr, rust_vec_len, data_len), +69 => wire__crate__bridge__api__rust_api_get_memories_usage_impl(port, ptr, rust_vec_len, data_len), +70 => wire__crate__bridge__api__rust_api_get_plan_balance_impl(port, ptr, rust_vec_len, data_len), +71 => wire__crate__bridge__api__rust_api_get_proof_of_work_impl(port, ptr, rust_vec_len, data_len), +72 => wire__crate__bridge__api__rust_api_get_server_key_for_passwordless_recovery_impl(port, ptr, rust_vec_len, data_len), +73 => wire__crate__bridge__api__rust_api_get_user_by_id_impl(port, ptr, rust_vec_len, data_len), +74 => wire__crate__bridge__api__rust_api_get_user_data_impl(port, ptr, rust_vec_len, data_len), +75 => wire__crate__bridge__api__rust_api_get_user_id_from_username_impl(port, ptr, rust_vec_len, data_len), +76 => wire__crate__bridge__api__rust_api_hand_outbox_to_os_impl(port, ptr, rust_vec_len, data_len), +77 => wire__crate__bridge__api__rust_api_initialize_media_upload_impl(port, ptr, rust_vec_len, data_len), +78 => wire__crate__bridge__api__rust_api_initialize_memory_location_impl(port, ptr, rust_vec_len, data_len), +79 => wire__crate__bridge__api__rust_api_insert_and_send_additional_data_impl(port, ptr, rust_vec_len, data_len), +80 => wire__crate__bridge__api__rust_api_insert_and_send_ask_about_user_impl(port, ptr, rust_vec_len, data_len), +81 => wire__crate__bridge__api__rust_api_insert_and_send_contact_share_impl(port, ptr, rust_vec_len, data_len), +82 => wire__crate__bridge__api__rust_api_insert_and_send_text_impl(port, ptr, rust_vec_len, data_len), +83 => wire__crate__bridge__api__rust_api_ipa_purchase_impl(port, ptr, rust_vec_len, data_len), +84 => wire__crate__bridge__api__rust_api_load_plan_balance_impl(port, ptr, rust_vec_len, data_len), +85 => wire__crate__bridge__api__rust_api_media_size_limit_report_impl(port, ptr, rust_vec_len, data_len), +86 => wire__crate__bridge__api__rust_api_media_step_finished_impl(port, ptr, rust_vec_len, data_len), +87 => wire__crate__bridge__api__rust_api_notification_badge_count_impl(port, ptr, rust_vec_len, data_len), +88 => wire__crate__bridge__api__rust_api_notify_messages_opened_impl(port, ptr, rust_vec_len, data_len), +89 => wire__crate__bridge__api__rust_api_perform_passwordless_recovery_heartbeat_impl(port, ptr, rust_vec_len, data_len), +90 => wire__crate__bridge__api__rust_api_prerender_media_impl(port, ptr, rust_vec_len, data_len), +91 => wire__crate__bridge__api__rust_api_prewarm_memory_location_impl(port, ptr, rust_vec_len, data_len), +92 => wire__crate__bridge__api__rust_api_purge_media_temp_folder_impl(port, ptr, rust_vec_len, data_len), +93 => wire__crate__bridge__api__rust_api_purge_widget_media_impl(port, ptr, rust_vec_len, data_len), +94 => wire__crate__bridge__api__rust_api_refresh_widget_manifest_impl(port, ptr, rust_vec_len, data_len), +95 => wire__crate__bridge__api__rust_api_register_impl(port, ptr, rust_vec_len, data_len), +96 => wire__crate__bridge__api__rust_api_register_home_widget_impl(port, ptr, rust_vec_len, data_len), +97 => wire__crate__bridge__api__rust_api_register_passwordless_notification_impl(port, ptr, rust_vec_len, data_len), +98 => wire__crate__bridge__api__rust_api_register_passwordless_recovery_impl(port, ptr, rust_vec_len, data_len), +99 => wire__crate__bridge__api__rust_api_reload_configuration_impl(port, ptr, rust_vec_len, data_len), +100 => wire__crate__bridge__api__rust_api_remove_additional_user_impl(port, ptr, rust_vec_len, data_len), +101 => wire__crate__bridge__api__rust_api_remove_media_files_impl(port, ptr, rust_vec_len, data_len), +102 => wire__crate__bridge__api__rust_api_report_user_impl(port, ptr, rust_vec_len, data_len), +103 => wire__crate__bridge__api__rust_api_request_binary_impl(port, ptr, rust_vec_len, data_len), +104 => wire__crate__bridge__api__rust_api_request_contact_by_username_impl(port, ptr, rust_vec_len, data_len), +105 => wire__crate__bridge__api__rust_api_request_media_reupload_impl(port, ptr, rust_vec_len, data_len), +106 => wire__crate__bridge__api__rust_api_request_memories_upload_impl(port, ptr, rust_vec_len, data_len), +107 => wire__crate__bridge__api__rust_api_retransmit_all_messages_impl(port, ptr, rust_vec_len, data_len), +108 => wire__crate__bridge__api__rust_api_retry_pending_media_reuploads_impl(port, ptr, rust_vec_len, data_len), +109 => wire__crate__bridge__api__rust_api_reupload_pending_media_impl(port, ptr, rust_vec_len, data_len), +110 => wire__crate__bridge__api__rust_api_save_media_to_gallery_impl(port, ptr, rust_vec_len, data_len), +111 => wire__crate__bridge__api__rust_api_send_binary_impl(port, ptr, rust_vec_len, data_len), +112 => wire__crate__bridge__api__rust_api_send_contact_profile_impl(port, ptr, rust_vec_len, data_len), +113 => wire__crate__bridge__api__rust_api_send_encrypted_content_impl(port, ptr, rust_vec_len, data_len), +114 => wire__crate__bridge__api__rust_api_send_encrypted_content_to_group_impl(port, ptr, rust_vec_len, data_len), +115 => wire__crate__bridge__api__rust_api_send_media_to_groups_impl(port, ptr, rust_vec_len, data_len), +116 => wire__crate__bridge__api__rust_api_send_queued_message_impl(port, ptr, rust_vec_len, data_len), +117 => wire__crate__bridge__api__rust_api_send_text_message_impl(port, ptr, rust_vec_len, data_len), +118 => wire__crate__bridge__api__rust_api_send_typing_impl(port, ptr, rust_vec_len, data_len), +119 => wire__crate__bridge__api__rust_api_set_background_impl(port, ptr, rust_vec_len, data_len), +120 => wire__crate__bridge__api__rust_api_set_home_widget_groups_impl(port, ptr, rust_vec_len, data_len), +121 => wire__crate__bridge__api__rust_api_set_login_token_impl(port, ptr, rust_vec_len, data_len), +122 => wire__crate__bridge__api__rust_api_set_media_display_limit_impl(port, ptr, rust_vec_len, data_len), +123 => wire__crate__bridge__api__rust_api_set_media_requires_authentication_impl(port, ptr, rust_vec_len, data_len), +124 => wire__crate__bridge__api__rust_api_set_media_trim_impl(port, ptr, rust_vec_len, data_len), +125 => wire__crate__bridge__api__rust_api_set_network_available_impl(port, ptr, rust_vec_len, data_len), +126 => wire__crate__bridge__api__rust_api_store_media_impl(port, ptr, rust_vec_len, data_len), +127 => wire__crate__bridge__api__rust_api_submit_recovery_share_impl(port, ptr, rust_vec_len, data_len), +128 => wire__crate__bridge__api__rust_api_sync_widget_permissions_impl(port, ptr, rust_vec_len, data_len), +129 => wire__crate__bridge__api__rust_api_toggle_media_remove_audio_impl(port, ptr, rust_vec_len, data_len), +130 => wire__crate__bridge__api__rust_api_try_request_contact_by_id_impl(port, ptr, rust_vec_len, data_len), +131 => wire__crate__bridge__api__rust_api_unregister_home_widget_impl(port, ptr, rust_vec_len, data_len), +132 => wire__crate__bridge__api__rust_api_update_fcm_token_impl(port, ptr, rust_vec_len, data_len), +133 => wire__crate__bridge__api__rust_api_update_signed_pre_key_impl(port, ptr, rust_vec_len, data_len), +134 => wire__crate__bridge__api__rust_api_upload_pqc_pre_keys_impl(port, ptr, rust_vec_len, data_len), +135 => wire__crate__bridge__wrapper__app_database__rust_app_database_changes_impl(port, ptr, rust_vec_len, data_len), +136 => wire__crate__bridge__wrapper__app_database__rust_app_database_execute_impl(port, ptr, rust_vec_len, data_len), +137 => wire__crate__bridge__wrapper__app_database__rust_app_database_legacy_import_complete_impl(port, ptr, rust_vec_len, data_len), +138 => wire__crate__bridge__wrapper__app_database__rust_app_database_migrate_legacy_database_impl(port, ptr, rust_vec_len, data_len), +139 => wire__crate__bridge__wrapper__app_database__rust_app_database_select_impl(port, ptr, rust_vec_len, data_len), +140 => wire__crate__bridge__wrapper__backup__rust_backup_archive_create_backup_archive_impl(port, ptr, rust_vec_len, data_len), +141 => wire__crate__bridge__wrapper__backup__rust_backup_archive_get_backup_download_token_impl(port, ptr, rust_vec_len, data_len), +142 => wire__crate__bridge__wrapper__backup__rust_backup_archive_restore_backup_archive_impl(port, ptr, rust_vec_len, data_len), +143 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_id_impl(port, ptr, rust_vec_len, data_len), +144 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), +145 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_identity_backup_bytes_impl(port, ptr, rust_vec_len, data_len), +146 => wire__crate__bridge__wrapper__backup__rust_backup_identity_import_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), +147 => wire__crate__bridge__wrapper__backup__rust_backup_identity_restore_identity_backup_impl(port, ptr, rust_vec_len, data_len), +148 => wire__crate__bridge__wrapper__backup__rust_backup_identity_set_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), +149 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_decrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len), +150 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_encrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len), +151 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_signal_identity_impl(port, ptr, rust_vec_len, data_len), +152 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_user_id_impl(port, ptr, rust_vec_len, data_len), +153 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_serialized_impl(port, ptr, rust_vec_len, data_len), +154 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_identity_impl(port, ptr, rust_vec_len, data_len), +155 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_local_credentials_impl(port, ptr, rust_vec_len, data_len), +156 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_serialize_impl(port, ptr, rust_vec_len, data_len), +157 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_set_user_id_impl(port, ptr, rust_vec_len, data_len), +158 => wire__crate__bridge__wrapper__signal__rust_signal_decrypt_impl(port, ptr, rust_vec_len, data_len), +159 => wire__crate__bridge__wrapper__signal__rust_signal_encrypt_impl(port, ptr, rust_vec_len, data_len), +160 => wire__crate__bridge__wrapper__signal__rust_signal_generate_bundle_impl(port, ptr, rust_vec_len, data_len), +161 => wire__crate__bridge__wrapper__signal__rust_signal_generate_pqc_prekeys_impl(port, ptr, rust_vec_len, data_len), +162 => wire__crate__bridge__wrapper__signal__rust_signal_get_contact_public_key_impl(port, ptr, rust_vec_len, data_len), +163 => wire__crate__bridge__wrapper__signal__rust_signal_get_safety_number_impl(port, ptr, rust_vec_len, data_len), +164 => wire__crate__bridge__wrapper__signal__rust_signal_get_user_public_key_impl(port, ptr, rust_vec_len, data_len), +165 => wire__crate__bridge__wrapper__signal__rust_signal_process_prekey_bundle_impl(port, ptr, rust_vec_len, data_len), +166 => wire__crate__bridge__wrapper__signal__rust_signal_reset_contact_session_impl(port, ptr, rust_vec_len, data_len), +167 => wire__crate__bridge__wrapper__rust_utils_generate_shares_impl(port, ptr, rust_vec_len, data_len), +168 => wire__crate__bridge__wrapper__rust_utils_recover_secret_impl(port, ptr, rust_vec_len, data_len), +169 => wire__crate__bridge__webxdc__send_update_impl(port, ptr, rust_vec_len, data_len), +170 => wire__crate__bridge__webxdc__serve_impl(port, ptr, rust_vec_len, data_len), +171 => wire__crate__bridge__stories__story_audience_chats_impl(port, ptr, rust_vec_len, data_len), +172 => wire__crate__bridge__groups__update_chat_deletion_time_impl(port, ptr, rust_vec_len, data_len), +173 => wire__crate__bridge__groups__update_group_name_impl(port, ptr, rust_vec_len, data_len), +174 => wire__crate__bridge__webxdc__updates_after_impl(port, ptr, rust_vec_len, data_len), +176 => wire__crate__bridge__user_config__user_config_api_create_impl(port, ptr, rust_vec_len, data_len), +177 => wire__crate__bridge__user_config__user_config_api_import_json_impl(port, ptr, rust_vec_len, data_len), +178 => wire__crate__bridge__user_config__user_config_api_load_impl(port, ptr, rust_vec_len, data_len), +179 => wire__crate__bridge__user_config__user_config_api_save_impl(port, ptr, rust_vec_len, data_len), +180 => wire__crate__bridge__user_config__user_config_api_update_impl(port, ptr, rust_vec_len, data_len), _ => unreachable!(), } } @@ -8275,15 +8331,15 @@ fn pde_ffi_dispatcher_sync_impl( 28 => wire__crate__bridge__stories__max_media_recipients_impl(ptr, rust_vec_len, data_len), 40 => wire__crate__bridge__api__rust_api_api_base_url_impl(ptr, rust_vec_len, data_len), 42 => wire__crate__bridge__api__rust_api_avatar_png_path_impl(ptr, rust_vec_len, data_len), - 54 => { + 55 => { wire__crate__bridge__api__rust_api_decode_avatar_svg_impl(ptr, rust_vec_len, data_len) } - 174 => wire__crate__bridge__user_config__user_config_api_clone_impl( + 175 => wire__crate__bridge__user_config__user_config_api_clone_impl( ptr, rust_vec_len, data_len, ), - 180 => wire__crate__bridge__logging__write_log_impl(ptr, rust_vec_len, data_len), + 181 => wire__crate__bridge__logging__write_log_impl(ptr, rust_vec_len, data_len), _ => unreachable!(), } } @@ -9058,6 +9114,30 @@ impl flutter_rust_bridge::IntoIntoDart } } // Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::services::stickers::StickerOutput { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.webp.into_into_dart().into_dart(), + self.sha256.into_into_dart().into_dart(), + self.content_hash.into_into_dart().into_dart(), + self.width.into_into_dart().into_dart(), + self.height.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::services::stickers::StickerOutput +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::services::stickers::StickerOutput +{ + fn into_into_dart(self) -> crate::services::stickers::StickerOutput { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs impl flutter_rust_bridge::IntoDart for FrbWrapper { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { [ @@ -10214,6 +10294,17 @@ impl SseEncode for crate::database::app::SqlValue { } } +impl SseEncode for crate::services::stickers::StickerOutput { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >::sse_encode(self.webp, serializer); + >::sse_encode(self.sha256, serializer); + ::sse_encode(self.content_hash, serializer); + ::sse_encode(self.width, serializer); + ::sse_encode(self.height, serializer); + } +} + impl SseEncode for crate::bridge::stories::StoryAudience { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { diff --git a/rust/src/services/groups/mod.rs b/rust/src/services/groups/mod.rs index 6b6a49b9..53baefcd 100644 --- a/rust/src/services/groups/mod.rs +++ b/rust/src/services/groups/mod.rs @@ -239,6 +239,7 @@ impl GroupService { webxdc_origin: None, webxdc_sync: None, webxdc_sync_request: None, + sticker: None, } .encode_to_vec(); diff --git a/rust/src/services/media_codec.rs b/rust/src/services/media_codec.rs index 1ead9166..f55290ea 100644 --- a/rust/src/services/media_codec.rs +++ b/rust/src/services/media_codec.rs @@ -35,7 +35,7 @@ const MIN_CROPPED_EDGE: u32 = 10; /// Encodes with libwebp. RGB input is encoded without an alpha channel so /// opaque photos do not pay for one. -fn encode_webp(image: &DynamicImage, quality: f32) -> Result> { +pub(crate) fn encode_webp(image: &DynamicImage, quality: f32) -> Result> { let total_started = Instant::now(); let has_alpha = image.color().has_alpha(); let conversion_started = Instant::now(); diff --git a/rust/src/services/messages.rs b/rust/src/services/messages.rs index 7d21ee89..e7600585 100644 --- a/rust/src/services/messages.rs +++ b/rust/src/services/messages.rs @@ -240,6 +240,9 @@ impl MessageService { quote_message_id: Option, additional_message_data: Option>, ) -> Result { + if let Some(data) = additional_message_data.as_deref() { + crate::services::stickers::validate_additional_if_sticker(data)?; + } let database = self.ctx.app_db.read().await.clone(); let message_id = uuid::Uuid::new_v4().to_string(); let timestamp = chrono::Utc::now().timestamp_millis(); @@ -415,6 +418,7 @@ impl MessageService { webxdc_origin: None, webxdc_sync: None, webxdc_sync_request: None, + sticker: None, } .encode_to_vec(); self.insert_and_send_additional_data(group_id, "contacts".into(), data, false) @@ -452,6 +456,7 @@ impl MessageService { webxdc_origin: None, webxdc_sync: None, webxdc_sync_request: None, + sticker: None, } .encode_to_vec(); self.insert_and_send_additional_data(group_id, "askAboutUser".into(), data, false) diff --git a/rust/src/services/mod.rs b/rust/src/services/mod.rs index 4f3d9a74..858edede 100644 --- a/rust/src/services/mod.rs +++ b/rust/src/services/mod.rs @@ -17,5 +17,6 @@ pub mod mediafiles; pub mod messages; pub mod notifications; pub mod outbox_dispatch; +pub mod stickers; pub mod stories; pub mod webxdc; diff --git a/rust/src/services/notifications.rs b/rust/src/services/notifications.rs index 9ccbe1f5..af7c9cca 100644 --- a/rust/src/services/notifications.rs +++ b/rust/src/services/notifications.rs @@ -310,19 +310,30 @@ pub(crate) async fn record_incoming_event( let conversation_id = content.group_id.clone(); let now = current_time().timestamp(); let draft = if let Some(message) = content.text_message.as_ref() { + let is_sticker = message + .additional_message_data + .as_deref() + .and_then(|data| { + crate::services::stickers::decode_additional(data) + .ok() + .flatten() + }) + .is_some(); // An answer to the user's own story says so: an emoji is a reaction // to it, anything else a reply. let about_story = match message.quote_message_id.as_deref() { Some(quoted) => is_own_story(transaction, quoted).await?, None => false, }; - let (kind, reaction) = match (about_story, message.quote_message_id.is_some()) { - (true, _) if is_emoji_only(&message.text) => { + let (kind, reaction) = match (about_story, message.quote_message_id.is_some(), is_sticker) { + (true, _, true) => ("story_reply", None), + (true, _, false) if is_emoji_only(&message.text) => { ("story_reaction", Some(message.text.trim().to_owned())) } - (true, _) => ("story_reply", None), - (false, true) => ("response", None), - (false, false) => ("text", None), + (true, _, false) => ("story_reply", None), + (false, _, true) => ("sticker", None), + (false, true, false) => ("response", None), + (false, false, false) => ("text", None), }; Some(NotificationDraft { event_id: receipt_id.to_owned(), @@ -516,7 +527,7 @@ async fn clear_stale_opened(database: &Arc) -> Result<()> { UPDATE notification_outbox SET cleared_at = ? WHERE cleared_at IS NULL - AND kind IN ('text', 'response', 'image', 'video', 'audio', 'twonly', 'webxdc', 'story', 'story_reply', 'story_reaction') + AND kind IN ('text', 'response', 'sticker', 'image', 'video', 'audio', 'twonly', 'webxdc', 'story', 'story_reply', 'story_reaction') AND EXISTS ( SELECT 1 FROM messages @@ -838,7 +849,7 @@ pub(crate) async fn clear_opened_messages( SET cleared_at = ? WHERE message_id = ? AND cleared_at IS NULL - AND kind IN ('text', 'response', 'image', 'video', 'audio', 'twonly', 'webxdc', 'story', 'story_reply', 'story_reaction') + AND kind IN ('text', 'response', 'sticker', 'image', 'video', 'audio', 'twonly', 'webxdc', 'story', 'story_reply', 'story_reaction') "#, ) .bind(cleared_at) @@ -925,6 +936,7 @@ fn localized_body(locale: &str, row: &PendingRow) -> String { let key = match row.kind.as_str() { "text" => "notificationText", "response" => "notificationResponse", + "sticker" => "notificationSticker", "twonly" => "notificationTwonly", "video" => "notificationVideo", "image" => "notificationImage", diff --git a/rust/src/services/stickers.rs b/rust/src/services/stickers.rs new file mode 100644 index 00000000..081c0a29 --- /dev/null +++ b/rust/src/services/stickers.rs @@ -0,0 +1,352 @@ +/* + * Copyright (c) 2026, Tobias Müller git@tsmr.eu + * + */ + +//! On-device sticker creation and validation. +//! +//! The compact U²-Net model is compiled into the native library for now. The +//! digest check is intentionally kept beside model loading so a future model +//! downloader can feed these same trusted bytes without changing inference. + +use crate::api::proto::client::{self as proto, additional_message_data}; +use crate::error::{Result, TwonlyError}; +use crate::services::media_codec; +use image::imageops::{self, FilterType}; +use image::{DynamicImage, GrayImage, ImageDecoder, Luma, Rgba, RgbaImage}; +use prost::Message as _; +use sha2::{Digest, Sha256}; +use std::io::Cursor; +use std::path::Path; +use std::sync::OnceLock; +use tract::prelude::*; + +const MODEL_BYTES: &[u8] = include_bytes!("../../models/u2netp.onnx"); +const MODEL_SHA256: &str = "309c8469258dda742793dce0ebea8e6dd393174f89934733ecc8b14c76f4ddd8"; +const MODEL_EDGE: u32 = 320; +const WORKING_EDGE: u32 = 900; +pub(crate) const MAX_STICKER_EDGE: u32 = 300; +pub(crate) const MAX_STICKER_BYTES: usize = 128 * 1024; +pub(crate) const STICKER_VERSION: u32 = 1; +const TRANSPARENT_ALPHA: u8 = 10; + +static MODEL: OnceLock> = OnceLock::new(); + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct StickerOutput { + pub webp: Vec, + pub sha256: Vec, + pub content_hash: String, + pub width: u32, + pub height: u32, +} + +fn sticker_error(message: impl Into) -> TwonlyError { + TwonlyError::Generic(message.into()) +} + +fn model() -> Result<&'static Runnable> { + let loaded = MODEL.get_or_init(|| { + let digest = hex::encode(Sha256::digest(MODEL_BYTES)); + if digest != MODEL_SHA256 { + return Err(format!( + "bundled sticker model digest mismatch: expected {MODEL_SHA256}, got {digest}" + )); + } + tract::onnx() + .and_then(|onnx| onnx.load_buffer(MODEL_BYTES)) + .and_then(|model| model.into_model()) + .and_then(|model| model.into_runnable()) + .map_err(|error| format!("could not load bundled sticker model: {error}")) + }); + loaded + .as_ref() + .map_err(|error| sticker_error(error.clone())) +} + +/// Removes the background from `source`, trims transparent borders and returns +/// a compact transparent WebP suitable for embedding in a text message. +pub(crate) fn create(source: &Path) -> Result { + let source = media_codec::decode(source)?; + if source.width() == 0 || source.height() == 0 { + return Err(sticker_error("the sticker source image is empty")); + } + + // There is no visual value in carrying a multi-megapixel source through + // compositing when the wire result is capped at 300 px. Keeping a 3x + // working image still gives the resized segmentation mask room to soften. + let source = source.thumbnail(WORKING_EDGE, WORKING_EDGE).to_rgba8(); + let input_image = imageops::resize( + &DynamicImage::ImageRgba8(source.clone()).to_rgb8(), + MODEL_EDGE, + MODEL_EDGE, + FilterType::Triangle, + ); + + let pixel_count = (MODEL_EDGE * MODEL_EDGE) as usize; + let mut input = vec![0.0_f32; pixel_count * 3]; + const MEAN: [f32; 3] = [0.485, 0.456, 0.406]; + const STD: [f32; 3] = [0.229, 0.224, 0.225]; + for (index, pixel) in input_image.pixels().enumerate() { + for channel in 0..3 { + input[channel * pixel_count + index] = + (f32::from(pixel[channel]) / 255.0 - MEAN[channel]) / STD[channel]; + } + } + + let tensor = Tensor::from_slice(&[1, 3, MODEL_EDGE as usize, MODEL_EDGE as usize], &input) + .map_err(|error| sticker_error(format!("could not create sticker input: {error}")))?; + let outputs = model()? + .run([tensor]) + .map_err(|error| sticker_error(format!("sticker background removal failed: {error}")))?; + let foreground = outputs + .first() + .ok_or_else(|| sticker_error("sticker model returned no mask"))? + .as_slice::() + .map_err(|error| sticker_error(format!("sticker mask has an invalid type: {error}")))?; + if foreground.len() < pixel_count { + return Err(sticker_error("sticker model returned a truncated mask")); + } + + let foreground = &foreground[..pixel_count]; + let (mut min, mut max) = (f32::INFINITY, f32::NEG_INFINITY); + for value in foreground.iter().copied().filter(|value| value.is_finite()) { + min = min.min(value); + max = max.max(value); + } + if !min.is_finite() || !max.is_finite() || max - min < f32::EPSILON { + return Err(sticker_error("sticker model produced an empty mask")); + } + + let mask = GrayImage::from_fn(MODEL_EDGE, MODEL_EDGE, |x, y| { + let value = foreground[(y * MODEL_EDGE + x) as usize]; + let normalized = ((value - min) / (max - min)).clamp(0.0, 1.0); + Luma([(normalized * 255.0).round() as u8]) + }); + let mask = imageops::resize( + &mask, + source.width(), + source.height(), + FilterType::CatmullRom, + ); + + let mut composited = source; + for (pixel, alpha) in composited.pixels_mut().zip(mask.pixels()) { + pixel[3] = ((u16::from(pixel[3]) * u16::from(alpha[0])) / 255) as u8; + } + + let bounds = visible_bounds(&composited) + .ok_or_else(|| sticker_error("no foreground was found in this image"))?; + let cropped = imageops::crop_imm( + &composited, + bounds.0, + bounds.1, + bounds.2 - bounds.0 + 1, + bounds.3 - bounds.1 + 1, + ) + .to_image(); + let padding = + ((cropped.width().max(cropped.height()) as f32 * 0.025).round() as u32).clamp(2, 18); + let mut padded = RgbaImage::from_pixel( + cropped.width() + padding * 2, + cropped.height() + padding * 2, + Rgba([0, 0, 0, 0]), + ); + imageops::overlay( + &mut padded, + &cropped, + i64::from(padding), + i64::from(padding), + ); + + let mut sticker = + DynamicImage::ImageRgba8(padded).thumbnail(MAX_STICKER_EDGE, MAX_STICKER_EDGE); + let mut encoded = Vec::new(); + 'encode: for edge in [300_u32, 280, 256, 224, 192] { + if sticker.width() > edge || sticker.height() > edge { + sticker = sticker.thumbnail(edge, edge); + } + for quality in [90.0_f32, 80.0, 70.0, 60.0] { + encoded = media_codec::encode_webp(&sticker, quality)?; + if encoded.len() <= MAX_STICKER_BYTES { + break 'encode; + } + } + } + if encoded.len() > MAX_STICKER_BYTES { + return Err(sticker_error(format!( + "sticker is too large after compression: {} bytes", + encoded.len() + ))); + } + + let sha256 = Sha256::digest(&encoded).to_vec(); + let output = StickerOutput { + content_hash: hex::encode(&sha256), + sha256, + width: sticker.width(), + height: sticker.height(), + webp: encoded, + }; + validate_sticker(&output.as_proto())?; + Ok(output) +} + +fn visible_bounds(image: &RgbaImage) -> Option<(u32, u32, u32, u32)> { + let (mut left, mut top) = (image.width(), image.height()); + let (mut right, mut bottom) = (0, 0); + let mut found = false; + for (x, y, pixel) in image.enumerate_pixels() { + if pixel[3] <= TRANSPARENT_ALPHA { + continue; + } + found = true; + left = left.min(x); + top = top.min(y); + right = right.max(x); + bottom = bottom.max(y); + } + found.then_some((left, top, right, bottom)) +} + +impl StickerOutput { + pub(crate) fn as_proto(&self) -> proto::StickerData { + proto::StickerData { + version: STICKER_VERSION, + webp: self.webp.clone(), + sha256: self.sha256.clone(), + width: self.width, + height: self.height, + } + } +} + +/// Returns the sticker carried by additional message data after checking all +/// wire bounds and its content digest. +pub(crate) fn decode_additional(bytes: &[u8]) -> Result> { + let data = proto::AdditionalMessageData::decode(bytes)?; + if data.r#type != additional_message_data::Type::Sticker as i32 { + return Ok(None); + } + let sticker = data + .sticker + .ok_or_else(|| sticker_error("sticker message is missing its image"))?; + validate_sticker(&sticker)?; + Ok(Some(sticker)) +} + +pub(crate) fn validate_additional_if_sticker(bytes: &[u8]) -> Result<()> { + let _ = decode_additional(bytes)?; + Ok(()) +} + +fn validate_sticker(sticker: &proto::StickerData) -> Result<()> { + if sticker.version != STICKER_VERSION { + return Err(sticker_error(format!( + "unsupported sticker version {}", + sticker.version + ))); + } + if sticker.webp.is_empty() || sticker.webp.len() > MAX_STICKER_BYTES { + return Err(sticker_error("sticker WebP has an invalid size")); + } + if sticker.sha256.len() != 32 || Sha256::digest(&sticker.webp).as_slice() != sticker.sha256 { + return Err(sticker_error("sticker digest does not match its bytes")); + } + if sticker.width == 0 + || sticker.height == 0 + || sticker.width > MAX_STICKER_EDGE + || sticker.height > MAX_STICKER_EDGE + { + return Err(sticker_error( + "sticker dimensions are outside the allowed bounds", + )); + } + if image::guess_format(&sticker.webp).ok() != Some(image::ImageFormat::WebP) { + return Err(sticker_error("sticker payload is not WebP")); + } + let decoder = image::codecs::webp::WebPDecoder::new(Cursor::new(&sticker.webp)) + .map_err(|error| sticker_error(format!("invalid sticker WebP: {error}")))?; + // Read the actual dimensions before allocating pixels. Declared protobuf + // dimensions cannot be trusted to bound a peer's image decoder allocation. + if decoder.dimensions() != (sticker.width, sticker.height) { + return Err(sticker_error("sticker dimensions do not match its WebP")); + } + if decoder.has_animation() { + return Err(sticker_error("animated stickers are not supported")); + } + let decoded = DynamicImage::from_decoder(decoder) + .map_err(|error| sticker_error(format!("invalid sticker WebP: {error}")))?; + if !decoded.color().has_alpha() || !decoded.to_rgba8().pixels().any(|pixel| pixel[3] > 0) { + return Err(sticker_error("sticker has no visible transparent image")); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn sample_sticker() -> proto::StickerData { + let image = DynamicImage::ImageRgba8(RgbaImage::from_fn(32, 24, |x, y| { + if x > 4 && x < 28 && y > 4 && y < 20 { + Rgba([240, 50, 80, 255]) + } else { + Rgba([0, 0, 0, 0]) + } + })); + let webp = media_codec::encode_webp(&image, 90.0).unwrap(); + proto::StickerData { + version: STICKER_VERSION, + sha256: Sha256::digest(&webp).to_vec(), + webp, + width: 32, + height: 24, + } + } + + #[test] + fn sticker_wire_data_round_trips_and_rejects_tampering() { + let sticker = sample_sticker(); + let data = proto::AdditionalMessageData { + r#type: additional_message_data::Type::Sticker as i32, + sticker: Some(sticker.clone()), + ..Default::default() + }; + assert_eq!( + decode_additional(&data.encode_to_vec()).unwrap(), + Some(sticker.clone()) + ); + let mut changed = sticker.clone(); + changed.webp[20] ^= 1; + assert!(validate_sticker(&changed).is_err()); + let mut changed = sticker.clone(); + changed.width = 300; + assert!(validate_sticker(&changed).is_err()); + let mut changed = sticker; + changed.version = 2; + assert!(validate_sticker(&changed).is_err()); + } + + #[test] + fn normal_additional_data_is_not_a_sticker() { + let data = proto::AdditionalMessageData::default(); + assert!(decode_additional(&data.encode_to_vec()).unwrap().is_none()); + } + + #[test] + fn bundled_model_creates_a_bounded_transparent_webp() { + let image = Path::new(env!("CARGO_MANIFEST_DIR")).join("../assets/images/logo.png"); + let sticker = create(&image).expect("bundled model must load and run"); + assert!(sticker.width <= 300 && sticker.height <= 300); + assert!(sticker.webp.len() <= MAX_STICKER_BYTES); + assert_eq!( + sticker.content_hash, + hex::encode(Sha256::digest(&sticker.webp)) + ); + validate_sticker(&sticker.as_proto()).unwrap(); + let image = image::load_from_memory(&sticker.webp).unwrap().to_rgba8(); + assert!(image.pixels().any(|pixel| pixel[3] == 0)); + assert!(image.pixels().any(|pixel| pixel[3] > 10)); + } +} diff --git a/test/drift/stickers_dao_test.dart b/test/drift/stickers_dao_test.dart new file mode 100644 index 00000000..4ed6c26b --- /dev/null +++ b/test/drift/stickers_dao_test.dart @@ -0,0 +1,75 @@ +import 'dart:typed_data'; + +import 'package:crypto/crypto.dart'; +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:twonly/src/database/twonly.db.dart'; + +void main() { + late TwonlyDB database; + + setUp(() => database = TwonlyDB(NativeDatabase.memory())); + tearDown(() => database.close()); + + test('identical stickers are deduplicated without resetting usage', () async { + final bytes = Uint8List.fromList([1, 2, 3]); + final hash = sha256.convert(bytes).toString(); + expect( + await database.stickersDao.save(webp: bytes, width: 30, height: 20), + isTrue, + ); + await database.stickersDao.recordUse(hash); + expect( + await database.stickersDao.save(webp: bytes, width: 30, height: 20), + isFalse, + ); + final stickers = await database.stickersDao.watchAll().first; + expect(stickers, hasLength(1)); + expect(stickers.single.usageCount, 1); + }); + + test( + 'most used stickers come first and removal only removes the selected one', + () async { + final first = Uint8List.fromList([1]); + final second = Uint8List.fromList([2]); + final firstHash = sha256.convert(first).toString(); + final secondHash = sha256.convert(second).toString(); + await database.stickersDao.save(webp: first, width: 30, height: 20); + await database.stickersDao.save(webp: second, width: 30, height: 20); + await database.stickersDao.recordUse(secondHash); + await database.stickersDao.recordUse(firstHash); + await database.stickersDao.recordUse(firstHash); + final stickers = await database.stickersDao.watchAll().first; + expect(stickers.map((sticker) => sticker.contentHash), [ + firstHash, + secondHash, + ]); + expect(stickers.map((sticker) => sticker.usageCount), [2, 1]); + await database.stickersDao.remove(firstHash); + expect(await database.stickersDao.getByHash(firstHash), isNull); + expect((await database.stickersDao.getByHash(secondHash))?.usageCount, 1); + }, + ); + + test( + 'false digests and invalid bounds cannot enter the collection', + () async { + final bytes = Uint8List.fromList([1]); + await expectLater( + database.stickersDao.save( + webp: bytes, + width: 30, + height: 20, + expectedHash: 'incorrect', + ), + throwsArgumentError, + ); + await expectLater( + database.stickersDao.save(webp: bytes, width: 301, height: 20), + throwsArgumentError, + ); + expect(await database.stickersDao.watchAll().first, isEmpty); + }, + ); +} diff --git a/test/services/notification_routing_test.dart b/test/services/notification_routing_test.dart index c9cd352a..6689209c 100644 --- a/test/services/notification_routing_test.dart +++ b/test/services/notification_routing_test.dart @@ -5,6 +5,7 @@ void main() { test('text notifications open their conversation', () { expect(NativeNotificationService.opensConversation('text'), isTrue); expect(NativeNotificationService.opensConversation('response'), isTrue); + expect(NativeNotificationService.opensConversation('sticker'), isTrue); }); test('replies and reactions to the own story open the conversation', () { diff --git a/test/visual/components/sticker_picker_test.dart b/test/visual/components/sticker_picker_test.dart new file mode 100644 index 00000000..722ade94 --- /dev/null +++ b/test/visual/components/sticker_picker_test.dart @@ -0,0 +1,206 @@ +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:crypto/crypto.dart'; +import 'package:drift/native.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/localization/generated/app_localizations.dart'; +import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart'; +import 'package:twonly/src/services/stickers/sticker.service.dart'; +import 'package:twonly/src/visual/components/emoji_picker/emoji_picker.dart'; +import 'package:twonly/src/visual/components/sticker_picker.dart'; +import 'package:twonly/src/visual/elements/my_button.element.dart'; +import 'package:twonly/src/visual/themes/light.dart'; +import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/chat_sticker.entry.dart'; + +// A small valid image for exercising image widgets; protocol codec validation +// and real transparent WebP inference are covered by the Rust sticker tests. +final Uint8List _image = File( + 'assets/images/default_avatar.png', +).readAsBytesSync(); + +StickerData _sticker() => StickerData( + version: 1, + webp: _image, + sha256: sha256.convert(_image).bytes, + width: 1, + height: 1, +); + +Widget _app(Widget child) => MaterialApp( + theme: lightTheme, + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Scaffold( + body: Align(alignment: Alignment.bottomCenter, child: child), + ), +); + +Future _settle(WidgetTester tester) async { + await tester.runAsync( + () => Future.delayed(const Duration(milliseconds: 50)), + ); + await tester.pumpAndSettle(); +} + +void main() { + late TwonlyDB database; + + setUp(() async { + SharedPreferences.setMockInitialValues({}); + await locator.reset(); + database = TwonlyDB(NativeDatabase.memory()); + locator.registerSingleton(database); + await database.customSelect('SELECT 1').get(); + }); + + tearDown(() async { + await database.close(); + await locator.reset(); + }); + + testWidgets( + 'tabs and resized drag handle stay in place across picker modes', + (tester) async { + var showStickers = false; + await tester.pumpWidget( + _app( + StatefulBuilder( + builder: (context, setState) { + return EmojiPicker( + alternateView: showStickers + ? StickerPicker( + onEmojiPressed: () => + setState(() => showStickers = false), + onStickerSelected: (_) {}, + ) + : null, + config: Config( + height: 300, + checkPlatformCompatibility: false, + bottomActionBarConfig: BottomActionBarConfig( + onStickerButtonPressed: () => + setState(() => showStickers = true), + ), + ), + ); + }, + ), + ), + ); + await _settle(tester); + final container = find.byKey(const Key('emojiPickerResizableContainer')); + final handle = find.byKey(const Key('emojiPickerDragHandle')); + await tester.drag(handle, const Offset(0, -80)); + await _settle(tester); + final height = tester.getSize(container).height; + expect(height, greaterThan(300)); + final emojiRect = tester.getRect(find.text('Emoji')); + final stickerRect = tester.getRect(find.text('Sticker')); + final handleRect = tester.getRect(handle); + await tester.tap(find.text('Sticker')); + await _settle(tester); + expect(tester.getRect(find.text('Emoji')), emojiRect); + expect(tester.getRect(find.text('Sticker')), stickerRect); + expect(tester.getRect(handle), handleRect); + expect(tester.getSize(container).height, height); + await tester.tap(find.text('Emoji')); + await _settle(tester); + expect(tester.getSize(container).height, height); + expect(tester.getRect(handle), handleRect); + }, + ); + + testWidgets('chat tap previews without saving; buttons save then remove', ( + tester, + ) async { + await tester.pumpWidget( + _app( + ChatStickerEntry( + message: Message( + groupId: 'group', + messageId: 'message', + senderId: 42, + type: 'text', + mediaStored: false, + mediaReopened: false, + isDeletedFromSender: false, + isWidgetMedia: false, + isStory: false, + createdAt: DateTime.now(), + ), + sticker: _sticker(), + ), + ), + ); + await _settle(tester); + await tester.tap(find.byType(ChatStickerEntry)); + await _settle(tester); + expect(find.byType(Dialog), findsOneWidget); + expect(find.text('Save sticker'), findsOneWidget); + expect( + await tester.runAsync(() => database.stickersDao.watchAll().first), + isEmpty, + ); + await tester.tap(find.byType(MyButton)); + await _settle(tester); + expect(find.byType(Dialog), findsNothing); + expect( + await tester.runAsync(() => database.stickersDao.watchAll().first), + hasLength(1), + ); + await tester.tap(find.byType(ChatStickerEntry)); + await _settle(tester); + expect(find.text('Remove from collection'), findsOneWidget); + await tester.tap(find.byType(MyButton)); + await _settle(tester); + expect( + await tester.runAsync(() => database.stickersDao.watchAll().first), + isEmpty, + ); + expect(find.byType(ChatStickerEntry), findsOneWidget); + }); + + testWidgets('long press in collection previews deletion without selecting', ( + tester, + ) async { + await tester.runAsync(() => StickerService.saveReceived(_sticker())); + var selected = false; + await tester.pumpWidget( + _app( + SizedBox( + height: 300, + child: StickerPicker( + onEmojiPressed: () {}, + onStickerSelected: (_) => selected = true, + ), + ), + ), + ); + await _settle(tester); + final tile = find.byKey(ValueKey(sha256.convert(_image).toString())); + await tester.longPress(tile); + await _settle(tester); + expect(selected, isFalse); + expect(find.text('Delete sticker'), findsOneWidget); + expect( + await tester.runAsync(() => database.stickersDao.watchAll().first), + hasLength(1), + ); + await tester.tap(find.byType(MyButton)); + await _settle(tester); + expect(find.byType(Dialog), findsNothing); + expect( + await tester.runAsync(() => database.stickersDao.watchAll().first), + isEmpty, + ); + expect(selected, isFalse); + // Dispose the live Drift query while the test can still drain its timers. + await tester.pumpWidget(const SizedBox.shrink()); + await tester.pumpAndSettle(); + }); +}