mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-10-09 02:54:14 +00:00
move flame service to Rust
This commit is contained in:
parent
54527fb55b
commit
512d4d05f8
20 changed files with 1620 additions and 954 deletions
|
|
@ -18,6 +18,31 @@ Future<void> initializeTwonlyFlutter({required InitConfig config}) =>
|
||||||
Future<void> initializeTwonlyStandalone({required InitConfig config}) =>
|
Future<void> initializeTwonlyStandalone({required InitConfig config}) =>
|
||||||
RustLib.instance.api.crateBridgeInitializeTwonlyStandalone(config: 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 {
|
class InitConfig {
|
||||||
final String databaseDir;
|
final String databaseDir;
|
||||||
final String dataDir;
|
final String dataDir;
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,28 @@
|
||||||
|
|
||||||
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
|
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
|
||||||
|
|
||||||
|
import '../bridge.dart';
|
||||||
import '../frb_generated.dart';
|
import '../frb_generated.dart';
|
||||||
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_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> 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<Map<String, FlameState>> flameStates({required List<String> groupIds}) =>
|
||||||
|
RustLib.instance.api.crateBridgeGroupsFlameStates(groupIds: groupIds);
|
||||||
|
|
||||||
|
/// Whether to offer the user a restore for this chat.
|
||||||
|
Future<bool> canRestoreFlames({required String groupId}) =>
|
||||||
|
RustLib.instance.api.crateBridgeGroupsCanRestoreFlames(groupId: groupId);
|
||||||
|
|
||||||
|
/// Restores the lost streak, posts the chat entry, and syncs the peer.
|
||||||
|
Future<bool> restoreFlames({required String groupId}) =>
|
||||||
|
RustLib.instance.api.crateBridgeGroupsRestoreFlames(groupId: groupId);
|
||||||
|
|
||||||
Future<bool> createNewGroup({
|
Future<bool> createNewGroup({
|
||||||
required String groupName,
|
required String groupName,
|
||||||
required Int64List memberIds,
|
required Int64List memberIds,
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -57,6 +57,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
Map<String, String> dco_decode_Map_String_String_None(dynamic raw);
|
Map<String, String> dco_decode_Map_String_String_None(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
Map<String, FlameState> dco_decode_Map_String_flame_state_None(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
Map<String, List<String>> dco_decode_Map_String_list_String_None(dynamic raw);
|
Map<String, List<String>> dco_decode_Map_String_list_String_None(dynamic raw);
|
||||||
|
|
||||||
|
|
@ -132,6 +135,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
double dco_decode_f_64(dynamic raw);
|
double dco_decode_f_64(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
FlameState dco_decode_flame_state(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
FlutterUserDiscovery dco_decode_flutter_user_discovery(dynamic raw);
|
FlutterUserDiscovery dco_decode_flutter_user_discovery(dynamic raw);
|
||||||
|
|
||||||
|
|
@ -224,6 +230,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw);
|
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
List<(String, FlameState)> dco_decode_list_record_string_flame_state(
|
||||||
|
dynamic raw,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
List<(String, List<String>)> dco_decode_list_record_string_list_string(
|
List<(String, List<String>)> dco_decode_list_record_string_list_string(
|
||||||
dynamic raw,
|
dynamic raw,
|
||||||
|
|
@ -320,6 +331,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
dynamic raw,
|
dynamic raw,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
(String, FlameState) dco_decode_record_string_flame_state(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
(String, List<String>) dco_decode_record_string_list_string(dynamic raw);
|
(String, List<String>) dco_decode_record_string_list_string(dynamic raw);
|
||||||
|
|
||||||
|
|
@ -424,6 +438,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
Map<String, FlameState> sse_decode_Map_String_flame_state_None(
|
||||||
|
SseDeserializer deserializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
Map<String, List<String>> sse_decode_Map_String_list_String_None(
|
Map<String, List<String>> sse_decode_Map_String_list_String_None(
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
|
|
@ -521,6 +540,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
double sse_decode_f_64(SseDeserializer deserializer);
|
double sse_decode_f_64(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
FlameState sse_decode_flame_state(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
FlutterUserDiscovery sse_decode_flutter_user_discovery(
|
FlutterUserDiscovery sse_decode_flutter_user_discovery(
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
|
|
@ -633,6 +655,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer);
|
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
List<(String, FlameState)> sse_decode_list_record_string_flame_state(
|
||||||
|
SseDeserializer deserializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
List<(String, List<String>)> sse_decode_list_record_string_list_string(
|
List<(String, List<String>)> sse_decode_list_record_string_list_string(
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
|
|
@ -743,6 +770,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
(String, FlameState) sse_decode_record_string_flame_state(
|
||||||
|
SseDeserializer deserializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
(String, List<String>) sse_decode_record_string_list_string(
|
(String, List<String>) sse_decode_record_string_list_string(
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
|
|
@ -879,6 +911,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_Map_String_flame_state_None(
|
||||||
|
Map<String, FlameState> self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_Map_String_list_String_None(
|
void sse_encode_Map_String_list_String_None(
|
||||||
Map<String, List<String>> self,
|
Map<String, List<String>> self,
|
||||||
|
|
@ -999,6 +1037,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_f_64(double self, SseSerializer serializer);
|
void sse_encode_f_64(double self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_flame_state(FlameState self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_flutter_user_discovery(
|
void sse_encode_flutter_user_discovery(
|
||||||
FlutterUserDiscovery self,
|
FlutterUserDiscovery self,
|
||||||
|
|
@ -1143,6 +1184,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_list_record_string_flame_state(
|
||||||
|
List<(String, FlameState)> self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_list_record_string_list_string(
|
void sse_encode_list_record_string_list_string(
|
||||||
List<(String, List<String>)> self,
|
List<(String, List<String>)> self,
|
||||||
|
|
@ -1281,6 +1328,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_record_string_flame_state(
|
||||||
|
(String, FlameState) self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_record_string_list_string(
|
void sse_encode_record_string_list_string(
|
||||||
(String, List<String>) self,
|
(String, List<String>) self,
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
Map<String, String> dco_decode_Map_String_String_None(dynamic raw);
|
Map<String, String> dco_decode_Map_String_String_None(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
Map<String, FlameState> dco_decode_Map_String_flame_state_None(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
Map<String, List<String>> dco_decode_Map_String_list_String_None(dynamic raw);
|
Map<String, List<String>> dco_decode_Map_String_list_String_None(dynamic raw);
|
||||||
|
|
||||||
|
|
@ -134,6 +137,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
double dco_decode_f_64(dynamic raw);
|
double dco_decode_f_64(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
FlameState dco_decode_flame_state(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
FlutterUserDiscovery dco_decode_flutter_user_discovery(dynamic raw);
|
FlutterUserDiscovery dco_decode_flutter_user_discovery(dynamic raw);
|
||||||
|
|
||||||
|
|
@ -226,6 +232,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw);
|
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
List<(String, FlameState)> dco_decode_list_record_string_flame_state(
|
||||||
|
dynamic raw,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
List<(String, List<String>)> dco_decode_list_record_string_list_string(
|
List<(String, List<String>)> dco_decode_list_record_string_list_string(
|
||||||
dynamic raw,
|
dynamic raw,
|
||||||
|
|
@ -322,6 +333,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
dynamic raw,
|
dynamic raw,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
(String, FlameState) dco_decode_record_string_flame_state(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
(String, List<String>) dco_decode_record_string_list_string(dynamic raw);
|
(String, List<String>) dco_decode_record_string_list_string(dynamic raw);
|
||||||
|
|
||||||
|
|
@ -426,6 +440,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
Map<String, FlameState> sse_decode_Map_String_flame_state_None(
|
||||||
|
SseDeserializer deserializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
Map<String, List<String>> sse_decode_Map_String_list_String_None(
|
Map<String, List<String>> sse_decode_Map_String_list_String_None(
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
|
|
@ -523,6 +542,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
double sse_decode_f_64(SseDeserializer deserializer);
|
double sse_decode_f_64(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
FlameState sse_decode_flame_state(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
FlutterUserDiscovery sse_decode_flutter_user_discovery(
|
FlutterUserDiscovery sse_decode_flutter_user_discovery(
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
|
|
@ -635,6 +657,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer);
|
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
List<(String, FlameState)> sse_decode_list_record_string_flame_state(
|
||||||
|
SseDeserializer deserializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
List<(String, List<String>)> sse_decode_list_record_string_list_string(
|
List<(String, List<String>)> sse_decode_list_record_string_list_string(
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
|
|
@ -745,6 +772,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
(String, FlameState) sse_decode_record_string_flame_state(
|
||||||
|
SseDeserializer deserializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
(String, List<String>) sse_decode_record_string_list_string(
|
(String, List<String>) sse_decode_record_string_list_string(
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
|
|
@ -881,6 +913,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_Map_String_flame_state_None(
|
||||||
|
Map<String, FlameState> self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_Map_String_list_String_None(
|
void sse_encode_Map_String_list_String_None(
|
||||||
Map<String, List<String>> self,
|
Map<String, List<String>> self,
|
||||||
|
|
@ -1001,6 +1039,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_f_64(double self, SseSerializer serializer);
|
void sse_encode_f_64(double self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_flame_state(FlameState self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_flutter_user_discovery(
|
void sse_encode_flutter_user_discovery(
|
||||||
FlutterUserDiscovery self,
|
FlutterUserDiscovery self,
|
||||||
|
|
@ -1145,6 +1186,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_list_record_string_flame_state(
|
||||||
|
List<(String, FlameState)> self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_list_record_string_list_string(
|
void sse_encode_list_record_string_list_string(
|
||||||
List<(String, List<String>)> self,
|
List<(String, List<String>)> self,
|
||||||
|
|
@ -1283,6 +1330,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_record_string_flame_state(
|
||||||
|
(String, FlameState) self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_record_string_list_string(
|
void sse_encode_record_string_list_string(
|
||||||
(String, List<String>) self,
|
(String, List<String>) self,
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import 'package:clock/clock.dart' show clock;
|
import 'package:clock/clock.dart' show clock;
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:hashlib/random.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/locator.dart';
|
||||||
import 'package:twonly/src/database/tables/groups.table.dart';
|
import 'package:twonly/src/database/tables/groups.table.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/services/flame.service.dart';
|
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
|
||||||
part 'groups.dao.g.dart';
|
part 'groups.dao.g.dart';
|
||||||
|
|
@ -236,7 +237,10 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
||||||
)..where((t) => t.groupId.equals(groupId))).getSingleOrNull();
|
)..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<FlameState> watchFlameCounter(String groupId) {
|
||||||
return (select(groups)..where(
|
return (select(groups)..where(
|
||||||
(u) =>
|
(u) =>
|
||||||
u.groupId.equals(groupId) &
|
u.groupId.equals(groupId) &
|
||||||
|
|
@ -244,7 +248,16 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
||||||
u.lastMessageSend.isNotNull(),
|
u.lastMessageSend.isNotNull(),
|
||||||
))
|
))
|
||||||
.watchSingleOrNull()
|
.watchSingleOrNull()
|
||||||
.map(getFlameCounterFromGroup);
|
.asyncMap((group) {
|
||||||
|
if (group == null) {
|
||||||
|
return const FlameState(
|
||||||
|
counter: 0,
|
||||||
|
isExpiring: false,
|
||||||
|
isBestFriend: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return rust_groups.flameState(groupId: groupId);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Group>> getAllDirectChats() {
|
Future<List<Group>> getAllDirectChats() {
|
||||||
|
|
|
||||||
|
|
@ -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<void> 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<void> 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<DateTime?>.absent();
|
|
||||||
var lastMessageReceived = const Value<DateTime?>.absent();
|
|
||||||
var lastFlameCounterChange = const Value<DateTime?>.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<void> 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),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// ignore_for_file: unreachable_from_main
|
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
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/locator.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.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';
|
import 'package:twonly/src/visual/components/animate_icon.comp.dart';
|
||||||
|
|
||||||
class FlameCounterWidget extends StatefulWidget {
|
class FlameCounterWidget extends StatefulWidget {
|
||||||
|
|
@ -28,7 +29,7 @@ class _FlameCounterWidgetState extends State<FlameCounterWidget> {
|
||||||
bool isBestFriend = false;
|
bool isBestFriend = false;
|
||||||
bool isExpiring = false;
|
bool isExpiring = false;
|
||||||
|
|
||||||
StreamSubscription<({int counter, bool isExpiring})>? flameCounterSub;
|
StreamSubscription<FlameState>? flameCounterSub;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -62,30 +63,23 @@ class _FlameCounterWidgetState extends State<FlameCounterWidget> {
|
||||||
group = await twonlyDB.groupsDao.getGroup(groupId);
|
group = await twonlyDB.groupsDao.getGroup(groupId);
|
||||||
}
|
}
|
||||||
if (groupId != null && group != null) {
|
if (groupId != null && group != null) {
|
||||||
isBestFriend =
|
|
||||||
userService.currentUser.myBestFriendGroupId == groupId &&
|
|
||||||
group.alsoBestFriend;
|
|
||||||
if (widget.group != null) {
|
if (widget.group != null) {
|
||||||
final result = getFlameCounterFromGroup(group);
|
_apply(await rust_groups.flameState(groupId: groupId));
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
flameCounter = result.counter;
|
|
||||||
isExpiring = result.isExpiring;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final stream = twonlyDB.groupsDao.watchFlameCounter(groupId);
|
final stream = twonlyDB.groupsDao.watchFlameCounter(groupId);
|
||||||
flameCounterSub = stream.listen((result) {
|
flameCounterSub = stream.listen(_apply);
|
||||||
if (mounted) {
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _apply(FlameState state) {
|
||||||
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
flameCounter = result.counter;
|
flameCounter = state.counter;
|
||||||
isExpiring = result.isExpiring;
|
isExpiring = state.isExpiring;
|
||||||
|
isBestFriend = state.isBestFriend;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,12 @@ import 'dart:collection';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.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/locator.dart';
|
||||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.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/data.pb.dart';
|
||||||
import 'package:twonly/src/services/flame.service.dart';
|
|
||||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
||||||
|
|
@ -56,6 +56,7 @@ class _ShareImageView extends State<ShareImageView> {
|
||||||
final TextEditingController searchUserName = TextEditingController();
|
final TextEditingController searchUserName = TextEditingController();
|
||||||
late StreamSubscription<List<Group>> allGroupSub;
|
late StreamSubscription<List<Group>> allGroupSub;
|
||||||
String lastQuery = '';
|
String lastQuery = '';
|
||||||
|
int _updateGroupsGeneration = 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -99,13 +100,20 @@ class _ShareImageView extends State<ShareImageView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateGroups(List<Group> groups) async {
|
Future<void> updateGroups(List<Group> 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
|
// Sort contacts by flameCounter and then by totalMediaCounter
|
||||||
groups.sort((a, b) {
|
groups.sort((a, b) {
|
||||||
// First, compare by flameCounter
|
// First, compare by flameCounter
|
||||||
|
final flameComparison = flameOf(b).compareTo(flameOf(a));
|
||||||
final flameComparison = getFlameCounterFromGroup(
|
|
||||||
b,
|
|
||||||
).counter.compareTo(getFlameCounterFromGroup(a).counter);
|
|
||||||
if (flameComparison != 0) {
|
if (flameComparison != 0) {
|
||||||
return flameComparison; // Sort by flameCounter in descending order
|
return flameComparison; // Sort by flameCounter in descending order
|
||||||
}
|
}
|
||||||
|
|
@ -122,9 +130,7 @@ class _ShareImageView extends State<ShareImageView> {
|
||||||
|
|
||||||
for (final group in groups) {
|
for (final group in groups) {
|
||||||
if (group.pinned) continue;
|
if (group.pinned) continue;
|
||||||
if (!group.archived &&
|
if (!group.archived && flameOf(group) > 0 && bestFriends.length < 6) {
|
||||||
getFlameCounterFromGroup(group).counter > 0 &&
|
|
||||||
bestFriends.length < 6) {
|
|
||||||
bestFriends.add(group);
|
bestFriends.add(group);
|
||||||
} else {
|
} else {
|
||||||
otherUsers.add(group);
|
otherUsers.add(group);
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,12 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:drift/drift.dart' show Value;
|
|
||||||
import 'package:fixnum/fixnum.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.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/locator.dart';
|
||||||
import 'package:twonly/src/constants/routes.keys.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/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/services/subscription.service.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
|
@ -34,6 +28,7 @@ class RestoreFlameComp extends StatefulWidget {
|
||||||
|
|
||||||
class _RestoreFlameCompState extends State<RestoreFlameComp> {
|
class _RestoreFlameCompState extends State<RestoreFlameComp> {
|
||||||
Group? _group;
|
Group? _group;
|
||||||
|
bool _canRestore = false;
|
||||||
late String _groupId;
|
late String _groupId;
|
||||||
late StreamSubscription<Group?> _groupSub;
|
late StreamSubscription<Group?> _groupSub;
|
||||||
|
|
||||||
|
|
@ -45,8 +40,15 @@ class _RestoreFlameCompState extends State<RestoreFlameComp> {
|
||||||
userService.currentUser.userId,
|
userService.currentUser.userId,
|
||||||
);
|
);
|
||||||
final stream = twonlyDB.groupsDao.watchGroup(_groupId);
|
final stream = twonlyDB.groupsDao.watchGroup(_groupId);
|
||||||
_groupSub = stream.listen((update) {
|
_groupSub = stream.listen((update) async {
|
||||||
if (mounted) setState(() => _group = update);
|
// 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<RestoreFlameComp> {
|
||||||
'Restoring flames from ${_group!.flameCounter} to ${_group!.maxFlameCounter}',
|
'Restoring flames from ${_group!.flameCounter} to ${_group!.maxFlameCounter}',
|
||||||
);
|
);
|
||||||
|
|
||||||
await restoreFlames(_groupId);
|
// Rust owns the whole restore: the counter, the chat entry, and the forced
|
||||||
|
// flame sync that brings the peer back in line.
|
||||||
final addData = AdditionalMessageData(
|
if (!await rust_groups.restoreFlames(groupId: _groupId)) {
|
||||||
type: AdditionalMessageData_Type.RESTORED_FLAME_COUNTER,
|
Log.error('Could not restore the 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
@override
|
||||||
|
|
@ -112,7 +83,7 @@ class _RestoreFlameCompState extends State<RestoreFlameComp> {
|
||||||
if (!userService.currentUser.showRestoreFlame) {
|
if (!userService.currentUser.showRestoreFlame) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
if (_group == null || !isItPossibleToRestoreFlames(_group!)) {
|
if (_group == null || !_canRestore) {
|
||||||
return Container();
|
return Container();
|
||||||
}
|
}
|
||||||
if (widget.flameOnRightSide) {
|
if (widget.flameOnRightSide) {
|
||||||
|
|
|
||||||
|
|
@ -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<FlameState> {
|
||||||
|
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<String>) -> Result<HashMap<String, FlameState>> {
|
||||||
|
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<bool> {
|
||||||
|
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<bool> {
|
||||||
|
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<i64>) -> Result<bool> {
|
pub async fn create_new_group(group_name: String, member_ids: Vec<i64>) -> Result<bool> {
|
||||||
let ctx = crate::context::Context::get_static()?;
|
let ctx = crate::context::Context::get_static()?;
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ use crate::context::Context;
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use flutter_rust_bridge::frb;
|
use flutter_rust_bridge::frb;
|
||||||
|
|
||||||
|
pub use crate::database::app::tables::FlameState;
|
||||||
pub use crate::user_discovery::AnnouncedUser;
|
pub use crate::user_discovery::AnnouncedUser;
|
||||||
pub use crate::user_discovery::OtherPromotion;
|
pub use crate::user_discovery::OtherPromotion;
|
||||||
|
|
||||||
|
|
@ -41,6 +42,13 @@ pub struct _AnnouncedUser {
|
||||||
pub public_id: i64,
|
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> {
|
pub(super) fn get_twonly_flutter() -> Result<&'static Context> {
|
||||||
let ctx = Context::get_static()?;
|
let ctx = Context::get_static()?;
|
||||||
Ok(&**ctx)
|
Ok(&**ctx)
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,12 @@
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use chrono::{Local, TimeZone};
|
|
||||||
use sqlx::{Sqlite, Transaction};
|
use sqlx::{Sqlite, Transaction};
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::error::{Result, TwonlyError};
|
use crate::error::{Result, TwonlyError};
|
||||||
|
use crate::utils::{current_time, start_of_local_day};
|
||||||
|
|
||||||
use super::Contact;
|
use super::Contact;
|
||||||
|
|
||||||
|
|
@ -19,14 +20,150 @@ impl Group {
|
||||||
pub async fn flame_sync_candidates(pool: &sqlx::Pool<Sqlite>) -> Result<Vec<FlameSyncGroup>> {
|
pub async fn flame_sync_candidates(pool: &sqlx::Pool<Sqlite>) -> Result<Vec<FlameSyncGroup>> {
|
||||||
Ok(sqlx::query_as!(
|
Ok(sqlx::query_as!(
|
||||||
FlameSyncGroup,
|
FlameSyncGroup,
|
||||||
r#"SELECT group_id, total_media_counter, last_flame_counter_change,
|
r#"SELECT group_id, total_media_counter, last_flame_counter_change, last_flame_sync
|
||||||
last_flame_sync, flame_counter
|
|
||||||
FROM groups WHERE last_flame_counter_change IS NOT NULL"#
|
FROM groups WHERE last_flame_counter_change IS NOT NULL"#
|
||||||
)
|
)
|
||||||
.fetch_all(pool)
|
.fetch_all(pool)
|
||||||
.await?)
|
.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<Sqlite>) -> Result<Option<String>> {
|
||||||
|
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<Sqlite>,
|
||||||
|
group_ids: Option<Vec<String>>,
|
||||||
|
) -> Result<HashMap<String, FlameState>> {
|
||||||
|
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::<HashSet<_>>());
|
||||||
|
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<Sqlite>, group_id: &str) -> Result<FlameState> {
|
||||||
|
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<Sqlite>, group_id: &str) -> Result<bool> {
|
||||||
|
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<Option<i64>> {
|
||||||
|
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(
|
pub async fn set_last_flame_sync(
|
||||||
pool: &sqlx::Pool<Sqlite>,
|
pool: &sqlx::Pool<Sqlite>,
|
||||||
group_id: &str,
|
group_id: &str,
|
||||||
|
|
@ -89,16 +226,8 @@ impl Group {
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let now = Local::now();
|
let now = current_time();
|
||||||
let start_of_today = Local
|
let start_of_today = start_of_local_day()?;
|
||||||
.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 two_days_ago = start_of_today - 2 * 24 * 60 * 60;
|
let two_days_ago = start_of_today - 2 * 24 * 60 * 60;
|
||||||
|
|
||||||
let mut flame_counter = group.flame_counter;
|
let mut flame_counter = group.flame_counter;
|
||||||
|
|
@ -349,7 +478,55 @@ pub struct FlameSyncGroup {
|
||||||
pub total_media_counter: i64,
|
pub total_media_counter: i64,
|
||||||
pub last_flame_counter_change: Option<i64>,
|
pub last_flame_counter_change: Option<i64>,
|
||||||
pub last_flame_sync: Option<i64>,
|
pub last_flame_sync: Option<i64>,
|
||||||
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<i64>,
|
||||||
|
last_message_received: Option<i64>,
|
||||||
|
last_flame_counter_change: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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)]
|
#[derive(bon::Builder)]
|
||||||
|
|
@ -708,10 +885,230 @@ fn current_unix_timestamp() -> Result<i64> {
|
||||||
Ok(crate::utils::current_time().timestamp())
|
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<i64>,
|
||||||
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::database::app::AppDatabase;
|
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]
|
#[tokio::test]
|
||||||
async fn media_exchange_updates_contact_and_flame_counters_atomically() {
|
async fn media_exchange_updates_contact_and_flame_counters_atomically() {
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@ pub mod receipt;
|
||||||
pub use contact::{Contact, UpdateContact};
|
pub use contact::{Contact, UpdateContact};
|
||||||
pub use group_history::{GroupHistoryType, InsertGroupHistories};
|
pub use group_history::{GroupHistoryType, InsertGroupHistories};
|
||||||
pub use groups::{
|
pub use groups::{
|
||||||
DeleteGroupMembers, GetGroupPublicKey, GetMissingGroupPublicKeys, GetUnjoinedGroups, Group,
|
DeleteGroupMembers, FlameState, GetGroupPublicKey, GetMissingGroupPublicKeys, GetUnjoinedGroups,
|
||||||
InsertGroup, InsertGroupHistory, InsertGroupMember, UpdateGroup, UpdateGroupMemberState,
|
Group, InsertGroup, InsertGroupHistory, InsertGroupMember, UpdateGroup, UpdateGroupMemberState,
|
||||||
};
|
};
|
||||||
pub use key_verification::{KeyVerification, KeyVerificationType, NewKeyVerification};
|
pub use key_verification::{KeyVerification, KeyVerificationType, NewKeyVerification};
|
||||||
pub use mediafiles::MediaFile;
|
pub use mediafiles::MediaFile;
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
|
||||||
default_rust_auto_opaque = RustAutoOpaqueMoi,
|
default_rust_auto_opaque = RustAutoOpaqueMoi,
|
||||||
);
|
);
|
||||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0";
|
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
|
// 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::SseCodec, _, _, _>(
|
||||||
|
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 = <String>::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(
|
fn wire__crate__bridge__webxdc__catalog_impl(
|
||||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
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::SseCodec, _, _, _>(
|
||||||
|
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 = <String>::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::SseCodec, _, _, _>(
|
||||||
|
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 = <Vec<String>>::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(
|
fn wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_change_exclusion_for_contact_impl(
|
||||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
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::SseCodec, _, _, _>(
|
||||||
|
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 = <String>::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(
|
fn wire__crate__bridge__api__rust_api_abandon_media_impl(
|
||||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
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::<crate::bridge::FlameState>.unwrap();
|
||||||
|
let _: i64 = FlameState.counter;
|
||||||
|
let _: bool = FlameState.is_expiring;
|
||||||
|
let _: bool = FlameState.is_best_friend;
|
||||||
|
};
|
||||||
|
|
||||||
// Section: related_funcs
|
// Section: related_funcs
|
||||||
|
|
||||||
fn decode_DartFn_Inputs_i_64_Output_unit_AnyhowException(
|
fn decode_DartFn_Inputs_i_64_Output_unit_AnyhowException(
|
||||||
|
|
@ -6338,6 +6493,14 @@ impl SseDecode for std::collections::HashMap<String, String> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SseDecode for std::collections::HashMap<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 inner = <Vec<(String, crate::bridge::FlameState)>>::sse_decode(deserializer);
|
||||||
|
return inner.into_iter().collect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl SseDecode for std::collections::HashMap<String, Vec<String>> {
|
impl SseDecode for std::collections::HashMap<String, Vec<String>> {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
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 = <i64>::sse_decode(deserializer);
|
||||||
|
let mut var_isExpiring = <bool>::sse_decode(deserializer);
|
||||||
|
let mut var_isBestFriend = <bool>::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 {
|
impl SseDecode for crate::bridge::wrapper::user_discovery::FlutterUserDiscovery {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||||
|
|
@ -6813,6 +6990,20 @@ impl SseDecode for Vec<u8> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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_ = <i32>::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<String>)> {
|
impl SseDecode for Vec<(String, Vec<String>)> {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||||
|
|
@ -7158,6 +7349,15 @@ impl SseDecode for (Vec<u8>, 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 = <String>::sse_decode(deserializer);
|
||||||
|
let mut var_field1 = <crate::bridge::FlameState>::sse_decode(deserializer);
|
||||||
|
return (var_field0, var_field1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl SseDecode for (String, Vec<String>) {
|
impl SseDecode for (String, Vec<String>) {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
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),
|
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),
|
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),
|
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),
|
5 => wire__crate__bridge__groups__can_restore_flames_impl(port, ptr, rust_vec_len, data_len),
|
||||||
6 => wire__crate__bridge__logging__clean_log_file_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__clear_log_file_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__webxdc__create_instance_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__groups__create_new_group_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__webxdc__delete_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__groups__fetch_group_state_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_states_for_unjoined_groups_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_missing_group_public_keys_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__wrapper__user_discovery__flutter_user_discovery_change_exclusion_for_contact_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__wrapper__user_discovery__flutter_user_discovery_get_current_version_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__wrapper__user_discovery__flutter_user_discovery_update_verification_state_for_user_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__callbacks__init_flutter_callbacks_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__initialize_twonly_flutter_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__initialize_twonly_standalone_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__webxdc__instance_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__groups__leave_group_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__logging__load_log_file_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__groups__manage_admin_state_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__webxdc__members_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__webxdc__one_time_instances_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__webxdc__prepare_bundle_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__logging__read_last_log_lines_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__refresh_catalog_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__groups__remove_member_from_group_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__api__rust_api_abandon_media_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__api__rust_api_add_additional_user_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__api__rust_api_allocate_sequence_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),
|
||||||
34 => wire__crate__bridge__api__rust_api_authentication_headers_impl(port, ptr, rust_vec_len, data_len),
|
33 => wire__crate__bridge__groups__restore_flames_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),
|
34 => wire__crate__bridge__api__rust_api_abandon_media_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),
|
35 => wire__crate__bridge__api__rust_api_add_additional_user_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),
|
36 => wire__crate__bridge__api__rust_api_allocate_sequence_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),
|
38 => wire__crate__bridge__api__rust_api_authentication_headers_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),
|
40 => wire__crate__bridge__api__rust_api_change_username_impl(port, ptr, rust_vec_len, data_len),
|
||||||
41 => wire__crate__bridge__api__rust_api_close_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_confirm_memories_upload_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_connect_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_connection_state_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_crop_media_transparent_borders_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_current_user_avatar_path_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),
|
||||||
48 => wire__crate__bridge__api__rust_api_delete_account_impl(port, ptr, rust_vec_len, data_len),
|
47 => wire__crate__bridge__api__rust_api_connect_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),
|
48 => wire__crate__bridge__api__rust_api_connection_state_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),
|
49 => wire__crate__bridge__api__rust_api_crop_media_transparent_borders_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),
|
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_download_done_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_download_media_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_download_pending_media_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_ensure_avatar_png_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_establish_signal_session_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_events_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_finish_started_media_uploads_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_force_ipa_check_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_get_memories_url_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_get_memories_usage_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_get_plan_balance_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_get_proof_of_work_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_server_key_for_passwordless_recovery_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_user_by_id_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_user_data_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_user_id_from_username_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_hand_outbox_to_os_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_initialize_media_upload_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_initialize_memory_location_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_insert_and_send_additional_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_insert_and_send_ask_about_user_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_insert_and_send_contact_share_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_insert_and_send_text_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_ipa_purchase_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_load_plan_balance_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_media_size_limit_report_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_media_step_finished_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_notification_badge_count_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_notify_messages_opened_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_perform_passwordless_recovery_heartbeat_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_prerender_media_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_prewarm_memory_location_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_purge_media_temp_folder_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_purge_widget_media_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_refresh_widget_manifest_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_register_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_register_home_widget_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_register_passwordless_notification_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_register_passwordless_recovery_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_reload_configuration_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_remove_additional_user_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_remove_media_files_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_report_user_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_request_binary_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_request_contact_by_username_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_request_media_reupload_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_request_memories_upload_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_retransmit_all_messages_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_retry_pending_media_reuploads_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_reupload_pending_media_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_save_media_to_gallery_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_send_binary_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_send_contact_profile_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_send_encrypted_content_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_send_encrypted_content_to_group_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_media_to_groups_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_queued_message_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_text_message_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_typing_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_set_background_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_set_home_widget_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_set_login_token_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_set_media_display_limit_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_media_requires_authentication_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_media_trim_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_network_available_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_store_media_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_submit_recovery_share_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_sync_widget_permissions_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_toggle_media_remove_audio_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_try_request_contact_by_id_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_unregister_home_widget_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_update_fcm_token_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_update_signed_pre_key_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_upload_pqc_pre_keys_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__wrapper__app_database__rust_app_database_changes_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__wrapper__app_database__rust_app_database_execute_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__wrapper__app_database__rust_app_database_legacy_import_complete_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__wrapper__app_database__rust_app_database_migrate_legacy_database_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_select_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__backup__rust_backup_archive_create_backup_archive_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__backup__rust_backup_archive_get_backup_download_token_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__backup__rust_backup_archive_restore_backup_archive_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__backup__rust_backup_identity_get_backup_id_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_identity_get_backup_password_keys_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_identity_get_identity_backup_bytes_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_identity_import_backup_password_keys_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_restore_identity_backup_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_set_backup_password_keys_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__key_manager__rust_key_manager_decrypt_cloud_media_key_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__key_manager__rust_key_manager_encrypt_cloud_media_key_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__key_manager__rust_key_manager_get_signal_identity_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__key_manager__rust_key_manager_get_user_id_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_import_serialized_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_import_signal_identity_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_remove_local_credentials_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_serialize_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_set_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__signal__rust_signal_decrypt_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__signal__rust_signal_encrypt_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__signal__rust_signal_generate_bundle_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__signal__rust_signal_generate_pqc_prekeys_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_get_contact_public_key_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_get_user_public_key_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_process_prekey_bundle_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__rust_utils_generate_shares_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__rust_utils_recover_secret_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__webxdc__send_update_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__webxdc__serve_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__groups__update_chat_deletion_time_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__groups__update_group_name_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__updates_after_impl(port, ptr, rust_vec_len, data_len),
|
163 => wire__crate__bridge__webxdc__send_update_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),
|
164 => wire__crate__bridge__webxdc__serve_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),
|
165 => wire__crate__bridge__groups__update_chat_deletion_time_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),
|
166 => wire__crate__bridge__groups__update_group_name_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),
|
167 => wire__crate__bridge__webxdc__updates_after_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),
|
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!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -7804,17 +8008,17 @@ fn pde_ffi_dispatcher_sync_impl(
|
||||||
) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse {
|
) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse {
|
||||||
// Codec=Pde (Serialization + dispatch), see doc to use other codecs
|
// Codec=Pde (Serialization + dispatch), see doc to use other codecs
|
||||||
match func_id {
|
match func_id {
|
||||||
33 => wire__crate__bridge__api__rust_api_api_base_url_impl(ptr, rust_vec_len, data_len),
|
37 => 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),
|
39 => wire__crate__bridge__api__rust_api_avatar_png_path_impl(ptr, rust_vec_len, data_len),
|
||||||
47 => {
|
51 => {
|
||||||
wire__crate__bridge__api__rust_api_decode_avatar_svg_impl(ptr, rust_vec_len, data_len)
|
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,
|
ptr,
|
||||||
rust_vec_len,
|
rust_vec_len,
|
||||||
data_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!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -7916,6 +8120,28 @@ impl flutter_rust_bridge::IntoIntoDart<crate::keys::backup_password_keys::Backup
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
|
impl flutter_rust_bridge::IntoDart for FrbWrapper<crate::bridge::FlameState> {
|
||||||
|
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<crate::bridge::FlameState>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
impl flutter_rust_bridge::IntoIntoDart<FrbWrapper<crate::bridge::FlameState>>
|
||||||
|
for crate::bridge::FlameState
|
||||||
|
{
|
||||||
|
fn into_into_dart(self) -> FrbWrapper<crate::bridge::FlameState> {
|
||||||
|
self.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
impl flutter_rust_bridge::IntoDart
|
impl flutter_rust_bridge::IntoDart
|
||||||
for crate::bridge::wrapper::user_discovery::FlutterUserDiscovery
|
for crate::bridge::wrapper::user_discovery::FlutterUserDiscovery
|
||||||
{
|
{
|
||||||
|
|
@ -8893,6 +9119,16 @@ impl SseEncode for std::collections::HashMap<String, String> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SseEncode for std::collections::HashMap<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) {
|
||||||
|
<Vec<(String, crate::bridge::FlameState)>>::sse_encode(
|
||||||
|
self.into_iter().collect(),
|
||||||
|
serializer,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl SseEncode for std::collections::HashMap<String, Vec<String>> {
|
impl SseEncode for std::collections::HashMap<String, Vec<String>> {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
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) {
|
||||||
|
<i64>::sse_encode(self.counter, serializer);
|
||||||
|
<bool>::sse_encode(self.is_expiring, serializer);
|
||||||
|
<bool>::sse_encode(self.is_best_friend, serializer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl SseEncode for crate::bridge::wrapper::user_discovery::FlutterUserDiscovery {
|
impl SseEncode for crate::bridge::wrapper::user_discovery::FlutterUserDiscovery {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {}
|
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {}
|
||||||
|
|
@ -9270,6 +9515,16 @@ impl SseEncode for Vec<u8> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
<i32>::sse_encode(self.len() as _, serializer);
|
||||||
|
for item in self {
|
||||||
|
<(String, crate::bridge::FlameState)>::sse_encode(item, serializer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl SseEncode for Vec<(String, Vec<String>)> {
|
impl SseEncode for Vec<(String, Vec<String>)> {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||||
|
|
@ -9552,6 +9807,14 @@ impl SseEncode for (Vec<u8>, 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) {
|
||||||
|
<String>::sse_encode(self.0, serializer);
|
||||||
|
<crate::bridge::FlameState>::sse_encode(self.1, serializer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl SseEncode for (String, Vec<String>) {
|
impl SseEncode for (String, Vec<String>) {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ use crate::api::messages::incoming::messages::{
|
||||||
use crate::api::messages::outgoing::send_c2c_message_to_contact;
|
use crate::api::messages::outgoing::send_c2c_message_to_contact;
|
||||||
use crate::api::proto::client::encrypted_content::GroupJoin;
|
use crate::api::proto::client::encrypted_content::GroupJoin;
|
||||||
use crate::api::proto::client::{
|
use crate::api::proto::client::{
|
||||||
encrypted_appended_group_state, encrypted_content, EncryptedAppendedGroupState,
|
self as proto, encrypted_appended_group_state, encrypted_content, EncryptedAppendedGroupState,
|
||||||
EncryptedContent, EncryptedGroupState,
|
EncryptedContent, EncryptedGroupState,
|
||||||
};
|
};
|
||||||
use crate::api::proto::http_requests::{append_group_state, AppendGroupState, NewGroupState};
|
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::error::{Result, TwonlyError};
|
||||||
use crate::services::messages::MessageService;
|
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 model::GroupRecord;
|
||||||
use prost::Message;
|
use prost::Message;
|
||||||
use rand::{RngCore, SeedableRng};
|
use rand::{RngCore, SeedableRng};
|
||||||
|
|
@ -150,35 +150,46 @@ impl GroupService {
|
||||||
pub async fn on_connected(&self) -> Result<()> {
|
pub async fn on_connected(&self) -> Result<()> {
|
||||||
self.fetch_group_states_for_unjoined_groups().await?;
|
self.fetch_group_states_for_unjoined_groups().await?;
|
||||||
self.fetch_missing_group_public_keys(None, false).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 db = self.ctx.app_db.read().await.clone();
|
||||||
let groups = Group::flame_sync_candidates(&db.pool).await?;
|
let groups = Group::flame_sync_candidates(&db.pool).await?;
|
||||||
|
if groups.is_empty() {
|
||||||
let Some(best_friend) = groups.iter().max_by_key(|group| group.total_media_counter) else {
|
|
||||||
return Ok(());
|
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 now = current_time().timestamp();
|
||||||
let start_today = now - now.rem_euclid(86_400);
|
let start_today = start_of_local_day()?;
|
||||||
|
|
||||||
for group in groups {
|
for group in groups {
|
||||||
let Some(changed) = group.last_flame_counter_change else {
|
let Some(changed) = group.last_flame_counter_change else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
let forced = force_for_group == Some(group.group_id.as_str());
|
||||||
|
|
||||||
if changed < start_today
|
if changed < start_today {
|
||||||
|| group
|
continue;
|
||||||
|
}
|
||||||
|
if !forced
|
||||||
|
&& group
|
||||||
.last_flame_sync
|
.last_flame_sync
|
||||||
.is_some_and(|sync| sync >= start_today)
|
.is_some_and(|sync| sync >= start_today)
|
||||||
{
|
{
|
||||||
continue;
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -187,10 +198,10 @@ impl GroupService {
|
||||||
group.group_id.clone(),
|
group.group_id.clone(),
|
||||||
EncryptedContent {
|
EncryptedContent {
|
||||||
flame_sync: Some(encrypted_content::FlameSync {
|
flame_sync: Some(encrypted_content::FlameSync {
|
||||||
flame_counter: group.flame_counter,
|
flame_counter: counter,
|
||||||
last_flame_counter_change: changed * 1000,
|
last_flame_counter_change: changed * 1000,
|
||||||
best_friend: group.group_id == best_friend_id,
|
best_friend: is_best_friend,
|
||||||
force_update: false,
|
force_update: forced,
|
||||||
}),
|
}),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
|
|
@ -205,6 +216,45 @@ impl GroupService {
|
||||||
Ok(())
|
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<bool> {
|
||||||
|
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<Context>) -> Self {
|
pub fn new(ctx: &Arc<Context>) -> Self {
|
||||||
Self { ctx: ctx.clone() }
|
Self { ctx: ctx.clone() }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -256,6 +256,9 @@ pub struct UserConfig {
|
||||||
#[serde(default = "defaults::true_value")]
|
#[serde(default = "defaults::true_value")]
|
||||||
#[frb(non_final)]
|
#[frb(non_final)]
|
||||||
pub show_restore_flame: bool,
|
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)]
|
#[frb(non_final)]
|
||||||
pub my_best_friend_group_id: Option<String>,
|
pub my_best_friend_group_id: Option<String>,
|
||||||
#[serde(default, with = "optional_datetime")]
|
#[serde(default, with = "optional_datetime")]
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ use chrono::{DateTime, Local, TimeZone};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::{RwLock, RwLockReadGuard};
|
use tokio::sync::{RwLock, RwLockReadGuard};
|
||||||
|
|
||||||
|
use crate::error::{Result, TwonlyError};
|
||||||
|
|
||||||
pub(crate) fn milliseconds_to_seconds(timestamp: i64) -> i64 {
|
pub(crate) fn milliseconds_to_seconds(timestamp: i64) -> i64 {
|
||||||
timestamp.div_euclid(1_000)
|
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())
|
.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<i64> {
|
||||||
|
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 {
|
pub(crate) fn new_uuid_v4() -> String {
|
||||||
uuid::Uuid::new_v4().to_string()
|
uuid::Uuid::new_v4().to_string()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<void> 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<void> 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<int> getAndCreateUserId() async {
|
|
||||||
return mutex.protect<int>(() 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>(
|
|
||||||
TwonlyDB.forTesting(
|
|
||||||
DatabaseConnection(
|
|
||||||
NativeDatabase.memory(),
|
|
||||||
// Recommended for widget tests to avoid test errors.
|
|
||||||
closeStreamsSynchronously: true,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
..registerSingleton<UserService>(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();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue