diff --git a/lib/core/bridge.dart b/lib/core/bridge.dart index e479d417..d6d667d2 100644 --- a/lib/core/bridge.dart +++ b/lib/core/bridge.dart @@ -18,6 +18,31 @@ Future initializeTwonlyFlutter({required InitConfig config}) => Future initializeTwonlyStandalone({required InitConfig config}) => RustLib.instance.api.crateBridgeInitializeTwonlyStandalone(config: config); +class FlameState { + final PlatformInt64 counter; + final bool isExpiring; + final bool isBestFriend; + + const FlameState({ + required this.counter, + required this.isExpiring, + required this.isBestFriend, + }); + + @override + int get hashCode => + counter.hashCode ^ isExpiring.hashCode ^ isBestFriend.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is FlameState && + runtimeType == other.runtimeType && + counter == other.counter && + isExpiring == other.isExpiring && + isBestFriend == other.isBestFriend; +} + class InitConfig { final String databaseDir; final String dataDir; diff --git a/lib/core/bridge/groups.dart b/lib/core/bridge/groups.dart index a4d8e5e1..ceba16dd 100644 --- a/lib/core/bridge/groups.dart +++ b/lib/core/bridge/groups.dart @@ -3,9 +3,28 @@ // ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import +import '../bridge.dart'; import '../frb_generated.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; +/// The flame counter for one chat, derived against today rather than read from +/// the stored column. See `Group::flame_states`. +Future flameState({required String groupId}) => + RustLib.instance.api.crateBridgeGroupsFlameState(groupId: groupId); + +/// The same derivation for several chats at once. Sorting a contact list by +/// streak needs every value up front, because a comparator cannot await. +Future> flameStates({required List groupIds}) => + RustLib.instance.api.crateBridgeGroupsFlameStates(groupIds: groupIds); + +/// Whether to offer the user a restore for this chat. +Future canRestoreFlames({required String groupId}) => + RustLib.instance.api.crateBridgeGroupsCanRestoreFlames(groupId: groupId); + +/// Restores the lost streak, posts the chat entry, and syncs the peer. +Future restoreFlames({required String groupId}) => + RustLib.instance.api.crateBridgeGroupsRestoreFlames(groupId: groupId); + Future createNewGroup({ required String groupName, required Int64List memberIds, diff --git a/lib/core/frb_generated.dart b/lib/core/frb_generated.dart index 357ec571..e7fc4383 100644 --- a/lib/core/frb_generated.dart +++ b/lib/core/frb_generated.dart @@ -87,7 +87,7 @@ class RustLib extends BaseEntrypoint { String get codegenVersion => '2.12.0'; @override - int get rustContentHash => 101877534; + int get rustContentHash => -2068471902; static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig( @@ -115,6 +115,8 @@ abstract class RustLibApi extends BaseApi { Future crateBridgeWebxdcCacheUpdate({required String instanceId}); + Future crateBridgeGroupsCanRestoreFlames({required String groupId}); + Future> crateBridgeWebxdcCatalog({ required String groupId, required List languages, @@ -146,6 +148,12 @@ abstract class RustLibApi extends BaseApi { required bool force, }); + Future crateBridgeGroupsFlameState({required String groupId}); + + Future> crateBridgeGroupsFlameStates({ + required List groupIds, + }); + Future crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryChangeExclusionForContact({ required int callbackId, @@ -209,6 +217,8 @@ abstract class RustLibApi extends BaseApi { required PlatformInt64 contactId, }); + Future crateBridgeGroupsRestoreFlames({required String groupId}); + Future crateBridgeApiRustApiAbandonMedia({required String mediaId}); Future crateBridgeApiRustApiAddAdditionalUser({ @@ -915,6 +925,37 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["instanceId"], ); + @override + Future crateBridgeGroupsCanRestoreFlames({required String groupId}) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(groupId, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 5, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_bool, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateBridgeGroupsCanRestoreFlamesConstMeta, + argValues: [groupId], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateBridgeGroupsCanRestoreFlamesConstMeta => + const TaskConstMeta( + debugName: "can_restore_flames", + argNames: ["groupId"], + ); + @override Future> crateBridgeWebxdcCatalog({ required String groupId, @@ -929,7 +970,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 5, + funcId: 6, port: port_, ); }, @@ -958,7 +999,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 6, + funcId: 7, port: port_, ); }, @@ -988,7 +1029,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 7, + funcId: 8, port: port_, ); }, @@ -1025,7 +1066,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 8, + funcId: 9, port: port_, ); }, @@ -1060,7 +1101,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 9, + funcId: 10, port: port_, ); }, @@ -1093,7 +1134,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 10, + funcId: 11, port: port_, ); }, @@ -1124,7 +1165,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 11, + funcId: 12, port: port_, ); }, @@ -1154,7 +1195,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 12, + funcId: 13, port: port_, ); }, @@ -1190,7 +1231,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 13, + funcId: 14, port: port_, ); }, @@ -1211,6 +1252,70 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["groupId", "force"], ); + @override + Future crateBridgeGroupsFlameState({required String groupId}) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(groupId, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 15, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_flame_state, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateBridgeGroupsFlameStateConstMeta, + argValues: [groupId], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateBridgeGroupsFlameStateConstMeta => + const TaskConstMeta( + debugName: "flame_state", + argNames: ["groupId"], + ); + + @override + Future> crateBridgeGroupsFlameStates({ + required List groupIds, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_list_String(groupIds, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 16, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_Map_String_flame_state_None, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateBridgeGroupsFlameStatesConstMeta, + argValues: [groupIds], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateBridgeGroupsFlameStatesConstMeta => + const TaskConstMeta( + debugName: "flame_states", + argNames: ["groupIds"], + ); + @override Future crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryChangeExclusionForContact({ @@ -1228,7 +1333,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 14, + funcId: 17, port: port_, ); }, @@ -1264,7 +1369,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 15, + funcId: 18, port: port_, ); }, @@ -1307,7 +1412,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 16, + funcId: 19, port: port_, ); }, @@ -1352,7 +1457,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 17, + funcId: 20, port: port_, ); }, @@ -1389,7 +1494,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 18, + funcId: 21, port: port_, ); }, @@ -1422,7 +1527,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 19, + funcId: 22, port: port_, ); }, @@ -1455,7 +1560,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 20, + funcId: 23, port: port_, ); }, @@ -1485,7 +1590,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 21, + funcId: 24, port: port_, ); }, @@ -1515,7 +1620,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 22, + funcId: 25, port: port_, ); }, @@ -1552,7 +1657,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 23, + funcId: 26, port: port_, ); }, @@ -1583,7 +1688,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 24, + funcId: 27, port: port_, ); }, @@ -1617,7 +1722,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 25, + funcId: 28, port: port_, ); }, @@ -1648,7 +1753,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 26, + funcId: 29, port: port_, ); }, @@ -1679,7 +1784,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 27, + funcId: 30, port: port_, ); }, @@ -1709,7 +1814,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 28, + funcId: 31, port: port_, ); }, @@ -1744,7 +1849,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 29, + funcId: 32, port: port_, ); }, @@ -1765,6 +1870,37 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["groupId", "contactId"], ); + @override + Future crateBridgeGroupsRestoreFlames({required String groupId}) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(groupId, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 33, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_bool, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateBridgeGroupsRestoreFlamesConstMeta, + argValues: [groupId], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateBridgeGroupsRestoreFlamesConstMeta => + const TaskConstMeta( + debugName: "restore_flames", + argNames: ["groupId"], + ); + @override Future crateBridgeApiRustApiAbandonMedia({required String mediaId}) { return handler.executeNormal( @@ -1775,7 +1911,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 30, + funcId: 34, port: port_, ); }, @@ -1808,7 +1944,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 31, + funcId: 35, port: port_, ); }, @@ -1838,7 +1974,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 32, + funcId: 36, port: port_, ); }, @@ -1866,7 +2002,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(protocol, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 33)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 37)!; }, codec: SseCodec( decodeSuccessData: sse_decode_String, @@ -1894,7 +2030,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 34, + funcId: 38, port: port_, ); }, @@ -1926,7 +2062,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_i_64(contactId, serializer); sse_encode_i_64(profileCounter, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 35)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 39)!; }, codec: SseCodec( decodeSuccessData: sse_decode_String, @@ -1955,7 +2091,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 36, + funcId: 40, port: port_, ); }, @@ -1985,7 +2121,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 37, + funcId: 41, port: port_, ); }, @@ -2026,7 +2162,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 38, + funcId: 42, port: port_, ); }, @@ -2067,7 +2203,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 39, + funcId: 43, port: port_, ); }, @@ -2102,7 +2238,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 40, + funcId: 44, port: port_, ); }, @@ -2134,7 +2270,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 41, + funcId: 45, port: port_, ); }, @@ -2166,7 +2302,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 42, + funcId: 46, port: port_, ); }, @@ -2196,7 +2332,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 43, + funcId: 47, port: port_, ); }, @@ -2226,7 +2362,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 44, + funcId: 48, port: port_, ); }, @@ -2259,7 +2395,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 45, + funcId: 49, port: port_, ); }, @@ -2290,7 +2426,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 46, + funcId: 50, port: port_, ); }, @@ -2320,7 +2456,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: 47)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 51)!; }, codec: SseCodec( decodeSuccessData: sse_decode_String, @@ -2348,7 +2484,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 48, + funcId: 52, port: port_, ); }, @@ -2379,7 +2515,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 49, + funcId: 53, port: port_, ); }, @@ -2412,7 +2548,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 50, + funcId: 54, port: port_, ); }, @@ -2442,7 +2578,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 51, + funcId: 55, port: port_, ); }, @@ -2473,7 +2609,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 52, + funcId: 56, port: port_, ); }, @@ -2504,7 +2640,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 53, + funcId: 57, port: port_, ); }, @@ -2534,7 +2670,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 54, + funcId: 58, port: port_, ); }, @@ -2567,7 +2703,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 55, + funcId: 59, port: port_, ); }, @@ -2602,7 +2738,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 56, + funcId: 60, port: port_, ); }, @@ -2635,7 +2771,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 57, + funcId: 61, port: port_, ); }, @@ -2667,7 +2803,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 58, + funcId: 62, port: port_, ); }, @@ -2697,7 +2833,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 59, + funcId: 63, port: port_, ); }, @@ -2732,7 +2868,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 60, + funcId: 64, port: port_, ); }, @@ -2762,7 +2898,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 61, + funcId: 65, port: port_, ); }, @@ -2792,7 +2928,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 62, + funcId: 66, port: port_, ); }, @@ -2822,7 +2958,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 63, + funcId: 67, port: port_, ); }, @@ -2863,7 +2999,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 64, + funcId: 68, port: port_, ); }, @@ -2910,7 +3046,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 65, + funcId: 69, port: port_, ); }, @@ -2943,7 +3079,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 66, + funcId: 70, port: port_, ); }, @@ -2976,7 +3112,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 67, + funcId: 71, port: port_, ); }, @@ -3006,7 +3142,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 68, + funcId: 72, port: port_, ); }, @@ -3046,7 +3182,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 69, + funcId: 73, port: port_, ); }, @@ -3079,7 +3215,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 70, + funcId: 74, port: port_, ); }, @@ -3118,7 +3254,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 71, + funcId: 75, port: port_, ); }, @@ -3154,7 +3290,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 72, + funcId: 76, port: port_, ); }, @@ -3189,7 +3325,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 73, + funcId: 77, port: port_, ); }, @@ -3231,7 +3367,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 74, + funcId: 78, port: port_, ); }, @@ -3273,7 +3409,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 75, + funcId: 79, port: port_, ); }, @@ -3303,7 +3439,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 76, + funcId: 80, port: port_, ); }, @@ -3336,7 +3472,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 77, + funcId: 81, port: port_, ); }, @@ -3371,7 +3507,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 78, + funcId: 82, port: port_, ); }, @@ -3401,7 +3537,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 79, + funcId: 83, port: port_, ); }, @@ -3436,7 +3572,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 80, + funcId: 84, port: port_, ); }, @@ -3466,7 +3602,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 81, + funcId: 85, port: port_, ); }, @@ -3499,7 +3635,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 82, + funcId: 86, port: port_, ); }, @@ -3529,7 +3665,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 83, + funcId: 87, port: port_, ); }, @@ -3559,7 +3695,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 84, + funcId: 88, port: port_, ); }, @@ -3589,7 +3725,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 85, + funcId: 89, port: port_, ); }, @@ -3619,7 +3755,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 86, + funcId: 90, port: port_, ); }, @@ -3658,7 +3794,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 87, + funcId: 91, port: port_, ); }, @@ -3693,7 +3829,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 88, + funcId: 92, port: port_, ); }, @@ -3732,7 +3868,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 89, + funcId: 93, port: port_, ); }, @@ -3774,7 +3910,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 90, + funcId: 94, port: port_, ); }, @@ -3805,7 +3941,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 91, + funcId: 95, port: port_, ); }, @@ -3838,7 +3974,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 92, + funcId: 96, port: port_, ); }, @@ -3871,7 +4007,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 93, + funcId: 97, port: port_, ); }, @@ -3906,7 +4042,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 94, + funcId: 98, port: port_, ); }, @@ -3939,7 +4075,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 95, + funcId: 99, port: port_, ); }, @@ -3972,7 +4108,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 96, + funcId: 100, port: port_, ); }, @@ -4005,7 +4141,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 97, + funcId: 101, port: port_, ); }, @@ -4042,7 +4178,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 98, + funcId: 102, port: port_, ); }, @@ -4072,7 +4208,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 99, + funcId: 103, port: port_, ); }, @@ -4102,7 +4238,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 100, + funcId: 104, port: port_, ); }, @@ -4132,7 +4268,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 101, + funcId: 105, port: port_, ); }, @@ -4165,7 +4301,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 102, + funcId: 106, port: port_, ); }, @@ -4196,7 +4332,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 103, + funcId: 107, port: port_, ); }, @@ -4229,7 +4365,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 104, + funcId: 108, port: port_, ); }, @@ -4275,7 +4411,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 105, + funcId: 109, port: port_, ); }, @@ -4331,7 +4467,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 106, + funcId: 110, port: port_, ); }, @@ -4379,7 +4515,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 107, + funcId: 111, port: port_, ); }, @@ -4417,7 +4553,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 108, + funcId: 112, port: port_, ); }, @@ -4452,7 +4588,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 109, + funcId: 113, port: port_, ); }, @@ -4487,7 +4623,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 110, + funcId: 114, port: port_, ); }, @@ -4520,7 +4656,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 111, + funcId: 115, port: port_, ); }, @@ -4557,7 +4693,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 112, + funcId: 116, port: port_, ); }, @@ -4588,7 +4724,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 113, + funcId: 117, port: port_, ); }, @@ -4626,7 +4762,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 114, + funcId: 118, port: port_, ); }, @@ -4661,7 +4797,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 115, + funcId: 119, port: port_, ); }, @@ -4700,7 +4836,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 116, + funcId: 120, port: port_, ); }, @@ -4733,7 +4869,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 117, + funcId: 121, port: port_, ); }, @@ -4764,7 +4900,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 118, + funcId: 122, port: port_, ); }, @@ -4799,7 +4935,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 119, + funcId: 123, port: port_, ); }, @@ -4829,7 +4965,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 120, + funcId: 124, port: port_, ); }, @@ -4862,7 +4998,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 121, + funcId: 125, port: port_, ); }, @@ -4897,7 +5033,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 122, + funcId: 126, port: port_, ); }, @@ -4930,7 +5066,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 123, + funcId: 127, port: port_, ); }, @@ -4961,7 +5097,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 124, + funcId: 128, port: port_, ); }, @@ -4998,7 +5134,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 125, + funcId: 129, port: port_, ); }, @@ -5050,7 +5186,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 126, + funcId: 130, port: port_, ); }, @@ -5103,7 +5239,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 127, + funcId: 131, port: port_, ); }, @@ -5143,7 +5279,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 128, + funcId: 132, port: port_, ); }, @@ -5176,7 +5312,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 129, + funcId: 133, port: port_, ); }, @@ -5209,7 +5345,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 130, + funcId: 134, port: port_, ); }, @@ -5246,7 +5382,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 131, + funcId: 135, port: port_, ); }, @@ -5278,7 +5414,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 132, + funcId: 136, port: port_, ); }, @@ -5311,7 +5447,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 133, + funcId: 137, port: port_, ); }, @@ -5346,7 +5482,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 134, + funcId: 138, port: port_, ); }, @@ -5378,7 +5514,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 135, + funcId: 139, port: port_, ); }, @@ -5416,7 +5552,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 136, + funcId: 140, port: port_, ); }, @@ -5449,7 +5585,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 137, + funcId: 141, port: port_, ); }, @@ -5487,7 +5623,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 138, + funcId: 142, port: port_, ); }, @@ -5524,7 +5660,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 139, + funcId: 143, port: port_, ); }, @@ -5561,7 +5697,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 140, + funcId: 144, port: port_, ); }, @@ -5599,7 +5735,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 141, + funcId: 145, port: port_, ); }, @@ -5637,7 +5773,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 142, + funcId: 146, port: port_, ); }, @@ -5670,7 +5806,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 143, + funcId: 147, port: port_, ); }, @@ -5702,7 +5838,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 144, + funcId: 148, port: port_, ); }, @@ -5737,7 +5873,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 145, + funcId: 149, port: port_, ); }, @@ -5774,7 +5910,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 146, + funcId: 150, port: port_, ); }, @@ -5807,7 +5943,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 147, + funcId: 151, port: port_, ); }, @@ -5839,7 +5975,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 148, + funcId: 152, port: port_, ); }, @@ -5874,7 +6010,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 149, + funcId: 153, port: port_, ); }, @@ -5913,7 +6049,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 150, + funcId: 154, port: port_, ); }, @@ -5950,7 +6086,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 151, + funcId: 155, port: port_, ); }, @@ -5980,7 +6116,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 152, + funcId: 156, port: port_, ); }, @@ -6012,7 +6148,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 153, + funcId: 157, port: port_, ); }, @@ -6047,7 +6183,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 154, + funcId: 158, port: port_, ); }, @@ -6079,7 +6215,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 155, + funcId: 159, port: port_, ); }, @@ -6117,7 +6253,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 156, + funcId: 160, port: port_, ); }, @@ -6156,7 +6292,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 157, + funcId: 161, port: port_, ); }, @@ -6191,7 +6327,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 158, + funcId: 162, port: port_, ); }, @@ -6236,7 +6372,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 159, + funcId: 163, port: port_, ); }, @@ -6279,7 +6415,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 160, + funcId: 164, port: port_, ); }, @@ -6313,7 +6449,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 161, + funcId: 165, port: port_, ); }, @@ -6348,7 +6484,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 162, + funcId: 166, port: port_, ); }, @@ -6383,7 +6519,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 163, + funcId: 167, port: port_, ); }, @@ -6416,7 +6552,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 164, + funcId: 168, )!; }, codec: SseCodec( @@ -6456,7 +6592,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 165, + funcId: 169, port: port_, ); }, @@ -6501,7 +6637,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 166, + funcId: 170, port: port_, ); }, @@ -6531,7 +6667,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 167, + funcId: 171, port: port_, ); }, @@ -6564,7 +6700,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 168, + funcId: 172, port: port_, ); }, @@ -6599,7 +6735,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 169, + funcId: 173, port: port_, ); }, @@ -6638,7 +6774,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 170, + funcId: 174, )!; }, codec: SseCodec( @@ -6771,6 +6907,16 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + Map dco_decode_Map_String_flame_state_None(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return Map.fromEntries( + dco_decode_list_record_string_flame_state( + raw, + ).map((e) => MapEntry(e.$1, e.$2)), + ); + } + @protected Map> dco_decode_Map_String_list_String_None( dynamic raw, @@ -6937,6 +7083,19 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return raw as double; } + @protected + FlameState dco_decode_flame_state(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 3) + throw Exception('unexpected arr length: expect 3 but see ${arr.length}'); + return FlameState( + counter: dco_decode_i_64(arr[0]), + isExpiring: dco_decode_bool(arr[1]), + isBestFriend: dco_decode_bool(arr[2]), + ); + } + @protected FlutterUserDiscovery dco_decode_flutter_user_discovery(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -7220,6 +7379,16 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return raw as Uint8List; } + @protected + List<(String, FlameState)> dco_decode_list_record_string_flame_state( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return (raw as List) + .map(dco_decode_record_string_flame_state) + .toList(); + } + @protected List<(String, List)> dco_decode_list_record_string_list_string( dynamic raw, @@ -7440,6 +7609,19 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + (String, FlameState) dco_decode_record_string_flame_state(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 2) { + throw Exception('Expected 2 elements, got ${arr.length}'); + } + return ( + dco_decode_String(arr[0]), + dco_decode_flame_state(arr[1]), + ); + } + @protected (String, List) dco_decode_record_string_list_string(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -7846,6 +8028,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return Map.fromEntries(inner.map((e) => MapEntry(e.$1, e.$2))); } + @protected + Map sse_decode_Map_String_flame_state_None( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var inner = sse_decode_list_record_string_flame_state(deserializer); + return Map.fromEntries(inner.map((e) => MapEntry(e.$1, e.$2))); + } + @protected Map> sse_decode_Map_String_list_String_None( SseDeserializer deserializer, @@ -8029,6 +8220,19 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return deserializer.buffer.getFloat64(); } + @protected + FlameState sse_decode_flame_state(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_counter = sse_decode_i_64(deserializer); + var var_isExpiring = sse_decode_bool(deserializer); + var var_isBestFriend = sse_decode_bool(deserializer); + return FlameState( + counter: var_counter, + isExpiring: var_isExpiring, + isBestFriend: var_isBestFriend, + ); + } + @protected FlutterUserDiscovery sse_decode_flutter_user_discovery( SseDeserializer deserializer, @@ -8377,6 +8581,20 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return deserializer.buffer.getUint8List(len_); } + @protected + List<(String, FlameState)> sse_decode_list_record_string_flame_state( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + var len_ = sse_decode_i_32(deserializer); + var ans_ = <(String, FlameState)>[]; + for (var idx_ = 0; idx_ < len_; ++idx_) { + ans_.add(sse_decode_record_string_flame_state(deserializer)); + } + return ans_; + } + @protected List<(String, List)> sse_decode_list_record_string_list_string( SseDeserializer deserializer, @@ -8733,6 +8951,16 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return (var_field0, var_field1); } + @protected + (String, FlameState) sse_decode_record_string_flame_state( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_field0 = sse_decode_String(deserializer); + var var_field1 = sse_decode_flame_state(deserializer); + return (var_field0, var_field1); + } + @protected (String, List) sse_decode_record_string_list_string( SseDeserializer deserializer, @@ -9246,6 +9474,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + void sse_encode_Map_String_flame_state_None( + Map self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_list_record_string_flame_state( + self.entries.map((e) => (e.key, e.value)).toList(), + serializer, + ); + } + @protected void sse_encode_Map_String_list_String_None( Map> self, @@ -9460,6 +9700,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { serializer.buffer.putFloat64(self); } + @protected + void sse_encode_flame_state(FlameState self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_64(self.counter, serializer); + sse_encode_bool(self.isExpiring, serializer); + sse_encode_bool(self.isBestFriend, serializer); + } + @protected void sse_encode_flutter_user_discovery( FlutterUserDiscovery self, @@ -9755,6 +10003,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { serializer.buffer.putUint8List(self); } + @protected + void sse_encode_list_record_string_flame_state( + List<(String, FlameState)> self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.length, serializer); + for (final item in self) { + sse_encode_record_string_flame_state(item, serializer); + } + } + @protected void sse_encode_list_record_string_list_string( List<(String, List)> self, @@ -10076,6 +10336,16 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_i_64(self.$2, serializer); } + @protected + void sse_encode_record_string_flame_state( + (String, FlameState) self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_String(self.$1, serializer); + sse_encode_flame_state(self.$2, serializer); + } + @protected void sse_encode_record_string_list_string( (String, List) self, diff --git a/lib/core/frb_generated.io.dart b/lib/core/frb_generated.io.dart index 545e4ce5..dfa57ed2 100644 --- a/lib/core/frb_generated.io.dart +++ b/lib/core/frb_generated.io.dart @@ -57,6 +57,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected Map dco_decode_Map_String_String_None(dynamic raw); + @protected + Map dco_decode_Map_String_flame_state_None(dynamic raw); + @protected Map> dco_decode_Map_String_list_String_None(dynamic raw); @@ -132,6 +135,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected double dco_decode_f_64(dynamic raw); + @protected + FlameState dco_decode_flame_state(dynamic raw); + @protected FlutterUserDiscovery dco_decode_flutter_user_discovery(dynamic raw); @@ -224,6 +230,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected Uint8List dco_decode_list_prim_u_8_strict(dynamic raw); + @protected + List<(String, FlameState)> dco_decode_list_record_string_flame_state( + dynamic raw, + ); + @protected List<(String, List)> dco_decode_list_record_string_list_string( dynamic raw, @@ -320,6 +331,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { dynamic raw, ); + @protected + (String, FlameState) dco_decode_record_string_flame_state(dynamic raw); + @protected (String, List) dco_decode_record_string_list_string(dynamic raw); @@ -424,6 +438,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + Map sse_decode_Map_String_flame_state_None( + SseDeserializer deserializer, + ); + @protected Map> sse_decode_Map_String_list_String_None( SseDeserializer deserializer, @@ -521,6 +540,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected double sse_decode_f_64(SseDeserializer deserializer); + @protected + FlameState sse_decode_flame_state(SseDeserializer deserializer); + @protected FlutterUserDiscovery sse_decode_flutter_user_discovery( SseDeserializer deserializer, @@ -633,6 +655,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer); + @protected + List<(String, FlameState)> sse_decode_list_record_string_flame_state( + SseDeserializer deserializer, + ); + @protected List<(String, List)> sse_decode_list_record_string_list_string( SseDeserializer deserializer, @@ -743,6 +770,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + (String, FlameState) sse_decode_record_string_flame_state( + SseDeserializer deserializer, + ); + @protected (String, List) sse_decode_record_string_list_string( SseDeserializer deserializer, @@ -879,6 +911,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_Map_String_flame_state_None( + Map self, + SseSerializer serializer, + ); + @protected void sse_encode_Map_String_list_String_None( Map> self, @@ -999,6 +1037,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_f_64(double self, SseSerializer serializer); + @protected + void sse_encode_flame_state(FlameState self, SseSerializer serializer); + @protected void sse_encode_flutter_user_discovery( FlutterUserDiscovery self, @@ -1143,6 +1184,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_list_record_string_flame_state( + List<(String, FlameState)> self, + SseSerializer serializer, + ); + @protected void sse_encode_list_record_string_list_string( List<(String, List)> self, @@ -1281,6 +1328,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_record_string_flame_state( + (String, FlameState) self, + SseSerializer serializer, + ); + @protected void sse_encode_record_string_list_string( (String, List) self, diff --git a/lib/core/frb_generated.web.dart b/lib/core/frb_generated.web.dart index b53dd885..35baaf77 100644 --- a/lib/core/frb_generated.web.dart +++ b/lib/core/frb_generated.web.dart @@ -59,6 +59,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected Map dco_decode_Map_String_String_None(dynamic raw); + @protected + Map dco_decode_Map_String_flame_state_None(dynamic raw); + @protected Map> dco_decode_Map_String_list_String_None(dynamic raw); @@ -134,6 +137,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected double dco_decode_f_64(dynamic raw); + @protected + FlameState dco_decode_flame_state(dynamic raw); + @protected FlutterUserDiscovery dco_decode_flutter_user_discovery(dynamic raw); @@ -226,6 +232,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected Uint8List dco_decode_list_prim_u_8_strict(dynamic raw); + @protected + List<(String, FlameState)> dco_decode_list_record_string_flame_state( + dynamic raw, + ); + @protected List<(String, List)> dco_decode_list_record_string_list_string( dynamic raw, @@ -322,6 +333,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { dynamic raw, ); + @protected + (String, FlameState) dco_decode_record_string_flame_state(dynamic raw); + @protected (String, List) dco_decode_record_string_list_string(dynamic raw); @@ -426,6 +440,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + Map sse_decode_Map_String_flame_state_None( + SseDeserializer deserializer, + ); + @protected Map> sse_decode_Map_String_list_String_None( SseDeserializer deserializer, @@ -523,6 +542,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected double sse_decode_f_64(SseDeserializer deserializer); + @protected + FlameState sse_decode_flame_state(SseDeserializer deserializer); + @protected FlutterUserDiscovery sse_decode_flutter_user_discovery( SseDeserializer deserializer, @@ -635,6 +657,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer); + @protected + List<(String, FlameState)> sse_decode_list_record_string_flame_state( + SseDeserializer deserializer, + ); + @protected List<(String, List)> sse_decode_list_record_string_list_string( SseDeserializer deserializer, @@ -745,6 +772,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + (String, FlameState) sse_decode_record_string_flame_state( + SseDeserializer deserializer, + ); + @protected (String, List) sse_decode_record_string_list_string( SseDeserializer deserializer, @@ -881,6 +913,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_Map_String_flame_state_None( + Map self, + SseSerializer serializer, + ); + @protected void sse_encode_Map_String_list_String_None( Map> self, @@ -1001,6 +1039,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_f_64(double self, SseSerializer serializer); + @protected + void sse_encode_flame_state(FlameState self, SseSerializer serializer); + @protected void sse_encode_flutter_user_discovery( FlutterUserDiscovery self, @@ -1145,6 +1186,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_list_record_string_flame_state( + List<(String, FlameState)> self, + SseSerializer serializer, + ); + @protected void sse_encode_list_record_string_list_string( List<(String, List)> self, @@ -1283,6 +1330,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_record_string_flame_state( + (String, FlameState) self, + SseSerializer serializer, + ); + @protected void sse_encode_record_string_list_string( (String, List) self, diff --git a/lib/src/database/daos/groups.dao.dart b/lib/src/database/daos/groups.dao.dart index 93ff62d6..1d147cc5 100644 --- a/lib/src/database/daos/groups.dao.dart +++ b/lib/src/database/daos/groups.dao.dart @@ -1,10 +1,11 @@ import 'package:clock/clock.dart' show clock; import 'package:drift/drift.dart'; import 'package:hashlib/random.dart'; +import 'package:twonly/core/bridge.dart' show FlameState; +import 'package:twonly/core/bridge/groups.dart' as rust_groups; import 'package:twonly/locator.dart'; import 'package:twonly/src/database/tables/groups.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/services/flame.service.dart'; import 'package:twonly/src/utils/misc.dart'; part 'groups.dao.g.dart'; @@ -236,7 +237,10 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { )..where((t) => t.groupId.equals(groupId))).getSingleOrNull(); } - Stream<({int counter, bool isExpiring})> watchFlameCounter(String groupId) { + /// Drift watches the row only to learn *when* to ask again: the counter + /// itself is derived in Rust, because it depends on the current day as much + /// as on the stored columns. + Stream watchFlameCounter(String groupId) { return (select(groups)..where( (u) => u.groupId.equals(groupId) & @@ -244,7 +248,16 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { u.lastMessageSend.isNotNull(), )) .watchSingleOrNull() - .map(getFlameCounterFromGroup); + .asyncMap((group) { + if (group == null) { + return const FlameState( + counter: 0, + isExpiring: false, + isBestFriend: false, + ); + } + return rust_groups.flameState(groupId: groupId); + }); } Future> getAllDirectChats() { diff --git a/lib/src/services/flame.service.dart b/lib/src/services/flame.service.dart deleted file mode 100644 index 5ab34537..00000000 --- a/lib/src/services/flame.service.dart +++ /dev/null @@ -1,220 +0,0 @@ -import 'package:clock/clock.dart'; -import 'package:collection/collection.dart'; -import 'package:drift/drift.dart'; -import 'package:fixnum/fixnum.dart'; -import 'package:twonly/locator.dart'; -import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; -import 'package:twonly/src/services/user.service.dart'; -import 'package:twonly/src/utils/misc.dart'; - -Future syncFlameCounters({String? forceForGroup}) async { - final groups = await twonlyDB.groupsDao.getAllGroups(); - if (groups.isEmpty) return; - final maxMessageCounter = groups.map((x) => x.totalMediaCounter).max; - final bestFriend = groups.firstWhere( - (x) => x.totalMediaCounter == maxMessageCounter, - ); - - if (userService.currentUser.myBestFriendGroupId != bestFriend.groupId) { - await UserService.update((user) { - user.myBestFriendGroupId = bestFriend.groupId; - }); - } - - for (final group in groups) { - if (group.lastFlameCounterChange == null) continue; - if (!isToday(group.lastFlameCounterChange!)) continue; - if (forceForGroup == null || group.groupId != forceForGroup) { - if (group.lastFlameSync != null) { - if (isToday(group.lastFlameSync!)) continue; - } - } - - final flameResult = getFlameCounterFromGroup(group); - - // only sync when flame counter is higher three or when they are bestFriends - if (flameResult.counter <= 2 && bestFriend.groupId != group.groupId) { - continue; - } - - await RustApi.sendEncryptedContentToGroup( - groupId: group.groupId, - content: EncryptedContent( - flameSync: EncryptedContent_FlameSync( - flameCounter: Int64(flameResult.counter), - lastFlameCounterChange: Int64( - group.lastFlameCounterChange!.millisecondsSinceEpoch, - ), - bestFriend: group.groupId == bestFriend.groupId, - forceUpdate: group.groupId == forceForGroup, - ), - ).writeToBuffer(), - onlySendIfNoReceiptsAreOpen: false, - ); - - await twonlyDB.groupsDao.updateGroup( - group.groupId, - GroupsCompanion( - lastFlameSync: Value(clock.now()), - ), - ); - } -} - -({int counter, bool isExpiring}) getFlameCounterFromGroup(Group? group) { - const zero = (counter: 0, isExpiring: false); - if (group == null) return zero; - if (group.lastMessageSend == null || - group.lastMessageReceived == null || - group.lastFlameCounterChange == null) { - return zero; - } - final now = clock.now(); - final startOfToday = DateTime(now.year, now.month, now.day); - final twoDaysAgo = startOfToday.subtract(const Duration(days: 2)); - final oneDayAgo = startOfToday.subtract(const Duration(days: 1)); - if (group.lastMessageSend!.isAfter(twoDaysAgo) && - group.lastMessageReceived!.isAfter(twoDaysAgo) || - group.lastFlameCounterChange!.isAfter(oneDayAgo)) { - // Flame is expiring when today no exchange has happened yet: - // both lastMessageSend and lastMessageReceived are before startOfToday. - final isExpiring = - group.lastMessageSend!.isBefore(oneDayAgo) || - group.lastMessageReceived!.isBefore(oneDayAgo); - return (counter: group.flameCounter, isExpiring: isExpiring); - } else { - return zero; - } -} - -Future incFlameCounter( - String groupId, - bool received, - DateTime timestamp, -) async { - final group = await twonlyDB.groupsDao.getGroup(groupId); - if (group == null) return; - - final contacts = await twonlyDB.groupsDao.getGroupContact( - group.groupId, - ); - for (final contact in contacts) { - await twonlyDB.contactsDao.updateContact( - contact.userId, - ContactsCompanion( - mediaReceivedCounter: Value( - contact.mediaReceivedCounter + (received ? 1 : 0), - ), - mediaSendCounter: Value( - contact.mediaSendCounter + (received ? 0 : 1), - ), - ), - ); - } - - final totalMediaCounter = group.totalMediaCounter + 1; - var flameCounter = group.flameCounter; - var maxFlameCounter = group.maxFlameCounter; - var maxFlameCounterFrom = group.maxFlameCounterFrom; - - if (group.lastMessageReceived != null && group.lastMessageSend != null) { - final now = clock.now(); - final startOfToday = DateTime(now.year, now.month, now.day); - final twoDaysAgo = startOfToday.subtract(const Duration(days: 2)); - if (group.lastMessageSend!.isBefore(twoDaysAgo) || - group.lastMessageReceived!.isBefore(twoDaysAgo)) { - flameCounter = 0; - } - } - - var lastMessageSend = const Value.absent(); - var lastMessageReceived = const Value.absent(); - var lastFlameCounterChange = const Value.absent(); - - final now = clock.now(); - final startOfToday = DateTime(now.year, now.month, now.day); - - if (group.lastFlameCounterChange == null || - group.lastFlameCounterChange!.isBefore(startOfToday)) { - // last flame update was yesterday. check if it can be updated. - var updateFlame = false; - if (received) { - if (group.lastMessageSend != null && - group.lastMessageSend!.isAfter(startOfToday)) { - // today a message was already send -> update flame - updateFlame = true; - } - } else if (group.lastMessageReceived != null && - group.lastMessageReceived!.isAfter(startOfToday)) { - // today a message was already received -> update flame - updateFlame = true; - } - if (updateFlame) { - flameCounter += 1; - if (group.lastFlameCounterChange == null || - group.lastFlameCounterChange!.isBefore(timestamp)) { - // only update if the timestamp is newer - lastFlameCounterChange = Value(timestamp); - } - // Overwrite max flame counter either the current is bigger or the the max flame counter is older then 4 days - if (flameCounter >= maxFlameCounter || - maxFlameCounterFrom == null || - maxFlameCounterFrom.isBefore( - clock.now().subtract(const Duration(days: 5)), - )) { - maxFlameCounter = flameCounter; - maxFlameCounterFrom = clock.now(); - } - } - } - - if (received) { - if (group.lastMessageReceived == null || - group.lastMessageReceived!.isBefore(timestamp)) { - lastMessageReceived = Value(timestamp); - } - } else { - if (group.lastMessageSend == null || - group.lastMessageSend!.isBefore(timestamp)) { - lastMessageSend = Value(timestamp); - } - } - - await twonlyDB.groupsDao.updateGroup( - groupId, - GroupsCompanion( - totalMediaCounter: Value(totalMediaCounter), - lastFlameCounterChange: lastFlameCounterChange, - lastMessageReceived: lastMessageReceived, - lastMessageSend: lastMessageSend, - flameCounter: Value(flameCounter), - maxFlameCounter: Value(maxFlameCounter), - maxFlameCounterFrom: Value(maxFlameCounterFrom), - ), - ); -} - -bool isItPossibleToRestoreFlames(Group group) { - final flameResult = getFlameCounterFromGroup(group); - return group.maxFlameCounter > 2 && - flameResult.counter < group.maxFlameCounter && - group.maxFlameCounterFrom!.isAfter( - clock.now().subtract(const Duration(days: 7)), - ); -} - -Future restoreFlames(String groupId) async { - final group = await twonlyDB.groupsDao.getGroup(groupId); - if (group == null) return; - final now = clock.now(); - await twonlyDB.groupsDao.updateGroup( - groupId, - GroupsCompanion( - flameCounter: Value(group.maxFlameCounter), - lastFlameCounterChange: Value(now), - lastMessageSend: Value(now), - lastMessageReceived: Value(now), - ), - ); -} diff --git a/lib/src/services/notifications/setup.notifications.dart b/lib/src/services/notifications/setup.notifications.dart index 0c6159da..2c4c0e1f 100644 --- a/lib/src/services/notifications/setup.notifications.dart +++ b/lib/src/services/notifications/setup.notifications.dart @@ -1,5 +1,3 @@ -// ignore_for_file: unreachable_from_main - import 'dart:async'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; diff --git a/lib/src/visual/components/flame_counter.comp.dart b/lib/src/visual/components/flame_counter.comp.dart index bc7005d5..2e5101b7 100644 --- a/lib/src/visual/components/flame_counter.comp.dart +++ b/lib/src/visual/components/flame_counter.comp.dart @@ -1,9 +1,10 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:twonly/core/bridge.dart' show FlameState; +import 'package:twonly/core/bridge/groups.dart' as rust_groups; import 'package:twonly/locator.dart'; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/services/flame.service.dart'; import 'package:twonly/src/visual/components/animate_icon.comp.dart'; class FlameCounterWidget extends StatefulWidget { @@ -28,7 +29,7 @@ class _FlameCounterWidgetState extends State { bool isBestFriend = false; bool isExpiring = false; - StreamSubscription<({int counter, bool isExpiring})>? flameCounterSub; + StreamSubscription? flameCounterSub; @override void initState() { @@ -62,31 +63,24 @@ class _FlameCounterWidgetState extends State { group = await twonlyDB.groupsDao.getGroup(groupId); } if (groupId != null && group != null) { - isBestFriend = - userService.currentUser.myBestFriendGroupId == groupId && - group.alsoBestFriend; if (widget.group != null) { - final result = getFlameCounterFromGroup(group); - if (mounted) { - setState(() { - flameCounter = result.counter; - isExpiring = result.isExpiring; - }); - } + _apply(await rust_groups.flameState(groupId: groupId)); return; } final stream = twonlyDB.groupsDao.watchFlameCounter(groupId); - flameCounterSub = stream.listen((result) { - if (mounted) { - setState(() { - flameCounter = result.counter; - isExpiring = result.isExpiring; - }); - } - }); + flameCounterSub = stream.listen(_apply); } } + void _apply(FlameState state) { + if (!mounted) return; + setState(() { + flameCounter = state.counter; + isExpiring = state.isExpiring; + isBestFriend = state.isBestFriend; + }); + } + @override Widget build(BuildContext context) { if (flameCounter < 1) return Container(); diff --git a/lib/src/visual/views/camera/share_image_contact_selection.view.dart b/lib/src/visual/views/camera/share_image_contact_selection.view.dart index d922d6eb..c6ba9347 100644 --- a/lib/src/visual/views/camera/share_image_contact_selection.view.dart +++ b/lib/src/visual/views/camera/share_image_contact_selection.view.dart @@ -3,12 +3,12 @@ import 'dart:collection'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/core/bridge/groups.dart' as rust_groups; import 'package:twonly/locator.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/tables/mediafiles.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/flame.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; @@ -56,6 +56,7 @@ class _ShareImageView extends State { final TextEditingController searchUserName = TextEditingController(); late StreamSubscription> allGroupSub; String lastQuery = ''; + int _updateGroupsGeneration = 0; @override void initState() { @@ -99,13 +100,20 @@ class _ShareImageView extends State { } Future updateGroups(List groups) async { + final generation = ++_updateGroupsGeneration; + // The counters come from Rust in one batch: a sort comparator cannot await, + // so every value has to be in hand before sorting starts. + final states = await rust_groups.flameStates( + groupIds: groups.map((g) => g.groupId).toList(), + ); + if (!mounted || generation != _updateGroupsGeneration) return; + + int flameOf(Group group) => states[group.groupId]?.counter ?? 0; + // Sort contacts by flameCounter and then by totalMediaCounter groups.sort((a, b) { // First, compare by flameCounter - - final flameComparison = getFlameCounterFromGroup( - b, - ).counter.compareTo(getFlameCounterFromGroup(a).counter); + final flameComparison = flameOf(b).compareTo(flameOf(a)); if (flameComparison != 0) { return flameComparison; // Sort by flameCounter in descending order } @@ -122,9 +130,7 @@ class _ShareImageView extends State { for (final group in groups) { if (group.pinned) continue; - if (!group.archived && - getFlameCounterFromGroup(group).counter > 0 && - bestFriends.length < 6) { + if (!group.archived && flameOf(group) > 0 && bestFriends.length < 6) { bestFriends.add(group); } else { otherUsers.add(group); diff --git a/lib/src/visual/views/contact/contact_components/restore_flame.comp.dart b/lib/src/visual/views/contact/contact_components/restore_flame.comp.dart index b83e85e1..186b0e38 100644 --- a/lib/src/visual/views/contact/contact_components/restore_flame.comp.dart +++ b/lib/src/visual/views/contact/contact_components/restore_flame.comp.dart @@ -1,18 +1,12 @@ import 'dart:async'; -import 'package:drift/drift.dart' show Value; -import 'package:fixnum/fixnum.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:twonly/core/bridge/groups.dart' as rust_groups; import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/routes.keys.dart'; -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/model/protobuf/client/generated/messages.pb.dart' - as pb; -import 'package:twonly/src/services/flame.service.dart'; import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; @@ -34,6 +28,7 @@ class RestoreFlameComp extends StatefulWidget { class _RestoreFlameCompState extends State { Group? _group; + bool _canRestore = false; late String _groupId; late StreamSubscription _groupSub; @@ -45,8 +40,15 @@ class _RestoreFlameCompState extends State { userService.currentUser.userId, ); final stream = twonlyDB.groupsDao.watchGroup(_groupId); - _groupSub = stream.listen((update) { - if (mounted) setState(() => _group = update); + _groupSub = stream.listen((update) async { + // Whether a restore is still on offer depends on today as much as on the + // row, so Rust decides it; the row change is only the cue to ask again. + final canRestore = await rust_groups.canRestoreFlames(groupId: _groupId); + if (!mounted) return; + setState(() { + _group = update; + _canRestore = canRestore; + }); }); } @@ -69,42 +71,11 @@ class _RestoreFlameCompState extends State { 'Restoring flames from ${_group!.flameCounter} to ${_group!.maxFlameCounter}', ); - await restoreFlames(_groupId); - - final addData = AdditionalMessageData( - type: AdditionalMessageData_Type.RESTORED_FLAME_COUNTER, - restoredFlameCounter: Int64(_group!.maxFlameCounter), - ); - - final message = await twonlyDB.messagesDao.insertMessage( - MessagesCompanion( - groupId: Value(_groupId), - type: Value(MessageType.restoreFlameCounter.name), - additionalMessageData: Value(addData.writeToBuffer()), - ), - ); - - if (message == null) { - Log.error('Could not insert message into database'); - return; + // Rust owns the whole restore: the counter, the chat entry, and the forced + // flame sync that brings the peer back in line. + if (!await rust_groups.restoreFlames(groupId: _groupId)) { + Log.error('Could not restore the flame counter'); } - - final encryptedContent = pb.EncryptedContent( - additionalDataMessage: pb.EncryptedContent_AdditionalDataMessage( - senderMessageId: message.messageId, - additionalMessageData: addData.writeToBuffer(), - timestamp: Int64(message.createdAt.millisecondsSinceEpoch), - type: MessageType.restoreFlameCounter.name, - ), - ); - - await syncFlameCounters(forceForGroup: _groupId); - await RustApi.sendEncryptedContentToGroup( - groupId: _groupId, - content: encryptedContent.writeToBuffer(), - messageId: message.messageId, - onlySendIfNoReceiptsAreOpen: false, - ); } @override @@ -112,7 +83,7 @@ class _RestoreFlameCompState extends State { if (!userService.currentUser.showRestoreFlame) { return const SizedBox.shrink(); } - if (_group == null || !isItPossibleToRestoreFlames(_group!)) { + if (_group == null || !_canRestore) { return Container(); } if (widget.flameOnRightSide) { diff --git a/rust/src/bridge/groups.rs b/rust/src/bridge/groups.rs index 2dd89111..1ebdea1e 100644 --- a/rust/src/bridge/groups.rs +++ b/rust/src/bridge/groups.rs @@ -3,7 +3,42 @@ * */ -use crate::{error::Result, services::groups::GroupService}; +use std::collections::HashMap; + +use crate::{ + database::app::tables::{FlameState, Group}, + error::Result, + services::groups::GroupService, +}; + +/// The flame counter for one chat, derived against today rather than read from +/// the stored column. See `Group::flame_states`. +pub async fn flame_state(group_id: String) -> Result { + let ctx = crate::context::Context::get_static()?; + let database = ctx.app_db.read().await.clone(); + Group::flame_state(&database.pool, &group_id).await +} + +/// The same derivation for several chats at once. Sorting a contact list by +/// streak needs every value up front, because a comparator cannot await. +pub async fn flame_states(group_ids: Vec) -> Result> { + let ctx = crate::context::Context::get_static()?; + let database = ctx.app_db.read().await.clone(); + Group::flame_states(&database.pool, Some(group_ids)).await +} + +/// Whether to offer the user a restore for this chat. +pub async fn can_restore_flames(group_id: String) -> Result { + let ctx = crate::context::Context::get_static()?; + let database = ctx.app_db.read().await.clone(); + Group::can_restore_flames(&database.pool, &group_id).await +} + +/// Restores the lost streak, posts the chat entry, and syncs the peer. +pub async fn restore_flames(group_id: String) -> Result { + let ctx = crate::context::Context::get_static()?; + GroupService::new(ctx).restore_flames(group_id).await +} pub async fn create_new_group(group_name: String, member_ids: Vec) -> Result { let ctx = crate::context::Context::get_static()?; diff --git a/rust/src/bridge/mod.rs b/rust/src/bridge/mod.rs index 3044c591..28ad561d 100644 --- a/rust/src/bridge/mod.rs +++ b/rust/src/bridge/mod.rs @@ -16,6 +16,7 @@ use crate::context::Context; use crate::error::Result; use flutter_rust_bridge::frb; +pub use crate::database::app::tables::FlameState; pub use crate::user_discovery::AnnouncedUser; pub use crate::user_discovery::OtherPromotion; @@ -41,6 +42,13 @@ pub struct _AnnouncedUser { pub public_id: i64, } +#[frb(mirror(FlameState))] +pub struct _FlameState { + pub counter: i64, + pub is_expiring: bool, + pub is_best_friend: bool, +} + pub(super) fn get_twonly_flutter() -> Result<&'static Context> { let ctx = Context::get_static()?; Ok(&**ctx) diff --git a/rust/src/database/app/tables/groups.rs b/rust/src/database/app/tables/groups.rs index ccd8640c..05bc850c 100644 --- a/rust/src/database/app/tables/groups.rs +++ b/rust/src/database/app/tables/groups.rs @@ -3,11 +3,12 @@ * */ -use chrono::{Local, TimeZone}; use sqlx::{Sqlite, Transaction}; +use std::collections::{HashMap, HashSet}; use crate::context::Context; use crate::error::{Result, TwonlyError}; +use crate::utils::{current_time, start_of_local_day}; use super::Contact; @@ -19,14 +20,150 @@ impl Group { pub async fn flame_sync_candidates(pool: &sqlx::Pool) -> Result> { Ok(sqlx::query_as!( FlameSyncGroup, - r#"SELECT group_id, total_media_counter, last_flame_counter_change, - last_flame_sync, flame_counter + r#"SELECT group_id, total_media_counter, last_flame_counter_change, last_flame_sync FROM groups WHERE last_flame_counter_change IS NOT NULL"# ) .fetch_all(pool) .await?) } + /// The chat the user exchanges the most media with. Derived rather than + /// stored: it is a pure function of `total_media_counter`, and persisting it + /// in the user configuration would leave Flutter's in-memory copy stale, + /// because nothing tells Dart when Rust rewrites that file. + pub async fn best_friend_group_id(pool: &sqlx::Pool) -> Result> { + Ok(sqlx::query_scalar!( + "SELECT group_id FROM groups ORDER BY total_media_counter DESC LIMIT 1" + ) + .fetch_optional(pool) + .await?) + } + + /// The flame counter as it is shown, which is not what the column holds: a + /// streak survives in the database until the next media exchange rewrites + /// it, so whether it still counts has to be decided against today. + /// + /// Passing `None` derives the state of every group; the best friend is + /// resolved once either way. + pub async fn flame_states( + pool: &sqlx::Pool, + group_ids: Option>, + ) -> Result> { + let rows = sqlx::query_as!( + FlameRow, + r#"SELECT group_id, flame_counter, also_best_friend, + last_message_send, last_message_received, last_flame_counter_change + FROM groups"# + ) + .fetch_all(pool) + .await?; + + let best_friend = Self::best_friend_group_id(pool).await?; + let start_of_today = start_of_local_day()?; + + let wanted = group_ids.map(|ids| ids.into_iter().collect::>()); + Ok(rows + .into_iter() + .filter(|row| { + wanted + .as_ref() + .is_none_or(|ids| ids.contains(&row.group_id)) + }) + .map(|row| { + let is_best_friend = row.also_best_friend != 0 + && best_friend.as_deref() == Some(row.group_id.as_str()); + let state = row.derive(start_of_today, is_best_friend); + (row.group_id, state) + }) + .collect()) + } + + pub async fn flame_state(pool: &sqlx::Pool, group_id: &str) -> Result { + Ok(Self::flame_states(pool, Some(vec![group_id.to_string()])) + .await? + .remove(group_id) + .unwrap_or_default()) + } + + /// Whether the "restore flames" offer makes sense: a streak worth restoring + /// was lost recently enough that the user still remembers having it. + pub async fn can_restore_flames(pool: &sqlx::Pool, group_id: &str) -> Result { + let Some(group) = sqlx::query!( + "SELECT max_flame_counter, max_flame_counter_from FROM groups WHERE group_id = ?", + group_id, + ) + .fetch_optional(pool) + .await? + else { + return Ok(false); + }; + + let counter = Self::flame_state(pool, group_id).await?.counter; + Ok(can_restore( + group.max_flame_counter, + group.max_flame_counter_from, + counter, + current_time().timestamp(), + )) + } + + /// Puts the lost streak back and marks today as exchanged in both + /// directions, so the restored counter survives the next derivation. + /// Returns the restored value, or `None` when there was nothing to restore. + pub async fn restore_flames( + tr: &mut Transaction<'_, Sqlite>, + group_id: &str, + ) -> Result> { + let Some(group) = sqlx::query!( + r#"SELECT flame_counter, max_flame_counter, max_flame_counter_from, + last_message_send, last_message_received, last_flame_counter_change + FROM groups WHERE group_id = ?"#, + group_id, + ) + .fetch_optional(&mut **tr) + .await? + else { + return Ok(None); + }; + + let now = current_time().timestamp(); + let state = FlameRow { + group_id: group_id.to_string(), + flame_counter: group.flame_counter, + also_best_friend: 0, + last_message_send: group.last_message_send, + last_message_received: group.last_message_received, + last_flame_counter_change: group.last_flame_counter_change, + } + .derive(start_of_local_day()?, false); + if !can_restore( + group.max_flame_counter, + group.max_flame_counter_from, + state.counter, + now, + ) { + return Ok(None); + } + + sqlx::query!( + r#"UPDATE groups SET + flame_counter = ?, + last_flame_counter_change = ?, + last_message_send = ?, + last_message_received = ? + WHERE group_id = ?"#, + group.max_flame_counter, + now, + now, + now, + group_id, + ) + .execute(&mut **tr) + .await?; + + Ok(Some(group.max_flame_counter)) + } + pub async fn set_last_flame_sync( pool: &sqlx::Pool, group_id: &str, @@ -89,16 +226,8 @@ impl Group { .await?; } - let now = Local::now(); - let start_of_today = Local - .from_local_datetime( - &now.date_naive() - .and_hms_opt(0, 0, 0) - .ok_or_else(|| TwonlyError::Generic("invalid local date".into()))?, - ) - .earliest() - .ok_or_else(|| TwonlyError::Generic("local day has no midnight".into()))? - .timestamp(); + let now = current_time(); + let start_of_today = start_of_local_day()?; let two_days_ago = start_of_today - 2 * 24 * 60 * 60; let mut flame_counter = group.flame_counter; @@ -349,7 +478,55 @@ pub struct FlameSyncGroup { pub total_media_counter: i64, pub last_flame_counter_change: Option, pub last_flame_sync: Option, - pub flame_counter: i64, +} + +/// The flame counter as the UI shows it. Everything here is derived from the +/// group row against the current local day, never read straight out of a column. +#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)] +pub struct FlameState { + pub counter: i64, + /// No exchange yet today, so the streak ends at midnight. + pub is_expiring: bool, + /// Mutual: our most-exchanged chat, and they said the same about us. + pub is_best_friend: bool, +} + +struct FlameRow { + group_id: String, + flame_counter: i64, + also_best_friend: i64, + last_message_send: Option, + last_message_received: Option, + last_flame_counter_change: Option, +} + +impl FlameRow { + fn derive(&self, start_of_today: i64, is_best_friend: bool) -> FlameState { + let expired = FlameState { + is_best_friend, + ..FlameState::default() + }; + let (Some(sent), Some(received), Some(changed)) = ( + self.last_message_send, + self.last_message_received, + self.last_flame_counter_change, + ) else { + return expired; + }; + + let two_days_ago = start_of_today - 2 * 24 * 60 * 60; + let one_day_ago = start_of_today - 24 * 60 * 60; + + if sent > two_days_ago && received > two_days_ago || changed > one_day_ago { + FlameState { + counter: self.flame_counter, + is_expiring: sent < one_day_ago || received < one_day_ago, + is_best_friend, + } + } else { + expired + } + } } #[derive(bon::Builder)] @@ -708,10 +885,230 @@ fn current_unix_timestamp() -> Result { Ok(crate::utils::current_time().timestamp()) } +/// A streak is worth offering back while it was big enough to miss and recent +/// enough to still be remembered. Pure so the boundaries stay testable without +/// a controllable clock. +fn can_restore( + max_flame_counter: i64, + max_flame_counter_from: Option, + counter: i64, + now: i64, +) -> bool { + let seven_days_ago = now - 7 * 24 * 60 * 60; + max_flame_counter > 2 + && counter < max_flame_counter + && max_flame_counter_from.is_some_and(|from| from > seven_days_ago) +} + #[cfg(test)] mod tests { use super::*; use crate::database::app::AppDatabase; + use chrono::{Local, TimeZone}; + + const DAY: i64 = 24 * 60 * 60; + + /// Local midnight of the given date, which is what the derivation compares + /// against. Mirrors the `DateTime(2026, 2, 2)` literals the Dart tests used. + fn local_midnight(year: i32, month: u32, day: u32) -> i64 { + Local + .with_ymd_and_hms(year, month, day, 0, 0, 0) + .earliest() + .unwrap() + .timestamp() + } + + fn row(counter: i64, sent: i64, received: i64, changed: i64) -> FlameRow { + FlameRow { + group_id: "group".into(), + flame_counter: counter, + also_best_friend: 0, + last_message_send: Some(sent), + last_message_received: Some(received), + last_flame_counter_change: Some(changed), + } + } + + #[test] + fn a_streak_without_any_exchange_is_zero() { + let empty = FlameRow { + group_id: "group".into(), + flame_counter: 7, + also_best_friend: 0, + last_message_send: None, + last_message_received: None, + last_flame_counter_change: None, + }; + assert_eq!(empty.derive(local_midnight(2026, 2, 3), false).counter, 0); + } + + /// The day-by-day walk the Dart `normal flame expiring` test performed: one + /// exchange on Feb 2 stays visible through Feb 4 and is gone on Feb 5. + #[test] + fn a_streak_survives_two_days_then_expires() { + let sent = local_midnight(2026, 2, 2) + 15 * 3600; + let received = local_midnight(2026, 2, 2) + 10 * 3600; + let row = row(1, sent, received, sent); + + assert_eq!(row.derive(local_midnight(2026, 2, 2), false).counter, 1); + assert_eq!(row.derive(local_midnight(2026, 2, 3), false).counter, 1); + assert_eq!(row.derive(local_midnight(2026, 2, 4), false).counter, 1); + assert_eq!(row.derive(local_midnight(2026, 2, 5), false).counter, 0); + assert_eq!(row.derive(local_midnight(2026, 3, 1), false).counter, 0); + } + + /// The `isExpiring` half of the same Dart test: an exchange from two days + /// back still counts, but warns that it ends at midnight. + #[test] + fn a_streak_warns_on_the_day_before_it_lapses() { + let exchange = local_midnight(2026, 2, 7) + 11 * 3600; + let row = row(3, exchange, exchange - 3600, exchange); + + let same_day = row.derive(local_midnight(2026, 2, 7), false); + assert_eq!((same_day.counter, same_day.is_expiring), (3, false)); + + let next_day = row.derive(local_midnight(2026, 2, 8), false); + assert_eq!((next_day.counter, next_day.is_expiring), (3, false)); + + let last_day = row.derive(local_midnight(2026, 2, 9), false); + assert_eq!((last_day.counter, last_day.is_expiring), (3, true)); + + let lapsed = row.derive(local_midnight(2026, 2, 10), false); + assert_eq!((lapsed.counter, lapsed.is_expiring), (0, false)); + } + + /// The Dart `isRestore Possible` test walked March 24 to 27 against a max + /// set on March 20; the offer stops exactly seven days after that. + #[test] + fn restoring_is_offered_for_seven_days() { + let max_from = local_midnight(2026, 3, 20) + 3 * 3600; + + for days in 4..=6 { + assert!( + can_restore(20, Some(max_from), 0, max_from + days * DAY), + "expected the offer {days} days after the streak was lost" + ); + } + assert!(!can_restore(20, Some(max_from), 0, max_from + 7 * DAY + 1)); + } + + #[test] + fn restoring_is_not_offered_without_a_streak_worth_restoring() { + let now = local_midnight(2026, 3, 24); + // Too short to miss. + assert!(!can_restore(2, Some(now - DAY), 0, now)); + // Nothing was lost. + assert!(!can_restore(20, Some(now - DAY), 20, now)); + // Never reached a maximum. + assert!(!can_restore(20, None, 0, now)); + } + + #[tokio::test] + async fn restoring_brings_the_streak_back_to_its_maximum() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("app.sqlite"); + let database = AppDatabase::new(path.to_str().unwrap(), None, false) + .await + .unwrap(); + database.run_migrations().await.unwrap(); + + let lapsed = current_time().timestamp() - 4 * DAY; + sqlx::query( + r#"INSERT INTO groups(group_id, group_name, flame_counter, max_flame_counter, + max_flame_counter_from, last_message_send, + last_message_received, last_flame_counter_change) + VALUES ('group', 'Group', 0, 5, ?, ?, ?, ?)"#, + ) + .bind(lapsed) + .bind(lapsed) + .bind(lapsed) + .bind(lapsed) + .execute(&database.pool) + .await + .unwrap(); + + assert_eq!( + Group::flame_state(&database.pool, "group") + .await + .unwrap() + .counter, + 0 + ); + assert!(Group::can_restore_flames(&database.pool, "group") + .await + .unwrap()); + + let mut transaction = database.pool.begin().await.unwrap(); + let restored = Group::restore_flames(&mut transaction, "group") + .await + .unwrap(); + transaction.commit().await.unwrap(); + assert_eq!(restored, Some(5)); + + let state = Group::flame_state(&database.pool, "group").await.unwrap(); + assert_eq!((state.counter, state.is_expiring), (5, false)); + // Nothing is left to restore once it is back. + assert!(!Group::can_restore_flames(&database.pool, "group") + .await + .unwrap()); + + // The write path enforces the same rule, so a rapid second tap cannot + // create another restoration message after the first one succeeded. + let mut transaction = database.pool.begin().await.unwrap(); + let restored_again = Group::restore_flames(&mut transaction, "group") + .await + .unwrap(); + transaction.commit().await.unwrap(); + assert_eq!(restored_again, None); + } + + /// The heart emoji needs both halves: our most-exchanged chat, and their + /// `also_best_friend` flag saying they picked us too. + #[tokio::test] + async fn the_best_friend_is_the_most_exchanged_chat_and_has_to_be_mutual() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("app.sqlite"); + let database = AppDatabase::new(path.to_str().unwrap(), None, false) + .await + .unwrap(); + database.run_migrations().await.unwrap(); + + let now = current_time().timestamp(); + for (group_id, media, mutual) in [("top", 50, 1), ("second", 10, 1), ("onesided", 5, 0)] { + sqlx::query( + r#"INSERT INTO groups(group_id, group_name, total_media_counter, also_best_friend, + flame_counter, last_message_send, last_message_received, + last_flame_counter_change) + VALUES (?, 'Group', ?, ?, 9, ?, ?, ?)"#, + ) + .bind(group_id) + .bind(media) + .bind(mutual) + .bind(now) + .bind(now) + .bind(now) + .execute(&database.pool) + .await + .unwrap(); + } + + assert_eq!( + Group::best_friend_group_id(&database.pool).await.unwrap(), + Some("top".to_string()) + ); + + let states = Group::flame_states(&database.pool, None).await.unwrap(); + assert!(states["top"].is_best_friend); + assert!(!states["second"].is_best_friend); + assert!(!states["onesided"].is_best_friend); + + // A requested subset still resolves the best friend across all groups. + let subset = Group::flame_states(&database.pool, Some(vec!["top".into()])) + .await + .unwrap(); + assert_eq!(subset.len(), 1); + assert!(subset["top"].is_best_friend); + } #[tokio::test] async fn media_exchange_updates_contact_and_flame_counters_atomically() { diff --git a/rust/src/database/app/tables/mod.rs b/rust/src/database/app/tables/mod.rs index cd0302ba..dc45dc8a 100644 --- a/rust/src/database/app/tables/mod.rs +++ b/rust/src/database/app/tables/mod.rs @@ -14,8 +14,8 @@ pub mod receipt; pub use contact::{Contact, UpdateContact}; pub use group_history::{GroupHistoryType, InsertGroupHistories}; pub use groups::{ - DeleteGroupMembers, GetGroupPublicKey, GetMissingGroupPublicKeys, GetUnjoinedGroups, Group, - InsertGroup, InsertGroupHistory, InsertGroupMember, UpdateGroup, UpdateGroupMemberState, + DeleteGroupMembers, FlameState, GetGroupPublicKey, GetMissingGroupPublicKeys, GetUnjoinedGroups, + Group, InsertGroup, InsertGroupHistory, InsertGroupMember, UpdateGroup, UpdateGroupMemberState, }; pub use key_verification::{KeyVerification, KeyVerificationType, NewKeyVerification}; pub use mediafiles::MediaFile; diff --git a/rust/src/frb_generated.rs b/rust/src/frb_generated.rs index c02bd4f3..9c1ef61c 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 = 101877534; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -2068471902; // Section: executor @@ -198,6 +198,43 @@ fn wire__crate__bridge__webxdc__cache_update_impl( }, ) } +fn wire__crate__bridge__groups__can_restore_flames_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: "can_restore_flames", + 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_group_id = ::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::groups::can_restore_flames(api_group_id).await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__bridge__webxdc__catalog_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -539,6 +576,78 @@ fn wire__crate__bridge__groups__fetch_missing_group_public_keys_impl( }, ) } +fn wire__crate__bridge__groups__flame_state_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: "flame_state", + 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_group_id = ::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::groups::flame_state(api_group_id).await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__bridge__groups__flame_states_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: "flame_states", + 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_group_ids = >::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::groups::flame_states(api_group_ids).await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_change_exclusion_for_contact_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -1084,6 +1193,42 @@ fn wire__crate__bridge__groups__remove_member_from_group_impl( }, ) } +fn wire__crate__bridge__groups__restore_flames_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: "restore_flames", + 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_group_id = ::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::groups::restore_flames(api_group_id).await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__bridge__api__rust_api_abandon_media_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -6229,6 +6374,16 @@ fn wire__crate__bridge__logging__write_log_impl( ) } +// Section: static_checks + +#[allow(clippy::unnecessary_literal_unwrap)] +const _: fn() = || { + let FlameState = None::.unwrap(); + let _: i64 = FlameState.counter; + let _: bool = FlameState.is_expiring; + let _: bool = FlameState.is_best_friend; +}; + // Section: related_funcs fn decode_DartFn_Inputs_i_64_Output_unit_AnyhowException( @@ -6338,6 +6493,14 @@ impl SseDecode for std::collections::HashMap { } } +impl SseDecode for std::collections::HashMap { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = >::sse_decode(deserializer); + return inner.into_iter().collect(); + } +} + impl SseDecode for std::collections::HashMap> { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -6449,6 +6612,20 @@ impl SseDecode for f64 { } } +impl SseDecode for crate::bridge::FlameState { + // 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_counter = ::sse_decode(deserializer); + let mut var_isExpiring = ::sse_decode(deserializer); + let mut var_isBestFriend = ::sse_decode(deserializer); + return crate::bridge::FlameState { + counter: var_counter, + is_expiring: var_isExpiring, + is_best_friend: var_isBestFriend, + }; + } +} + impl SseDecode for crate::bridge::wrapper::user_discovery::FlutterUserDiscovery { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -6813,6 +6990,20 @@ impl SseDecode for Vec { } } +impl SseDecode for Vec<(String, crate::bridge::FlameState)> { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut len_ = ::sse_decode(deserializer); + let mut ans_ = Vec::with_capacity(len_ as usize); + for idx_ in 0..len_ { + ans_.push(<(String, crate::bridge::FlameState)>::sse_decode( + deserializer, + )); + } + return ans_; + } +} + impl SseDecode for Vec<(String, Vec)> { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -7158,6 +7349,15 @@ impl SseDecode for (Vec, i64) { } } +impl SseDecode for (String, crate::bridge::FlameState) { + // 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_field0 = ::sse_decode(deserializer); + let mut var_field1 = ::sse_decode(deserializer); + return (var_field0, var_field1); + } +} + impl SseDecode for (String, Vec) { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -7631,167 +7831,171 @@ fn pde_ffi_dispatcher_primary_impl( 2 => wire__crate__bridge__groups__add_new_group_members_impl(port, ptr, rust_vec_len, data_len), 3 => wire__crate__bridge__webxdc__address_for_impl(port, ptr, rust_vec_len, data_len), 4 => wire__crate__bridge__webxdc__cache_update_impl(port, ptr, rust_vec_len, data_len), -5 => wire__crate__bridge__webxdc__catalog_impl(port, ptr, rust_vec_len, data_len), -6 => wire__crate__bridge__logging__clean_log_file_impl(port, ptr, rust_vec_len, data_len), -7 => wire__crate__bridge__logging__clear_log_file_impl(port, ptr, rust_vec_len, data_len), -8 => wire__crate__bridge__webxdc__create_instance_impl(port, ptr, rust_vec_len, data_len), -9 => wire__crate__bridge__groups__create_new_group_impl(port, ptr, rust_vec_len, data_len), -10 => wire__crate__bridge__webxdc__delete_instance_impl(port, ptr, rust_vec_len, data_len), -11 => wire__crate__bridge__groups__fetch_group_state_impl(port, ptr, rust_vec_len, data_len), -12 => wire__crate__bridge__groups__fetch_group_states_for_unjoined_groups_impl(port, ptr, rust_vec_len, data_len), -13 => wire__crate__bridge__groups__fetch_missing_group_public_keys_impl(port, ptr, rust_vec_len, data_len), -14 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_change_exclusion_for_contact_impl(port, ptr, rust_vec_len, data_len), -15 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_get_current_version_impl(port, ptr, rust_vec_len, data_len), -16 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_update_verification_state_for_user_impl(port, ptr, rust_vec_len, data_len), -17 => wire__crate__bridge__callbacks__init_flutter_callbacks_impl(port, ptr, rust_vec_len, data_len), -18 => wire__crate__bridge__initialize_twonly_flutter_impl(port, ptr, rust_vec_len, data_len), -19 => wire__crate__bridge__initialize_twonly_standalone_impl(port, ptr, rust_vec_len, data_len), -20 => wire__crate__bridge__webxdc__instance_impl(port, ptr, rust_vec_len, data_len), -21 => wire__crate__bridge__groups__leave_group_impl(port, ptr, rust_vec_len, data_len), -22 => wire__crate__bridge__logging__load_log_file_impl(port, ptr, rust_vec_len, data_len), -23 => wire__crate__bridge__groups__manage_admin_state_impl(port, ptr, rust_vec_len, data_len), -24 => wire__crate__bridge__webxdc__members_impl(port, ptr, rust_vec_len, data_len), -25 => wire__crate__bridge__webxdc__one_time_instances_impl(port, ptr, rust_vec_len, data_len), -26 => wire__crate__bridge__webxdc__prepare_bundle_impl(port, ptr, rust_vec_len, data_len), -27 => wire__crate__bridge__logging__read_last_log_lines_impl(port, ptr, rust_vec_len, data_len), -28 => wire__crate__bridge__webxdc__refresh_catalog_impl(port, ptr, rust_vec_len, data_len), -29 => wire__crate__bridge__groups__remove_member_from_group_impl(port, ptr, rust_vec_len, data_len), -30 => wire__crate__bridge__api__rust_api_abandon_media_impl(port, ptr, rust_vec_len, data_len), -31 => wire__crate__bridge__api__rust_api_add_additional_user_impl(port, ptr, rust_vec_len, data_len), -32 => wire__crate__bridge__api__rust_api_allocate_sequence_impl(port, ptr, rust_vec_len, data_len), -34 => wire__crate__bridge__api__rust_api_authentication_headers_impl(port, ptr, rust_vec_len, data_len), -36 => wire__crate__bridge__api__rust_api_change_username_impl(port, ptr, rust_vec_len, data_len), -37 => wire__crate__bridge__api__rust_api_check_for_deleted_usernames_impl(port, ptr, rust_vec_len, data_len), -38 => wire__crate__bridge__api__rust_api_check_for_passwordless_notification_impl(port, ptr, rust_vec_len, data_len), -39 => wire__crate__bridge__api__rust_api_clear_contact_request_notifications_impl(port, ptr, rust_vec_len, data_len), -40 => wire__crate__bridge__api__rust_api_clear_conversation_notifications_impl(port, ptr, rust_vec_len, data_len), -41 => wire__crate__bridge__api__rust_api_close_impl(port, ptr, rust_vec_len, data_len), -42 => wire__crate__bridge__api__rust_api_confirm_memories_upload_impl(port, ptr, rust_vec_len, data_len), -43 => wire__crate__bridge__api__rust_api_connect_impl(port, ptr, rust_vec_len, data_len), -44 => wire__crate__bridge__api__rust_api_connection_state_impl(port, ptr, rust_vec_len, data_len), -45 => wire__crate__bridge__api__rust_api_crop_media_transparent_borders_impl(port, ptr, rust_vec_len, data_len), -46 => wire__crate__bridge__api__rust_api_current_user_avatar_path_impl(port, ptr, rust_vec_len, data_len), -48 => wire__crate__bridge__api__rust_api_delete_account_impl(port, ptr, rust_vec_len, data_len), -49 => wire__crate__bridge__api__rust_api_delete_memory_impl(port, ptr, rust_vec_len, data_len), -50 => wire__crate__bridge__api__rust_api_delete_widget_media_impl(port, ptr, rust_vec_len, data_len), -51 => wire__crate__bridge__api__rust_api_disable_memories_backup_impl(port, ptr, rust_vec_len, data_len), -52 => wire__crate__bridge__api__rust_api_download_done_impl(port, ptr, rust_vec_len, data_len), -53 => wire__crate__bridge__api__rust_api_download_media_impl(port, ptr, rust_vec_len, data_len), -54 => wire__crate__bridge__api__rust_api_download_pending_media_impl(port, ptr, rust_vec_len, data_len), -55 => wire__crate__bridge__api__rust_api_ensure_avatar_png_impl(port, ptr, rust_vec_len, data_len), -56 => wire__crate__bridge__api__rust_api_establish_signal_session_impl(port, ptr, rust_vec_len, data_len), -57 => wire__crate__bridge__api__rust_api_events_impl(port, ptr, rust_vec_len, data_len), -58 => wire__crate__bridge__api__rust_api_finish_started_media_uploads_impl(port, ptr, rust_vec_len, data_len), -59 => wire__crate__bridge__api__rust_api_force_ipa_check_impl(port, ptr, rust_vec_len, data_len), -60 => wire__crate__bridge__api__rust_api_get_memories_url_impl(port, ptr, rust_vec_len, data_len), -61 => wire__crate__bridge__api__rust_api_get_memories_usage_impl(port, ptr, rust_vec_len, data_len), -62 => wire__crate__bridge__api__rust_api_get_plan_balance_impl(port, ptr, rust_vec_len, data_len), -63 => wire__crate__bridge__api__rust_api_get_proof_of_work_impl(port, ptr, rust_vec_len, data_len), -64 => wire__crate__bridge__api__rust_api_get_server_key_for_passwordless_recovery_impl(port, ptr, rust_vec_len, data_len), -65 => wire__crate__bridge__api__rust_api_get_user_by_id_impl(port, ptr, rust_vec_len, data_len), -66 => wire__crate__bridge__api__rust_api_get_user_data_impl(port, ptr, rust_vec_len, data_len), -67 => wire__crate__bridge__api__rust_api_get_user_id_from_username_impl(port, ptr, rust_vec_len, data_len), -68 => wire__crate__bridge__api__rust_api_hand_outbox_to_os_impl(port, ptr, rust_vec_len, data_len), -69 => wire__crate__bridge__api__rust_api_initialize_media_upload_impl(port, ptr, rust_vec_len, data_len), -70 => wire__crate__bridge__api__rust_api_initialize_memory_location_impl(port, ptr, rust_vec_len, data_len), -71 => wire__crate__bridge__api__rust_api_insert_and_send_additional_data_impl(port, ptr, rust_vec_len, data_len), -72 => wire__crate__bridge__api__rust_api_insert_and_send_ask_about_user_impl(port, ptr, rust_vec_len, data_len), -73 => wire__crate__bridge__api__rust_api_insert_and_send_contact_share_impl(port, ptr, rust_vec_len, data_len), -74 => wire__crate__bridge__api__rust_api_insert_and_send_text_impl(port, ptr, rust_vec_len, data_len), -75 => wire__crate__bridge__api__rust_api_ipa_purchase_impl(port, ptr, rust_vec_len, data_len), -76 => wire__crate__bridge__api__rust_api_load_plan_balance_impl(port, ptr, rust_vec_len, data_len), -77 => wire__crate__bridge__api__rust_api_media_size_limit_report_impl(port, ptr, rust_vec_len, data_len), -78 => wire__crate__bridge__api__rust_api_media_step_finished_impl(port, ptr, rust_vec_len, data_len), -79 => wire__crate__bridge__api__rust_api_notification_badge_count_impl(port, ptr, rust_vec_len, data_len), -80 => wire__crate__bridge__api__rust_api_notify_messages_opened_impl(port, ptr, rust_vec_len, data_len), -81 => wire__crate__bridge__api__rust_api_perform_passwordless_recovery_heartbeat_impl(port, ptr, rust_vec_len, data_len), -82 => wire__crate__bridge__api__rust_api_prerender_media_impl(port, ptr, rust_vec_len, data_len), -83 => wire__crate__bridge__api__rust_api_prewarm_memory_location_impl(port, ptr, rust_vec_len, data_len), -84 => wire__crate__bridge__api__rust_api_purge_media_temp_folder_impl(port, ptr, rust_vec_len, data_len), -85 => wire__crate__bridge__api__rust_api_purge_widget_media_impl(port, ptr, rust_vec_len, data_len), -86 => wire__crate__bridge__api__rust_api_refresh_widget_manifest_impl(port, ptr, rust_vec_len, data_len), -87 => wire__crate__bridge__api__rust_api_register_impl(port, ptr, rust_vec_len, data_len), -88 => wire__crate__bridge__api__rust_api_register_home_widget_impl(port, ptr, rust_vec_len, data_len), -89 => wire__crate__bridge__api__rust_api_register_passwordless_notification_impl(port, ptr, rust_vec_len, data_len), -90 => wire__crate__bridge__api__rust_api_register_passwordless_recovery_impl(port, ptr, rust_vec_len, data_len), -91 => wire__crate__bridge__api__rust_api_reload_configuration_impl(port, ptr, rust_vec_len, data_len), -92 => wire__crate__bridge__api__rust_api_remove_additional_user_impl(port, ptr, rust_vec_len, data_len), -93 => wire__crate__bridge__api__rust_api_remove_media_files_impl(port, ptr, rust_vec_len, data_len), -94 => wire__crate__bridge__api__rust_api_report_user_impl(port, ptr, rust_vec_len, data_len), -95 => wire__crate__bridge__api__rust_api_request_binary_impl(port, ptr, rust_vec_len, data_len), -96 => wire__crate__bridge__api__rust_api_request_contact_by_username_impl(port, ptr, rust_vec_len, data_len), -97 => wire__crate__bridge__api__rust_api_request_media_reupload_impl(port, ptr, rust_vec_len, data_len), -98 => wire__crate__bridge__api__rust_api_request_memories_upload_impl(port, ptr, rust_vec_len, data_len), -99 => wire__crate__bridge__api__rust_api_retransmit_all_messages_impl(port, ptr, rust_vec_len, data_len), -100 => wire__crate__bridge__api__rust_api_retry_pending_media_reuploads_impl(port, ptr, rust_vec_len, data_len), -101 => wire__crate__bridge__api__rust_api_reupload_pending_media_impl(port, ptr, rust_vec_len, data_len), -102 => wire__crate__bridge__api__rust_api_save_media_to_gallery_impl(port, ptr, rust_vec_len, data_len), -103 => wire__crate__bridge__api__rust_api_send_binary_impl(port, ptr, rust_vec_len, data_len), -104 => wire__crate__bridge__api__rust_api_send_contact_profile_impl(port, ptr, rust_vec_len, data_len), -105 => wire__crate__bridge__api__rust_api_send_encrypted_content_impl(port, ptr, rust_vec_len, data_len), -106 => wire__crate__bridge__api__rust_api_send_encrypted_content_to_group_impl(port, ptr, rust_vec_len, data_len), -107 => wire__crate__bridge__api__rust_api_send_media_to_groups_impl(port, ptr, rust_vec_len, data_len), -108 => wire__crate__bridge__api__rust_api_send_queued_message_impl(port, ptr, rust_vec_len, data_len), -109 => wire__crate__bridge__api__rust_api_send_text_message_impl(port, ptr, rust_vec_len, data_len), -110 => wire__crate__bridge__api__rust_api_send_typing_impl(port, ptr, rust_vec_len, data_len), -111 => wire__crate__bridge__api__rust_api_set_background_impl(port, ptr, rust_vec_len, data_len), -112 => wire__crate__bridge__api__rust_api_set_home_widget_groups_impl(port, ptr, rust_vec_len, data_len), -113 => wire__crate__bridge__api__rust_api_set_login_token_impl(port, ptr, rust_vec_len, data_len), -114 => wire__crate__bridge__api__rust_api_set_media_display_limit_impl(port, ptr, rust_vec_len, data_len), -115 => wire__crate__bridge__api__rust_api_set_media_requires_authentication_impl(port, ptr, rust_vec_len, data_len), -116 => wire__crate__bridge__api__rust_api_set_media_trim_impl(port, ptr, rust_vec_len, data_len), -117 => wire__crate__bridge__api__rust_api_set_network_available_impl(port, ptr, rust_vec_len, data_len), -118 => wire__crate__bridge__api__rust_api_store_media_impl(port, ptr, rust_vec_len, data_len), -119 => wire__crate__bridge__api__rust_api_submit_recovery_share_impl(port, ptr, rust_vec_len, data_len), -120 => wire__crate__bridge__api__rust_api_sync_widget_permissions_impl(port, ptr, rust_vec_len, data_len), -121 => wire__crate__bridge__api__rust_api_toggle_media_remove_audio_impl(port, ptr, rust_vec_len, data_len), -122 => wire__crate__bridge__api__rust_api_try_request_contact_by_id_impl(port, ptr, rust_vec_len, data_len), -123 => wire__crate__bridge__api__rust_api_unregister_home_widget_impl(port, ptr, rust_vec_len, data_len), -124 => wire__crate__bridge__api__rust_api_update_fcm_token_impl(port, ptr, rust_vec_len, data_len), -125 => wire__crate__bridge__api__rust_api_update_signed_pre_key_impl(port, ptr, rust_vec_len, data_len), -126 => wire__crate__bridge__api__rust_api_upload_pqc_pre_keys_impl(port, ptr, rust_vec_len, data_len), -127 => wire__crate__bridge__wrapper__app_database__rust_app_database_changes_impl(port, ptr, rust_vec_len, data_len), -128 => wire__crate__bridge__wrapper__app_database__rust_app_database_execute_impl(port, ptr, rust_vec_len, data_len), -129 => wire__crate__bridge__wrapper__app_database__rust_app_database_legacy_import_complete_impl(port, ptr, rust_vec_len, data_len), -130 => wire__crate__bridge__wrapper__app_database__rust_app_database_migrate_legacy_database_impl(port, ptr, rust_vec_len, data_len), -131 => wire__crate__bridge__wrapper__app_database__rust_app_database_select_impl(port, ptr, rust_vec_len, data_len), -132 => wire__crate__bridge__wrapper__backup__rust_backup_archive_create_backup_archive_impl(port, ptr, rust_vec_len, data_len), -133 => wire__crate__bridge__wrapper__backup__rust_backup_archive_get_backup_download_token_impl(port, ptr, rust_vec_len, data_len), -134 => wire__crate__bridge__wrapper__backup__rust_backup_archive_restore_backup_archive_impl(port, ptr, rust_vec_len, data_len), -135 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_id_impl(port, ptr, rust_vec_len, data_len), -136 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), -137 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_identity_backup_bytes_impl(port, ptr, rust_vec_len, data_len), -138 => wire__crate__bridge__wrapper__backup__rust_backup_identity_import_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), -139 => wire__crate__bridge__wrapper__backup__rust_backup_identity_restore_identity_backup_impl(port, ptr, rust_vec_len, data_len), -140 => wire__crate__bridge__wrapper__backup__rust_backup_identity_set_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), -141 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_decrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len), -142 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_encrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len), -143 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_signal_identity_impl(port, ptr, rust_vec_len, data_len), -144 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_user_id_impl(port, ptr, rust_vec_len, data_len), -145 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_serialized_impl(port, ptr, rust_vec_len, data_len), -146 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_identity_impl(port, ptr, rust_vec_len, data_len), -147 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_local_credentials_impl(port, ptr, rust_vec_len, data_len), -148 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_serialize_impl(port, ptr, rust_vec_len, data_len), -149 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_set_user_id_impl(port, ptr, rust_vec_len, data_len), -150 => wire__crate__bridge__wrapper__signal__rust_signal_decrypt_impl(port, ptr, rust_vec_len, data_len), -151 => wire__crate__bridge__wrapper__signal__rust_signal_encrypt_impl(port, ptr, rust_vec_len, data_len), -152 => wire__crate__bridge__wrapper__signal__rust_signal_generate_bundle_impl(port, ptr, rust_vec_len, data_len), -153 => wire__crate__bridge__wrapper__signal__rust_signal_generate_pqc_prekeys_impl(port, ptr, rust_vec_len, data_len), -154 => wire__crate__bridge__wrapper__signal__rust_signal_get_contact_public_key_impl(port, ptr, rust_vec_len, data_len), -155 => wire__crate__bridge__wrapper__signal__rust_signal_get_user_public_key_impl(port, ptr, rust_vec_len, data_len), -156 => wire__crate__bridge__wrapper__signal__rust_signal_process_prekey_bundle_impl(port, ptr, rust_vec_len, data_len), -157 => wire__crate__bridge__wrapper__rust_utils_generate_shares_impl(port, ptr, rust_vec_len, data_len), -158 => wire__crate__bridge__wrapper__rust_utils_recover_secret_impl(port, ptr, rust_vec_len, data_len), -159 => wire__crate__bridge__webxdc__send_update_impl(port, ptr, rust_vec_len, data_len), -160 => wire__crate__bridge__webxdc__serve_impl(port, ptr, rust_vec_len, data_len), -161 => wire__crate__bridge__groups__update_chat_deletion_time_impl(port, ptr, rust_vec_len, data_len), -162 => wire__crate__bridge__groups__update_group_name_impl(port, ptr, rust_vec_len, data_len), -163 => wire__crate__bridge__webxdc__updates_after_impl(port, ptr, rust_vec_len, data_len), -165 => wire__crate__bridge__user_config__user_config_api_create_impl(port, ptr, rust_vec_len, data_len), -166 => wire__crate__bridge__user_config__user_config_api_import_json_impl(port, ptr, rust_vec_len, data_len), -167 => wire__crate__bridge__user_config__user_config_api_load_impl(port, ptr, rust_vec_len, data_len), -168 => wire__crate__bridge__user_config__user_config_api_save_impl(port, ptr, rust_vec_len, data_len), -169 => wire__crate__bridge__user_config__user_config_api_update_impl(port, ptr, rust_vec_len, data_len), +5 => wire__crate__bridge__groups__can_restore_flames_impl(port, ptr, rust_vec_len, data_len), +6 => wire__crate__bridge__webxdc__catalog_impl(port, ptr, rust_vec_len, data_len), +7 => wire__crate__bridge__logging__clean_log_file_impl(port, ptr, rust_vec_len, data_len), +8 => wire__crate__bridge__logging__clear_log_file_impl(port, ptr, rust_vec_len, data_len), +9 => wire__crate__bridge__webxdc__create_instance_impl(port, ptr, rust_vec_len, data_len), +10 => wire__crate__bridge__groups__create_new_group_impl(port, ptr, rust_vec_len, data_len), +11 => wire__crate__bridge__webxdc__delete_instance_impl(port, ptr, rust_vec_len, data_len), +12 => wire__crate__bridge__groups__fetch_group_state_impl(port, ptr, rust_vec_len, data_len), +13 => wire__crate__bridge__groups__fetch_group_states_for_unjoined_groups_impl(port, ptr, rust_vec_len, data_len), +14 => wire__crate__bridge__groups__fetch_missing_group_public_keys_impl(port, ptr, rust_vec_len, data_len), +15 => wire__crate__bridge__groups__flame_state_impl(port, ptr, rust_vec_len, data_len), +16 => wire__crate__bridge__groups__flame_states_impl(port, ptr, rust_vec_len, data_len), +17 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_change_exclusion_for_contact_impl(port, ptr, rust_vec_len, data_len), +18 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_get_current_version_impl(port, ptr, rust_vec_len, data_len), +19 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_update_verification_state_for_user_impl(port, ptr, rust_vec_len, data_len), +20 => wire__crate__bridge__callbacks__init_flutter_callbacks_impl(port, ptr, rust_vec_len, data_len), +21 => wire__crate__bridge__initialize_twonly_flutter_impl(port, ptr, rust_vec_len, data_len), +22 => wire__crate__bridge__initialize_twonly_standalone_impl(port, ptr, rust_vec_len, data_len), +23 => wire__crate__bridge__webxdc__instance_impl(port, ptr, rust_vec_len, data_len), +24 => wire__crate__bridge__groups__leave_group_impl(port, ptr, rust_vec_len, data_len), +25 => wire__crate__bridge__logging__load_log_file_impl(port, ptr, rust_vec_len, data_len), +26 => wire__crate__bridge__groups__manage_admin_state_impl(port, ptr, rust_vec_len, data_len), +27 => wire__crate__bridge__webxdc__members_impl(port, ptr, rust_vec_len, data_len), +28 => wire__crate__bridge__webxdc__one_time_instances_impl(port, ptr, rust_vec_len, data_len), +29 => wire__crate__bridge__webxdc__prepare_bundle_impl(port, ptr, rust_vec_len, data_len), +30 => wire__crate__bridge__logging__read_last_log_lines_impl(port, ptr, rust_vec_len, data_len), +31 => wire__crate__bridge__webxdc__refresh_catalog_impl(port, ptr, rust_vec_len, data_len), +32 => wire__crate__bridge__groups__remove_member_from_group_impl(port, ptr, rust_vec_len, data_len), +33 => wire__crate__bridge__groups__restore_flames_impl(port, ptr, rust_vec_len, data_len), +34 => wire__crate__bridge__api__rust_api_abandon_media_impl(port, ptr, rust_vec_len, data_len), +35 => wire__crate__bridge__api__rust_api_add_additional_user_impl(port, ptr, rust_vec_len, data_len), +36 => wire__crate__bridge__api__rust_api_allocate_sequence_impl(port, ptr, rust_vec_len, data_len), +38 => wire__crate__bridge__api__rust_api_authentication_headers_impl(port, ptr, rust_vec_len, data_len), +40 => wire__crate__bridge__api__rust_api_change_username_impl(port, ptr, rust_vec_len, data_len), +41 => wire__crate__bridge__api__rust_api_check_for_deleted_usernames_impl(port, ptr, rust_vec_len, data_len), +42 => wire__crate__bridge__api__rust_api_check_for_passwordless_notification_impl(port, ptr, rust_vec_len, data_len), +43 => wire__crate__bridge__api__rust_api_clear_contact_request_notifications_impl(port, ptr, rust_vec_len, data_len), +44 => wire__crate__bridge__api__rust_api_clear_conversation_notifications_impl(port, ptr, rust_vec_len, data_len), +45 => wire__crate__bridge__api__rust_api_close_impl(port, ptr, rust_vec_len, data_len), +46 => wire__crate__bridge__api__rust_api_confirm_memories_upload_impl(port, ptr, rust_vec_len, data_len), +47 => wire__crate__bridge__api__rust_api_connect_impl(port, ptr, rust_vec_len, data_len), +48 => wire__crate__bridge__api__rust_api_connection_state_impl(port, ptr, rust_vec_len, data_len), +49 => wire__crate__bridge__api__rust_api_crop_media_transparent_borders_impl(port, ptr, rust_vec_len, data_len), +50 => wire__crate__bridge__api__rust_api_current_user_avatar_path_impl(port, ptr, rust_vec_len, data_len), +52 => wire__crate__bridge__api__rust_api_delete_account_impl(port, ptr, rust_vec_len, data_len), +53 => wire__crate__bridge__api__rust_api_delete_memory_impl(port, ptr, rust_vec_len, data_len), +54 => wire__crate__bridge__api__rust_api_delete_widget_media_impl(port, ptr, rust_vec_len, data_len), +55 => wire__crate__bridge__api__rust_api_disable_memories_backup_impl(port, ptr, rust_vec_len, data_len), +56 => wire__crate__bridge__api__rust_api_download_done_impl(port, ptr, rust_vec_len, data_len), +57 => wire__crate__bridge__api__rust_api_download_media_impl(port, ptr, rust_vec_len, data_len), +58 => wire__crate__bridge__api__rust_api_download_pending_media_impl(port, ptr, rust_vec_len, data_len), +59 => wire__crate__bridge__api__rust_api_ensure_avatar_png_impl(port, ptr, rust_vec_len, data_len), +60 => wire__crate__bridge__api__rust_api_establish_signal_session_impl(port, ptr, rust_vec_len, data_len), +61 => wire__crate__bridge__api__rust_api_events_impl(port, ptr, rust_vec_len, data_len), +62 => wire__crate__bridge__api__rust_api_finish_started_media_uploads_impl(port, ptr, rust_vec_len, data_len), +63 => wire__crate__bridge__api__rust_api_force_ipa_check_impl(port, ptr, rust_vec_len, data_len), +64 => wire__crate__bridge__api__rust_api_get_memories_url_impl(port, ptr, rust_vec_len, data_len), +65 => wire__crate__bridge__api__rust_api_get_memories_usage_impl(port, ptr, rust_vec_len, data_len), +66 => wire__crate__bridge__api__rust_api_get_plan_balance_impl(port, ptr, rust_vec_len, data_len), +67 => wire__crate__bridge__api__rust_api_get_proof_of_work_impl(port, ptr, rust_vec_len, data_len), +68 => wire__crate__bridge__api__rust_api_get_server_key_for_passwordless_recovery_impl(port, ptr, rust_vec_len, data_len), +69 => wire__crate__bridge__api__rust_api_get_user_by_id_impl(port, ptr, rust_vec_len, data_len), +70 => wire__crate__bridge__api__rust_api_get_user_data_impl(port, ptr, rust_vec_len, data_len), +71 => wire__crate__bridge__api__rust_api_get_user_id_from_username_impl(port, ptr, rust_vec_len, data_len), +72 => wire__crate__bridge__api__rust_api_hand_outbox_to_os_impl(port, ptr, rust_vec_len, data_len), +73 => wire__crate__bridge__api__rust_api_initialize_media_upload_impl(port, ptr, rust_vec_len, data_len), +74 => wire__crate__bridge__api__rust_api_initialize_memory_location_impl(port, ptr, rust_vec_len, data_len), +75 => wire__crate__bridge__api__rust_api_insert_and_send_additional_data_impl(port, ptr, rust_vec_len, data_len), +76 => wire__crate__bridge__api__rust_api_insert_and_send_ask_about_user_impl(port, ptr, rust_vec_len, data_len), +77 => wire__crate__bridge__api__rust_api_insert_and_send_contact_share_impl(port, ptr, rust_vec_len, data_len), +78 => wire__crate__bridge__api__rust_api_insert_and_send_text_impl(port, ptr, rust_vec_len, data_len), +79 => wire__crate__bridge__api__rust_api_ipa_purchase_impl(port, ptr, rust_vec_len, data_len), +80 => wire__crate__bridge__api__rust_api_load_plan_balance_impl(port, ptr, rust_vec_len, data_len), +81 => wire__crate__bridge__api__rust_api_media_size_limit_report_impl(port, ptr, rust_vec_len, data_len), +82 => wire__crate__bridge__api__rust_api_media_step_finished_impl(port, ptr, rust_vec_len, data_len), +83 => wire__crate__bridge__api__rust_api_notification_badge_count_impl(port, ptr, rust_vec_len, data_len), +84 => wire__crate__bridge__api__rust_api_notify_messages_opened_impl(port, ptr, rust_vec_len, data_len), +85 => wire__crate__bridge__api__rust_api_perform_passwordless_recovery_heartbeat_impl(port, ptr, rust_vec_len, data_len), +86 => wire__crate__bridge__api__rust_api_prerender_media_impl(port, ptr, rust_vec_len, data_len), +87 => wire__crate__bridge__api__rust_api_prewarm_memory_location_impl(port, ptr, rust_vec_len, data_len), +88 => wire__crate__bridge__api__rust_api_purge_media_temp_folder_impl(port, ptr, rust_vec_len, data_len), +89 => wire__crate__bridge__api__rust_api_purge_widget_media_impl(port, ptr, rust_vec_len, data_len), +90 => wire__crate__bridge__api__rust_api_refresh_widget_manifest_impl(port, ptr, rust_vec_len, data_len), +91 => wire__crate__bridge__api__rust_api_register_impl(port, ptr, rust_vec_len, data_len), +92 => wire__crate__bridge__api__rust_api_register_home_widget_impl(port, ptr, rust_vec_len, data_len), +93 => wire__crate__bridge__api__rust_api_register_passwordless_notification_impl(port, ptr, rust_vec_len, data_len), +94 => wire__crate__bridge__api__rust_api_register_passwordless_recovery_impl(port, ptr, rust_vec_len, data_len), +95 => wire__crate__bridge__api__rust_api_reload_configuration_impl(port, ptr, rust_vec_len, data_len), +96 => wire__crate__bridge__api__rust_api_remove_additional_user_impl(port, ptr, rust_vec_len, data_len), +97 => wire__crate__bridge__api__rust_api_remove_media_files_impl(port, ptr, rust_vec_len, data_len), +98 => wire__crate__bridge__api__rust_api_report_user_impl(port, ptr, rust_vec_len, data_len), +99 => wire__crate__bridge__api__rust_api_request_binary_impl(port, ptr, rust_vec_len, data_len), +100 => wire__crate__bridge__api__rust_api_request_contact_by_username_impl(port, ptr, rust_vec_len, data_len), +101 => wire__crate__bridge__api__rust_api_request_media_reupload_impl(port, ptr, rust_vec_len, data_len), +102 => wire__crate__bridge__api__rust_api_request_memories_upload_impl(port, ptr, rust_vec_len, data_len), +103 => wire__crate__bridge__api__rust_api_retransmit_all_messages_impl(port, ptr, rust_vec_len, data_len), +104 => wire__crate__bridge__api__rust_api_retry_pending_media_reuploads_impl(port, ptr, rust_vec_len, data_len), +105 => wire__crate__bridge__api__rust_api_reupload_pending_media_impl(port, ptr, rust_vec_len, data_len), +106 => wire__crate__bridge__api__rust_api_save_media_to_gallery_impl(port, ptr, rust_vec_len, data_len), +107 => wire__crate__bridge__api__rust_api_send_binary_impl(port, ptr, rust_vec_len, data_len), +108 => wire__crate__bridge__api__rust_api_send_contact_profile_impl(port, ptr, rust_vec_len, data_len), +109 => wire__crate__bridge__api__rust_api_send_encrypted_content_impl(port, ptr, rust_vec_len, data_len), +110 => wire__crate__bridge__api__rust_api_send_encrypted_content_to_group_impl(port, ptr, rust_vec_len, data_len), +111 => wire__crate__bridge__api__rust_api_send_media_to_groups_impl(port, ptr, rust_vec_len, data_len), +112 => wire__crate__bridge__api__rust_api_send_queued_message_impl(port, ptr, rust_vec_len, data_len), +113 => wire__crate__bridge__api__rust_api_send_text_message_impl(port, ptr, rust_vec_len, data_len), +114 => wire__crate__bridge__api__rust_api_send_typing_impl(port, ptr, rust_vec_len, data_len), +115 => wire__crate__bridge__api__rust_api_set_background_impl(port, ptr, rust_vec_len, data_len), +116 => wire__crate__bridge__api__rust_api_set_home_widget_groups_impl(port, ptr, rust_vec_len, data_len), +117 => wire__crate__bridge__api__rust_api_set_login_token_impl(port, ptr, rust_vec_len, data_len), +118 => wire__crate__bridge__api__rust_api_set_media_display_limit_impl(port, ptr, rust_vec_len, data_len), +119 => wire__crate__bridge__api__rust_api_set_media_requires_authentication_impl(port, ptr, rust_vec_len, data_len), +120 => wire__crate__bridge__api__rust_api_set_media_trim_impl(port, ptr, rust_vec_len, data_len), +121 => wire__crate__bridge__api__rust_api_set_network_available_impl(port, ptr, rust_vec_len, data_len), +122 => wire__crate__bridge__api__rust_api_store_media_impl(port, ptr, rust_vec_len, data_len), +123 => wire__crate__bridge__api__rust_api_submit_recovery_share_impl(port, ptr, rust_vec_len, data_len), +124 => wire__crate__bridge__api__rust_api_sync_widget_permissions_impl(port, ptr, rust_vec_len, data_len), +125 => wire__crate__bridge__api__rust_api_toggle_media_remove_audio_impl(port, ptr, rust_vec_len, data_len), +126 => wire__crate__bridge__api__rust_api_try_request_contact_by_id_impl(port, ptr, rust_vec_len, data_len), +127 => wire__crate__bridge__api__rust_api_unregister_home_widget_impl(port, ptr, rust_vec_len, data_len), +128 => wire__crate__bridge__api__rust_api_update_fcm_token_impl(port, ptr, rust_vec_len, data_len), +129 => wire__crate__bridge__api__rust_api_update_signed_pre_key_impl(port, ptr, rust_vec_len, data_len), +130 => wire__crate__bridge__api__rust_api_upload_pqc_pre_keys_impl(port, ptr, rust_vec_len, data_len), +131 => wire__crate__bridge__wrapper__app_database__rust_app_database_changes_impl(port, ptr, rust_vec_len, data_len), +132 => wire__crate__bridge__wrapper__app_database__rust_app_database_execute_impl(port, ptr, rust_vec_len, data_len), +133 => wire__crate__bridge__wrapper__app_database__rust_app_database_legacy_import_complete_impl(port, ptr, rust_vec_len, data_len), +134 => wire__crate__bridge__wrapper__app_database__rust_app_database_migrate_legacy_database_impl(port, ptr, rust_vec_len, data_len), +135 => wire__crate__bridge__wrapper__app_database__rust_app_database_select_impl(port, ptr, rust_vec_len, data_len), +136 => wire__crate__bridge__wrapper__backup__rust_backup_archive_create_backup_archive_impl(port, ptr, rust_vec_len, data_len), +137 => wire__crate__bridge__wrapper__backup__rust_backup_archive_get_backup_download_token_impl(port, ptr, rust_vec_len, data_len), +138 => wire__crate__bridge__wrapper__backup__rust_backup_archive_restore_backup_archive_impl(port, ptr, rust_vec_len, data_len), +139 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_id_impl(port, ptr, rust_vec_len, data_len), +140 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), +141 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_identity_backup_bytes_impl(port, ptr, rust_vec_len, data_len), +142 => wire__crate__bridge__wrapper__backup__rust_backup_identity_import_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), +143 => wire__crate__bridge__wrapper__backup__rust_backup_identity_restore_identity_backup_impl(port, ptr, rust_vec_len, data_len), +144 => wire__crate__bridge__wrapper__backup__rust_backup_identity_set_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), +145 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_decrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len), +146 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_encrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len), +147 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_signal_identity_impl(port, ptr, rust_vec_len, data_len), +148 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_user_id_impl(port, ptr, rust_vec_len, data_len), +149 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_serialized_impl(port, ptr, rust_vec_len, data_len), +150 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_identity_impl(port, ptr, rust_vec_len, data_len), +151 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_local_credentials_impl(port, ptr, rust_vec_len, data_len), +152 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_serialize_impl(port, ptr, rust_vec_len, data_len), +153 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_set_user_id_impl(port, ptr, rust_vec_len, data_len), +154 => wire__crate__bridge__wrapper__signal__rust_signal_decrypt_impl(port, ptr, rust_vec_len, data_len), +155 => wire__crate__bridge__wrapper__signal__rust_signal_encrypt_impl(port, ptr, rust_vec_len, data_len), +156 => wire__crate__bridge__wrapper__signal__rust_signal_generate_bundle_impl(port, ptr, rust_vec_len, data_len), +157 => wire__crate__bridge__wrapper__signal__rust_signal_generate_pqc_prekeys_impl(port, ptr, rust_vec_len, data_len), +158 => wire__crate__bridge__wrapper__signal__rust_signal_get_contact_public_key_impl(port, ptr, rust_vec_len, data_len), +159 => wire__crate__bridge__wrapper__signal__rust_signal_get_user_public_key_impl(port, ptr, rust_vec_len, data_len), +160 => wire__crate__bridge__wrapper__signal__rust_signal_process_prekey_bundle_impl(port, ptr, rust_vec_len, data_len), +161 => wire__crate__bridge__wrapper__rust_utils_generate_shares_impl(port, ptr, rust_vec_len, data_len), +162 => wire__crate__bridge__wrapper__rust_utils_recover_secret_impl(port, ptr, rust_vec_len, data_len), +163 => wire__crate__bridge__webxdc__send_update_impl(port, ptr, rust_vec_len, data_len), +164 => wire__crate__bridge__webxdc__serve_impl(port, ptr, rust_vec_len, data_len), +165 => wire__crate__bridge__groups__update_chat_deletion_time_impl(port, ptr, rust_vec_len, data_len), +166 => wire__crate__bridge__groups__update_group_name_impl(port, ptr, rust_vec_len, data_len), +167 => wire__crate__bridge__webxdc__updates_after_impl(port, ptr, rust_vec_len, data_len), +169 => wire__crate__bridge__user_config__user_config_api_create_impl(port, ptr, rust_vec_len, data_len), +170 => wire__crate__bridge__user_config__user_config_api_import_json_impl(port, ptr, rust_vec_len, data_len), +171 => wire__crate__bridge__user_config__user_config_api_load_impl(port, ptr, rust_vec_len, data_len), +172 => wire__crate__bridge__user_config__user_config_api_save_impl(port, ptr, rust_vec_len, data_len), +173 => wire__crate__bridge__user_config__user_config_api_update_impl(port, ptr, rust_vec_len, data_len), _ => unreachable!(), } } @@ -7804,17 +8008,17 @@ fn pde_ffi_dispatcher_sync_impl( ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { // Codec=Pde (Serialization + dispatch), see doc to use other codecs match func_id { - 33 => wire__crate__bridge__api__rust_api_api_base_url_impl(ptr, rust_vec_len, data_len), - 35 => wire__crate__bridge__api__rust_api_avatar_png_path_impl(ptr, rust_vec_len, data_len), - 47 => { + 37 => wire__crate__bridge__api__rust_api_api_base_url_impl(ptr, rust_vec_len, data_len), + 39 => wire__crate__bridge__api__rust_api_avatar_png_path_impl(ptr, rust_vec_len, data_len), + 51 => { wire__crate__bridge__api__rust_api_decode_avatar_svg_impl(ptr, rust_vec_len, data_len) } - 164 => wire__crate__bridge__user_config__user_config_api_clone_impl( + 168 => wire__crate__bridge__user_config__user_config_api_clone_impl( ptr, rust_vec_len, data_len, ), - 170 => wire__crate__bridge__logging__write_log_impl(ptr, rust_vec_len, data_len), + 174 => wire__crate__bridge__logging__write_log_impl(ptr, rust_vec_len, data_len), _ => unreachable!(), } } @@ -7916,6 +8120,28 @@ impl flutter_rust_bridge::IntoIntoDart { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.0.counter.into_into_dart().into_dart(), + self.0.is_expiring.into_into_dart().into_dart(), + self.0.is_best_friend.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for FrbWrapper +{ +} +impl flutter_rust_bridge::IntoIntoDart> + for crate::bridge::FlameState +{ + fn into_into_dart(self) -> FrbWrapper { + self.into() + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs impl flutter_rust_bridge::IntoDart for crate::bridge::wrapper::user_discovery::FlutterUserDiscovery { @@ -8893,6 +9119,16 @@ impl SseEncode for std::collections::HashMap { } } +impl SseEncode for std::collections::HashMap { + // 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.into_iter().collect(), + serializer, + ); + } +} + impl SseEncode for std::collections::HashMap> { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -8998,6 +9234,15 @@ impl SseEncode for f64 { } } +impl SseEncode for crate::bridge::FlameState { + // 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.counter, serializer); + ::sse_encode(self.is_expiring, serializer); + ::sse_encode(self.is_best_friend, serializer); + } +} + impl SseEncode for crate::bridge::wrapper::user_discovery::FlutterUserDiscovery { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {} @@ -9270,6 +9515,16 @@ impl SseEncode for Vec { } } +impl SseEncode for Vec<(String, crate::bridge::FlameState)> { + // 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.len() as _, serializer); + for item in self { + <(String, crate::bridge::FlameState)>::sse_encode(item, serializer); + } + } +} + impl SseEncode for Vec<(String, Vec)> { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -9552,6 +9807,14 @@ impl SseEncode for (Vec, i64) { } } +impl SseEncode for (String, crate::bridge::FlameState) { + // 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.0, serializer); + ::sse_encode(self.1, serializer); + } +} + impl SseEncode for (String, Vec) { // 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 d2e8d728..6b6a49b9 100644 --- a/rust/src/services/groups/mod.rs +++ b/rust/src/services/groups/mod.rs @@ -13,7 +13,7 @@ use crate::api::messages::incoming::messages::{ use crate::api::messages::outgoing::send_c2c_message_to_contact; use crate::api::proto::client::encrypted_content::GroupJoin; use crate::api::proto::client::{ - encrypted_appended_group_state, encrypted_content, EncryptedAppendedGroupState, + self as proto, encrypted_appended_group_state, encrypted_content, EncryptedAppendedGroupState, EncryptedContent, EncryptedGroupState, }; use crate::api::proto::http_requests::{append_group_state, AppendGroupState, NewGroupState}; @@ -26,7 +26,7 @@ use crate::database::app::tables::{ }; use crate::error::{Result, TwonlyError}; use crate::services::messages::MessageService; -use crate::utils::{current_time, new_uuid_v4}; +use crate::utils::{current_time, new_uuid_v4, start_of_local_day}; use model::GroupRecord; use prost::Message; use rand::{RngCore, SeedableRng}; @@ -150,35 +150,46 @@ impl GroupService { pub async fn on_connected(&self) -> Result<()> { self.fetch_group_states_for_unjoined_groups().await?; self.fetch_missing_group_public_keys(None, false).await?; - self.sync_flame_counters().await + self.sync_flame_counters(None).await } - async fn sync_flame_counters(&self) -> Result<()> { + /// Tells each peer where their shared streak stands. Normally at most once + /// per day per chat; `force_for_group` overrides that guard and asks the + /// peer to take our value, which is how a restore reaches the other side. + pub(crate) async fn sync_flame_counters(&self, force_for_group: Option<&str>) -> Result<()> { let db = self.ctx.app_db.read().await.clone(); let groups = Group::flame_sync_candidates(&db.pool).await?; - - let Some(best_friend) = groups.iter().max_by_key(|group| group.total_media_counter) else { + if groups.is_empty() { return Ok(()); - }; + } - let best_friend_id = best_friend.group_id.clone(); + let best_friend_id = Group::best_friend_group_id(&db.pool).await?; + let states = Group::flame_states(&db.pool, None).await?; let now = current_time().timestamp(); - let start_today = now - now.rem_euclid(86_400); + let start_today = start_of_local_day()?; for group in groups { let Some(changed) = group.last_flame_counter_change else { continue; }; + let forced = force_for_group == Some(group.group_id.as_str()); - if changed < start_today - || group + if changed < start_today { + continue; + } + if !forced + && group .last_flame_sync .is_some_and(|sync| sync >= start_today) { continue; } - if group.flame_counter <= 2 && group.group_id != best_friend_id { + let is_best_friend = best_friend_id.as_deref() == Some(group.group_id.as_str()); + // The stored column can still hold a streak that today's derivation + // has already let expire, so send what the user actually sees. + let counter = states.get(&group.group_id).map_or(0, |state| state.counter); + if counter <= 2 && !is_best_friend { continue; } @@ -187,10 +198,10 @@ impl GroupService { group.group_id.clone(), EncryptedContent { flame_sync: Some(encrypted_content::FlameSync { - flame_counter: group.flame_counter, + flame_counter: counter, last_flame_counter_change: changed * 1000, - best_friend: group.group_id == best_friend_id, - force_update: false, + best_friend: is_best_friend, + force_update: forced, }), ..Default::default() } @@ -205,6 +216,45 @@ impl GroupService { Ok(()) } + /// Puts a lost streak back, tells the chat it happened, and pushes the + /// restored value to the peer so both sides agree again. + pub(crate) async fn restore_flames(&self, group_id: String) -> Result { + let database = self.ctx.app_db.read().await.clone(); + let mut transaction = database.pool.begin().await?; + let restored = Group::restore_flames(&mut transaction, &group_id).await?; + transaction.commit().await?; + + let Some(restored) = restored else { + return Ok(false); + }; + + let data = proto::AdditionalMessageData { + r#type: proto::additional_message_data::Type::RestoredFlameCounter as i32, + link: None, + contacts: Vec::new(), + restored_flame_counter: Some(restored), + ask_about_user_id: None, + webxdc_app: None, + webxdc_update: None, + webxdc_origin: None, + webxdc_sync: None, + webxdc_sync_request: None, + } + .encode_to_vec(); + + MessageService::new(&self.ctx) + .insert_and_send_additional_data( + group_id.clone(), + "restoreFlameCounter".into(), + data, + false, + ) + .await?; + + self.sync_flame_counters(Some(&group_id)).await?; + Ok(true) + } + pub fn new(ctx: &Arc) -> Self { Self { ctx: ctx.clone() } } diff --git a/rust/src/user_config.rs b/rust/src/user_config.rs index 2d12f211..b9f3fecb 100644 --- a/rust/src/user_config.rs +++ b/rust/src/user_config.rs @@ -256,6 +256,9 @@ pub struct UserConfig { #[serde(default = "defaults::true_value")] #[frb(non_final)] pub show_restore_flame: bool, + /// Unused: the best friend is derived from `total_media_counter` whenever + /// it is needed (`Group::best_friend_group_id`). Kept so configurations + /// written by older versions still deserialize. #[frb(non_final)] pub my_best_friend_group_id: Option, #[serde(default, with = "optional_datetime")] diff --git a/rust/src/utils.rs b/rust/src/utils.rs index 5a243732..c2ca6adf 100644 --- a/rust/src/utils.rs +++ b/rust/src/utils.rs @@ -7,6 +7,8 @@ use chrono::{DateTime, Local, TimeZone}; use std::sync::Arc; use tokio::sync::{RwLock, RwLockReadGuard}; +use crate::error::{Result, TwonlyError}; + pub(crate) fn milliseconds_to_seconds(timestamp: i64) -> i64 { timestamp.div_euclid(1_000) } @@ -22,6 +24,22 @@ pub(crate) fn is_today(timestamp: i64) -> bool { .is_some_and(|date| date.date_naive() == current_time().date_naive()) } +/// Midnight of the current day in the user's timezone. Flame counters roll over +/// at the local day boundary, so every comparison against "today" has to start +/// here instead of at a UTC boundary. +pub(crate) fn start_of_local_day() -> Result { + Ok(Local + .from_local_datetime( + ¤t_time() + .date_naive() + .and_hms_opt(0, 0, 0) + .ok_or_else(|| TwonlyError::Generic("invalid local date".into()))?, + ) + .earliest() + .ok_or_else(|| TwonlyError::Generic("local day has no midnight".into()))? + .timestamp()) +} + pub(crate) fn new_uuid_v4() -> String { uuid::Uuid::new_v4().to_string() } diff --git a/test/features/flame_counter_test.dart b/test/features/flame_counter_test.dart deleted file mode 100644 index 0b5a09a9..00000000 --- a/test/features/flame_counter_test.dart +++ /dev/null @@ -1,290 +0,0 @@ -import 'package:clock/clock.dart'; -import 'package:drift/drift.dart'; -import 'package:drift/native.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mutex/mutex.dart'; -import 'package:twonly/locator.dart'; -import 'package:twonly/src/database/daos/contacts.dao.dart'; -import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/services/flame.service.dart'; -import 'package:twonly/src/services/user.service.dart'; - -import '../mocks/user_config.dart'; - -Future expectFlame(DateTime time, String groupId, int counter) async { - await withClock( - Clock.fixed(time), - () async { - final group = (await twonlyDB.groupsDao.getGroup(groupId))!; - expect( - getFlameCounterFromGroup(group).counter, - counter, - reason: StackTrace.current.toString(), - ); - }, - ); -} - -Future expectFlameExpiring( - DateTime time, - String groupId, { - required int counter, - required bool isExpiring, -}) async { - await withClock( - Clock.fixed(time), - () async { - final group = (await twonlyDB.groupsDao.getGroup(groupId))!; - final result = getFlameCounterFromGroup(group); - expect( - result.counter, - counter, - reason: 'counter mismatch — ${StackTrace.current}', - ); - expect( - result.isExpiring, - isExpiring, - reason: 'isExpiring mismatch — ${StackTrace.current}', - ); - }, - ); -} - -void main() { - final mutex = Mutex(); - var usedUserIds = 0; - - Future getAndCreateUserId() async { - return mutex.protect(() async { - final userId = usedUserIds += 1; - await twonlyDB.contactsDao.insertOnConflictUpdate( - ContactsCompanion(userId: Value(userId), username: Value('$userId')), - ); - return userId; - }); - } - - setUp(() async { - await locator.reset(); - locator - ..registerSingleton( - TwonlyDB.forTesting( - DatabaseConnection( - NativeDatabase.memory(), - // Recommended for widget tests to avoid test errors. - closeStreamsSynchronously: true, - ), - ), - ) - ..registerSingleton(UserService()); - - userService.currentUser = testUserConfig( - userId: 0x133337, - username: 'test_user', - displayName: 'Test User', - subscriptionPlan: 'Free', - currentSetupPage: null, - appVersion: 62, - ); - }); - - test('normal flame expiring', () async { - final contactId = await getAndCreateUserId(); - final contact = (await twonlyDB.contactsDao.getContactById(contactId))!; - await twonlyDB.groupsDao.createNewDirectChat( - contactId, - GroupsCompanion( - groupName: Value( - getContactDisplayName(contact), - ), - ), - ); - - final group = (await twonlyDB.groupsDao.getDirectChat(contactId))!; - - await expectFlame(DateTime(2026, 2, 3, 16), group.groupId, 0); - - await withClock( - Clock.fixed(DateTime(2026, 2, 2, 16)), - () async { - await incFlameCounter(group.groupId, true, DateTime(2026, 2, 2, 10)); - await incFlameCounter(group.groupId, false, DateTime(2026, 2, 2, 15)); - }, - ); - - await expectFlame(DateTime(2026, 2, 2, 18), group.groupId, 1); - await expectFlame(DateTime(2026, 2, 3, 16), group.groupId, 1); - await expectFlame(DateTime(2026, 2, 4, 16), group.groupId, 1); - await expectFlame(DateTime(2026, 2, 5, 16), group.groupId, 0); - - await withClock( - Clock.fixed(DateTime(2026, 2, 5, 16)), - () async { - await incFlameCounter(group.groupId, true, DateTime(2026, 2, 5, 17)); - await incFlameCounter(group.groupId, false, DateTime(2026, 2, 5, 18)); - }, - ); - - await expectFlame(DateTime(2026, 2, 5, 19), group.groupId, 1); - - await withClock( - Clock.fixed(DateTime(2026, 2, 6, 1)), - () async { - await incFlameCounter(group.groupId, true, DateTime(2026, 2, 6, 1)); - await incFlameCounter(group.groupId, false, DateTime(2026, 2, 6, 2)); - }, - ); - - await expectFlame(DateTime(2026, 2, 6, 19), group.groupId, 2); - await expectFlame(DateTime(2026, 3, 1, 19), group.groupId, 0); - - // ── isExpiring tests ────────────────────────────────────────────────────── - // After the Feb-5/Feb-6 exchanges the flame is 2. - // On Feb-7 no exchange has happened yet → isExpiring should be true. - await expectFlameExpiring( - DateTime(2026, 2, 7, 10), - group.groupId, - counter: 2, - isExpiring: false, - ); - // Once both sides exchange on Feb-7, isExpiring becomes false. - await withClock( - Clock.fixed(DateTime(2026, 2, 7, 12)), - () async { - await incFlameCounter(group.groupId, true, DateTime(2026, 2, 7, 10)); - await incFlameCounter(group.groupId, false, DateTime(2026, 2, 7, 11)); - }, - ); - await expectFlameExpiring( - DateTime(2026, 2, 7, 13), - group.groupId, - counter: 3, - isExpiring: false, - ); - // On Feb-8, before any exchange, isExpiring is true again. - await expectFlameExpiring( - DateTime(2026, 2, 8, 9), - group.groupId, - counter: 3, - isExpiring: false, - ); - // On Feb-9, last exchange was Feb-7 — still within the two-day window, - // so the flame is still alive but expiring. - await expectFlameExpiring( - DateTime(2026, 2, 9, 9), - group.groupId, - counter: 3, - isExpiring: true, - ); - // On Feb-10, last exchange (Feb-7) is now before twoDaysAgo → flame is gone. - await expectFlameExpiring( - DateTime(2026, 2, 10, 9), - group.groupId, - counter: 0, - isExpiring: false, - ); - }); - - test('isRestore Possible', () async { - final contactId = await getAndCreateUserId(); - final contact = (await twonlyDB.contactsDao.getContactById(contactId))!; - await twonlyDB.groupsDao.createNewDirectChat( - contactId, - GroupsCompanion( - groupName: Value( - getContactDisplayName(contact), - ), - ), - ); - - final group = (await twonlyDB.groupsDao.getDirectChat(contactId))!; - - for (var i = 1; i <= 20; i++) { - await withClock( - Clock.fixed(DateTime(2026, 3, i, 1)), - () async { - await incFlameCounter(group.groupId, true, DateTime(2026, 3, i, 2)); - await incFlameCounter(group.groupId, false, DateTime(2026, 3, i, 3)); - }, - ); - } - - await expectFlame(DateTime(2026, 3, 20, 19), group.groupId, 20); - await expectFlame(DateTime(2026, 3, 23, 19), group.groupId, 0); - - await withClock( - Clock.fixed(DateTime(2026, 3, 24, 19)), - () async { - final group2 = (await twonlyDB.groupsDao.getGroup(group.groupId))!; - expect(isItPossibleToRestoreFlames(group2), true); - }, - ); - await withClock( - Clock.fixed(DateTime(2026, 3, 25, 19)), - () async { - final group2 = (await twonlyDB.groupsDao.getGroup(group.groupId))!; - expect(isItPossibleToRestoreFlames(group2), true); - }, - ); - await withClock( - Clock.fixed(DateTime(2026, 3, 26, 19)), - () async { - final group2 = (await twonlyDB.groupsDao.getGroup(group.groupId))!; - expect(isItPossibleToRestoreFlames(group2), true); - }, - ); - await withClock( - Clock.fixed(DateTime(2026, 3, 27, 19)), - () async { - final group2 = (await twonlyDB.groupsDao.getGroup(group.groupId))!; - expect(isItPossibleToRestoreFlames(group2), false); - }, - ); - }); - - test('flame restoring', () async { - final contactId = await getAndCreateUserId(); - final contact = (await twonlyDB.contactsDao.getContactById(contactId))!; - await twonlyDB.groupsDao.createNewDirectChat( - contactId, - GroupsCompanion( - groupName: Value( - getContactDisplayName(contact), - ), - ), - ); - - final group = (await twonlyDB.groupsDao.getDirectChat(contactId))!; - - for (var i = 1; i <= 5; i++) { - await withClock( - Clock.fixed(DateTime(2026, 3, i, 1)), - () async { - await incFlameCounter(group.groupId, true, DateTime(2026, 3, i, 2)); - await incFlameCounter(group.groupId, false, DateTime(2026, 3, i, 3)); - }, - ); - } - - await expectFlame(DateTime(2026, 3, 5, 19), group.groupId, 5); - await expectFlame(DateTime(2026, 3, 8, 19), group.groupId, 0); - - await withClock( - Clock.fixed(DateTime(2026, 3, 9, 12)), - () async { - await restoreFlames(group.groupId); - }, - ); - - await expectFlameExpiring( - DateTime(2026, 3, 9, 13), - group.groupId, - counter: 5, - isExpiring: false, - ); - }); - - tearDown(() async { - await twonlyDB.close(); - }); -}