move flame service to Rust

This commit is contained in:
otsmr 2026-09-11 23:42:25 +02:00
parent 54527fb55b
commit 512d4d05f8
20 changed files with 1620 additions and 954 deletions

View file

@ -18,6 +18,31 @@ Future<void> initializeTwonlyFlutter({required InitConfig config}) =>
Future<void> initializeTwonlyStandalone({required InitConfig config}) =>
RustLib.instance.api.crateBridgeInitializeTwonlyStandalone(config: config);
class FlameState {
final PlatformInt64 counter;
final bool isExpiring;
final bool isBestFriend;
const FlameState({
required this.counter,
required this.isExpiring,
required this.isBestFriend,
});
@override
int get hashCode =>
counter.hashCode ^ isExpiring.hashCode ^ isBestFriend.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is FlameState &&
runtimeType == other.runtimeType &&
counter == other.counter &&
isExpiring == other.isExpiring &&
isBestFriend == other.isBestFriend;
}
class InitConfig {
final String databaseDir;
final String dataDir;

View file

@ -3,9 +3,28 @@
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import '../bridge.dart';
import '../frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
/// The flame counter for one chat, derived against today rather than read from
/// the stored column. See `Group::flame_states`.
Future<FlameState> 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({
required String groupName,
required Int64List memberIds,

File diff suppressed because it is too large Load diff

View file

@ -57,6 +57,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
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
Map<String, List<String>> dco_decode_Map_String_list_String_None(dynamic raw);
@ -132,6 +135,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
double dco_decode_f_64(dynamic raw);
@protected
FlameState dco_decode_flame_state(dynamic raw);
@protected
FlutterUserDiscovery dco_decode_flutter_user_discovery(dynamic raw);
@ -224,6 +230,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw);
@protected
List<(String, FlameState)> dco_decode_list_record_string_flame_state(
dynamic raw,
);
@protected
List<(String, List<String>)> dco_decode_list_record_string_list_string(
dynamic raw,
@ -320,6 +331,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
dynamic raw,
);
@protected
(String, FlameState) dco_decode_record_string_flame_state(dynamic raw);
@protected
(String, List<String>) dco_decode_record_string_list_string(dynamic raw);
@ -424,6 +438,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
Map<String, FlameState> sse_decode_Map_String_flame_state_None(
SseDeserializer deserializer,
);
@protected
Map<String, List<String>> sse_decode_Map_String_list_String_None(
SseDeserializer deserializer,
@ -521,6 +540,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
double sse_decode_f_64(SseDeserializer deserializer);
@protected
FlameState sse_decode_flame_state(SseDeserializer deserializer);
@protected
FlutterUserDiscovery sse_decode_flutter_user_discovery(
SseDeserializer deserializer,
@ -633,6 +655,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer);
@protected
List<(String, FlameState)> sse_decode_list_record_string_flame_state(
SseDeserializer deserializer,
);
@protected
List<(String, List<String>)> sse_decode_list_record_string_list_string(
SseDeserializer deserializer,
@ -743,6 +770,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
(String, FlameState) sse_decode_record_string_flame_state(
SseDeserializer deserializer,
);
@protected
(String, List<String>) sse_decode_record_string_list_string(
SseDeserializer deserializer,
@ -879,6 +911,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_Map_String_flame_state_None(
Map<String, FlameState> self,
SseSerializer serializer,
);
@protected
void sse_encode_Map_String_list_String_None(
Map<String, List<String>> self,
@ -999,6 +1037,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_f_64(double self, SseSerializer serializer);
@protected
void sse_encode_flame_state(FlameState self, SseSerializer serializer);
@protected
void sse_encode_flutter_user_discovery(
FlutterUserDiscovery self,
@ -1143,6 +1184,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_list_record_string_flame_state(
List<(String, FlameState)> self,
SseSerializer serializer,
);
@protected
void sse_encode_list_record_string_list_string(
List<(String, List<String>)> self,
@ -1281,6 +1328,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_record_string_flame_state(
(String, FlameState) self,
SseSerializer serializer,
);
@protected
void sse_encode_record_string_list_string(
(String, List<String>) self,

View file

@ -59,6 +59,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
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
Map<String, List<String>> dco_decode_Map_String_list_String_None(dynamic raw);
@ -134,6 +137,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
double dco_decode_f_64(dynamic raw);
@protected
FlameState dco_decode_flame_state(dynamic raw);
@protected
FlutterUserDiscovery dco_decode_flutter_user_discovery(dynamic raw);
@ -226,6 +232,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw);
@protected
List<(String, FlameState)> dco_decode_list_record_string_flame_state(
dynamic raw,
);
@protected
List<(String, List<String>)> dco_decode_list_record_string_list_string(
dynamic raw,
@ -322,6 +333,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
dynamic raw,
);
@protected
(String, FlameState) dco_decode_record_string_flame_state(dynamic raw);
@protected
(String, List<String>) dco_decode_record_string_list_string(dynamic raw);
@ -426,6 +440,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
Map<String, FlameState> sse_decode_Map_String_flame_state_None(
SseDeserializer deserializer,
);
@protected
Map<String, List<String>> sse_decode_Map_String_list_String_None(
SseDeserializer deserializer,
@ -523,6 +542,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
double sse_decode_f_64(SseDeserializer deserializer);
@protected
FlameState sse_decode_flame_state(SseDeserializer deserializer);
@protected
FlutterUserDiscovery sse_decode_flutter_user_discovery(
SseDeserializer deserializer,
@ -635,6 +657,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer);
@protected
List<(String, FlameState)> sse_decode_list_record_string_flame_state(
SseDeserializer deserializer,
);
@protected
List<(String, List<String>)> sse_decode_list_record_string_list_string(
SseDeserializer deserializer,
@ -745,6 +772,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
(String, FlameState) sse_decode_record_string_flame_state(
SseDeserializer deserializer,
);
@protected
(String, List<String>) sse_decode_record_string_list_string(
SseDeserializer deserializer,
@ -881,6 +913,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_Map_String_flame_state_None(
Map<String, FlameState> self,
SseSerializer serializer,
);
@protected
void sse_encode_Map_String_list_String_None(
Map<String, List<String>> self,
@ -1001,6 +1039,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_f_64(double self, SseSerializer serializer);
@protected
void sse_encode_flame_state(FlameState self, SseSerializer serializer);
@protected
void sse_encode_flutter_user_discovery(
FlutterUserDiscovery self,
@ -1145,6 +1186,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_list_record_string_flame_state(
List<(String, FlameState)> self,
SseSerializer serializer,
);
@protected
void sse_encode_list_record_string_list_string(
List<(String, List<String>)> self,
@ -1283,6 +1330,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_record_string_flame_state(
(String, FlameState) self,
SseSerializer serializer,
);
@protected
void sse_encode_record_string_list_string(
(String, List<String>) self,

View file

@ -1,10 +1,11 @@
import 'package:clock/clock.dart' show clock;
import 'package:drift/drift.dart';
import 'package:hashlib/random.dart';
import 'package:twonly/core/bridge.dart' show FlameState;
import 'package:twonly/core/bridge/groups.dart' as rust_groups;
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/tables/groups.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/flame.service.dart';
import 'package:twonly/src/utils/misc.dart';
part 'groups.dao.g.dart';
@ -236,7 +237,10 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
)..where((t) => t.groupId.equals(groupId))).getSingleOrNull();
}
Stream<({int counter, bool isExpiring})> watchFlameCounter(String groupId) {
/// Drift watches the row only to learn *when* to ask again: the counter
/// itself is derived in Rust, because it depends on the current day as much
/// as on the stored columns.
Stream<FlameState> watchFlameCounter(String groupId) {
return (select(groups)..where(
(u) =>
u.groupId.equals(groupId) &
@ -244,7 +248,16 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
u.lastMessageSend.isNotNull(),
))
.watchSingleOrNull()
.map(getFlameCounterFromGroup);
.asyncMap((group) {
if (group == null) {
return const FlameState(
counter: 0,
isExpiring: false,
isBestFriend: false,
);
}
return rust_groups.flameState(groupId: groupId);
});
}
Future<List<Group>> getAllDirectChats() {

View file

@ -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),
),
);
}

View file

@ -1,5 +1,3 @@
// ignore_for_file: unreachable_from_main
import 'dart:async';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';

View file

@ -1,9 +1,10 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:twonly/core/bridge.dart' show FlameState;
import 'package:twonly/core/bridge/groups.dart' as rust_groups;
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/flame.service.dart';
import 'package:twonly/src/visual/components/animate_icon.comp.dart';
class FlameCounterWidget extends StatefulWidget {
@ -28,7 +29,7 @@ class _FlameCounterWidgetState extends State<FlameCounterWidget> {
bool isBestFriend = false;
bool isExpiring = false;
StreamSubscription<({int counter, bool isExpiring})>? flameCounterSub;
StreamSubscription<FlameState>? flameCounterSub;
@override
void initState() {
@ -62,31 +63,24 @@ class _FlameCounterWidgetState extends State<FlameCounterWidget> {
group = await twonlyDB.groupsDao.getGroup(groupId);
}
if (groupId != null && group != null) {
isBestFriend =
userService.currentUser.myBestFriendGroupId == groupId &&
group.alsoBestFriend;
if (widget.group != null) {
final result = getFlameCounterFromGroup(group);
if (mounted) {
setState(() {
flameCounter = result.counter;
isExpiring = result.isExpiring;
});
}
_apply(await rust_groups.flameState(groupId: groupId));
return;
}
final stream = twonlyDB.groupsDao.watchFlameCounter(groupId);
flameCounterSub = stream.listen((result) {
if (mounted) {
setState(() {
flameCounter = result.counter;
isExpiring = result.isExpiring;
});
}
});
flameCounterSub = stream.listen(_apply);
}
}
void _apply(FlameState state) {
if (!mounted) return;
setState(() {
flameCounter = state.counter;
isExpiring = state.isExpiring;
isBestFriend = state.isBestFriend;
});
}
@override
Widget build(BuildContext context) {
if (flameCounter < 1) return Container();

View file

@ -3,12 +3,12 @@ import 'dart:collection';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/core/bridge/groups.dart' as rust_groups;
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart';
import 'package:twonly/src/services/flame.service.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
@ -56,6 +56,7 @@ class _ShareImageView extends State<ShareImageView> {
final TextEditingController searchUserName = TextEditingController();
late StreamSubscription<List<Group>> allGroupSub;
String lastQuery = '';
int _updateGroupsGeneration = 0;
@override
void initState() {
@ -99,13 +100,20 @@ class _ShareImageView extends State<ShareImageView> {
}
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
groups.sort((a, b) {
// First, compare by flameCounter
final flameComparison = getFlameCounterFromGroup(
b,
).counter.compareTo(getFlameCounterFromGroup(a).counter);
final flameComparison = flameOf(b).compareTo(flameOf(a));
if (flameComparison != 0) {
return flameComparison; // Sort by flameCounter in descending order
}
@ -122,9 +130,7 @@ class _ShareImageView extends State<ShareImageView> {
for (final group in groups) {
if (group.pinned) continue;
if (!group.archived &&
getFlameCounterFromGroup(group).counter > 0 &&
bestFriends.length < 6) {
if (!group.archived && flameOf(group) > 0 && bestFriends.length < 6) {
bestFriends.add(group);
} else {
otherUsers.add(group);

View file

@ -1,18 +1,12 @@
import 'dart:async';
import 'package:drift/drift.dart' show Value;
import 'package:fixnum/fixnum.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:twonly/core/bridge/groups.dart' as rust_groups;
import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
as pb;
import 'package:twonly/src/services/flame.service.dart';
import 'package:twonly/src/services/subscription.service.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
@ -34,6 +28,7 @@ class RestoreFlameComp extends StatefulWidget {
class _RestoreFlameCompState extends State<RestoreFlameComp> {
Group? _group;
bool _canRestore = false;
late String _groupId;
late StreamSubscription<Group?> _groupSub;
@ -45,8 +40,15 @@ class _RestoreFlameCompState extends State<RestoreFlameComp> {
userService.currentUser.userId,
);
final stream = twonlyDB.groupsDao.watchGroup(_groupId);
_groupSub = stream.listen((update) {
if (mounted) setState(() => _group = update);
_groupSub = stream.listen((update) async {
// Whether a restore is still on offer depends on today as much as on the
// row, so Rust decides it; the row change is only the cue to ask again.
final canRestore = await rust_groups.canRestoreFlames(groupId: _groupId);
if (!mounted) return;
setState(() {
_group = update;
_canRestore = canRestore;
});
});
}
@ -69,42 +71,11 @@ class _RestoreFlameCompState extends State<RestoreFlameComp> {
'Restoring flames from ${_group!.flameCounter} to ${_group!.maxFlameCounter}',
);
await restoreFlames(_groupId);
final addData = AdditionalMessageData(
type: AdditionalMessageData_Type.RESTORED_FLAME_COUNTER,
restoredFlameCounter: Int64(_group!.maxFlameCounter),
);
final message = await twonlyDB.messagesDao.insertMessage(
MessagesCompanion(
groupId: Value(_groupId),
type: Value(MessageType.restoreFlameCounter.name),
additionalMessageData: Value(addData.writeToBuffer()),
),
);
if (message == null) {
Log.error('Could not insert message into database');
return;
// Rust owns the whole restore: the counter, the chat entry, and the forced
// flame sync that brings the peer back in line.
if (!await rust_groups.restoreFlames(groupId: _groupId)) {
Log.error('Could not restore the flame counter');
}
final encryptedContent = pb.EncryptedContent(
additionalDataMessage: pb.EncryptedContent_AdditionalDataMessage(
senderMessageId: message.messageId,
additionalMessageData: addData.writeToBuffer(),
timestamp: Int64(message.createdAt.millisecondsSinceEpoch),
type: MessageType.restoreFlameCounter.name,
),
);
await syncFlameCounters(forceForGroup: _groupId);
await RustApi.sendEncryptedContentToGroup(
groupId: _groupId,
content: encryptedContent.writeToBuffer(),
messageId: message.messageId,
onlySendIfNoReceiptsAreOpen: false,
);
}
@override
@ -112,7 +83,7 @@ class _RestoreFlameCompState extends State<RestoreFlameComp> {
if (!userService.currentUser.showRestoreFlame) {
return const SizedBox.shrink();
}
if (_group == null || !isItPossibleToRestoreFlames(_group!)) {
if (_group == null || !_canRestore) {
return Container();
}
if (widget.flameOnRightSide) {

View file

@ -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> {
let ctx = crate::context::Context::get_static()?;

View file

@ -16,6 +16,7 @@ use crate::context::Context;
use crate::error::Result;
use flutter_rust_bridge::frb;
pub use crate::database::app::tables::FlameState;
pub use crate::user_discovery::AnnouncedUser;
pub use crate::user_discovery::OtherPromotion;
@ -41,6 +42,13 @@ pub struct _AnnouncedUser {
pub public_id: i64,
}
#[frb(mirror(FlameState))]
pub struct _FlameState {
pub counter: i64,
pub is_expiring: bool,
pub is_best_friend: bool,
}
pub(super) fn get_twonly_flutter() -> Result<&'static Context> {
let ctx = Context::get_static()?;
Ok(&**ctx)

View file

@ -3,11 +3,12 @@
*
*/
use chrono::{Local, TimeZone};
use sqlx::{Sqlite, Transaction};
use std::collections::{HashMap, HashSet};
use crate::context::Context;
use crate::error::{Result, TwonlyError};
use crate::utils::{current_time, start_of_local_day};
use super::Contact;
@ -19,14 +20,150 @@ impl Group {
pub async fn flame_sync_candidates(pool: &sqlx::Pool<Sqlite>) -> Result<Vec<FlameSyncGroup>> {
Ok(sqlx::query_as!(
FlameSyncGroup,
r#"SELECT group_id, total_media_counter, last_flame_counter_change,
last_flame_sync, flame_counter
r#"SELECT group_id, total_media_counter, last_flame_counter_change, last_flame_sync
FROM groups WHERE last_flame_counter_change IS NOT NULL"#
)
.fetch_all(pool)
.await?)
}
/// The chat the user exchanges the most media with. Derived rather than
/// stored: it is a pure function of `total_media_counter`, and persisting it
/// in the user configuration would leave Flutter's in-memory copy stale,
/// because nothing tells Dart when Rust rewrites that file.
pub async fn best_friend_group_id(pool: &sqlx::Pool<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(
pool: &sqlx::Pool<Sqlite>,
group_id: &str,
@ -89,16 +226,8 @@ impl Group {
.await?;
}
let now = Local::now();
let start_of_today = Local
.from_local_datetime(
&now.date_naive()
.and_hms_opt(0, 0, 0)
.ok_or_else(|| TwonlyError::Generic("invalid local date".into()))?,
)
.earliest()
.ok_or_else(|| TwonlyError::Generic("local day has no midnight".into()))?
.timestamp();
let now = current_time();
let start_of_today = start_of_local_day()?;
let two_days_ago = start_of_today - 2 * 24 * 60 * 60;
let mut flame_counter = group.flame_counter;
@ -349,7 +478,55 @@ pub struct FlameSyncGroup {
pub total_media_counter: i64,
pub last_flame_counter_change: Option<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)]
@ -708,10 +885,230 @@ fn current_unix_timestamp() -> Result<i64> {
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)]
mod tests {
use super::*;
use crate::database::app::AppDatabase;
use chrono::{Local, TimeZone};
const DAY: i64 = 24 * 60 * 60;
/// Local midnight of the given date, which is what the derivation compares
/// against. Mirrors the `DateTime(2026, 2, 2)` literals the Dart tests used.
fn local_midnight(year: i32, month: u32, day: u32) -> i64 {
Local
.with_ymd_and_hms(year, month, day, 0, 0, 0)
.earliest()
.unwrap()
.timestamp()
}
fn row(counter: i64, sent: i64, received: i64, changed: i64) -> FlameRow {
FlameRow {
group_id: "group".into(),
flame_counter: counter,
also_best_friend: 0,
last_message_send: Some(sent),
last_message_received: Some(received),
last_flame_counter_change: Some(changed),
}
}
#[test]
fn a_streak_without_any_exchange_is_zero() {
let empty = FlameRow {
group_id: "group".into(),
flame_counter: 7,
also_best_friend: 0,
last_message_send: None,
last_message_received: None,
last_flame_counter_change: None,
};
assert_eq!(empty.derive(local_midnight(2026, 2, 3), false).counter, 0);
}
/// The day-by-day walk the Dart `normal flame expiring` test performed: one
/// exchange on Feb 2 stays visible through Feb 4 and is gone on Feb 5.
#[test]
fn a_streak_survives_two_days_then_expires() {
let sent = local_midnight(2026, 2, 2) + 15 * 3600;
let received = local_midnight(2026, 2, 2) + 10 * 3600;
let row = row(1, sent, received, sent);
assert_eq!(row.derive(local_midnight(2026, 2, 2), false).counter, 1);
assert_eq!(row.derive(local_midnight(2026, 2, 3), false).counter, 1);
assert_eq!(row.derive(local_midnight(2026, 2, 4), false).counter, 1);
assert_eq!(row.derive(local_midnight(2026, 2, 5), false).counter, 0);
assert_eq!(row.derive(local_midnight(2026, 3, 1), false).counter, 0);
}
/// The `isExpiring` half of the same Dart test: an exchange from two days
/// back still counts, but warns that it ends at midnight.
#[test]
fn a_streak_warns_on_the_day_before_it_lapses() {
let exchange = local_midnight(2026, 2, 7) + 11 * 3600;
let row = row(3, exchange, exchange - 3600, exchange);
let same_day = row.derive(local_midnight(2026, 2, 7), false);
assert_eq!((same_day.counter, same_day.is_expiring), (3, false));
let next_day = row.derive(local_midnight(2026, 2, 8), false);
assert_eq!((next_day.counter, next_day.is_expiring), (3, false));
let last_day = row.derive(local_midnight(2026, 2, 9), false);
assert_eq!((last_day.counter, last_day.is_expiring), (3, true));
let lapsed = row.derive(local_midnight(2026, 2, 10), false);
assert_eq!((lapsed.counter, lapsed.is_expiring), (0, false));
}
/// The Dart `isRestore Possible` test walked March 24 to 27 against a max
/// set on March 20; the offer stops exactly seven days after that.
#[test]
fn restoring_is_offered_for_seven_days() {
let max_from = local_midnight(2026, 3, 20) + 3 * 3600;
for days in 4..=6 {
assert!(
can_restore(20, Some(max_from), 0, max_from + days * DAY),
"expected the offer {days} days after the streak was lost"
);
}
assert!(!can_restore(20, Some(max_from), 0, max_from + 7 * DAY + 1));
}
#[test]
fn restoring_is_not_offered_without_a_streak_worth_restoring() {
let now = local_midnight(2026, 3, 24);
// Too short to miss.
assert!(!can_restore(2, Some(now - DAY), 0, now));
// Nothing was lost.
assert!(!can_restore(20, Some(now - DAY), 20, now));
// Never reached a maximum.
assert!(!can_restore(20, None, 0, now));
}
#[tokio::test]
async fn restoring_brings_the_streak_back_to_its_maximum() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("app.sqlite");
let database = AppDatabase::new(path.to_str().unwrap(), None, false)
.await
.unwrap();
database.run_migrations().await.unwrap();
let lapsed = current_time().timestamp() - 4 * DAY;
sqlx::query(
r#"INSERT INTO groups(group_id, group_name, flame_counter, max_flame_counter,
max_flame_counter_from, last_message_send,
last_message_received, last_flame_counter_change)
VALUES ('group', 'Group', 0, 5, ?, ?, ?, ?)"#,
)
.bind(lapsed)
.bind(lapsed)
.bind(lapsed)
.bind(lapsed)
.execute(&database.pool)
.await
.unwrap();
assert_eq!(
Group::flame_state(&database.pool, "group")
.await
.unwrap()
.counter,
0
);
assert!(Group::can_restore_flames(&database.pool, "group")
.await
.unwrap());
let mut transaction = database.pool.begin().await.unwrap();
let restored = Group::restore_flames(&mut transaction, "group")
.await
.unwrap();
transaction.commit().await.unwrap();
assert_eq!(restored, Some(5));
let state = Group::flame_state(&database.pool, "group").await.unwrap();
assert_eq!((state.counter, state.is_expiring), (5, false));
// Nothing is left to restore once it is back.
assert!(!Group::can_restore_flames(&database.pool, "group")
.await
.unwrap());
// The write path enforces the same rule, so a rapid second tap cannot
// create another restoration message after the first one succeeded.
let mut transaction = database.pool.begin().await.unwrap();
let restored_again = Group::restore_flames(&mut transaction, "group")
.await
.unwrap();
transaction.commit().await.unwrap();
assert_eq!(restored_again, None);
}
/// The heart emoji needs both halves: our most-exchanged chat, and their
/// `also_best_friend` flag saying they picked us too.
#[tokio::test]
async fn the_best_friend_is_the_most_exchanged_chat_and_has_to_be_mutual() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("app.sqlite");
let database = AppDatabase::new(path.to_str().unwrap(), None, false)
.await
.unwrap();
database.run_migrations().await.unwrap();
let now = current_time().timestamp();
for (group_id, media, mutual) in [("top", 50, 1), ("second", 10, 1), ("onesided", 5, 0)] {
sqlx::query(
r#"INSERT INTO groups(group_id, group_name, total_media_counter, also_best_friend,
flame_counter, last_message_send, last_message_received,
last_flame_counter_change)
VALUES (?, 'Group', ?, ?, 9, ?, ?, ?)"#,
)
.bind(group_id)
.bind(media)
.bind(mutual)
.bind(now)
.bind(now)
.bind(now)
.execute(&database.pool)
.await
.unwrap();
}
assert_eq!(
Group::best_friend_group_id(&database.pool).await.unwrap(),
Some("top".to_string())
);
let states = Group::flame_states(&database.pool, None).await.unwrap();
assert!(states["top"].is_best_friend);
assert!(!states["second"].is_best_friend);
assert!(!states["onesided"].is_best_friend);
// A requested subset still resolves the best friend across all groups.
let subset = Group::flame_states(&database.pool, Some(vec!["top".into()]))
.await
.unwrap();
assert_eq!(subset.len(), 1);
assert!(subset["top"].is_best_friend);
}
#[tokio::test]
async fn media_exchange_updates_contact_and_flame_counters_atomically() {

View file

@ -14,8 +14,8 @@ pub mod receipt;
pub use contact::{Contact, UpdateContact};
pub use group_history::{GroupHistoryType, InsertGroupHistories};
pub use groups::{
DeleteGroupMembers, GetGroupPublicKey, GetMissingGroupPublicKeys, GetUnjoinedGroups, Group,
InsertGroup, InsertGroupHistory, InsertGroupMember, UpdateGroup, UpdateGroupMemberState,
DeleteGroupMembers, FlameState, GetGroupPublicKey, GetMissingGroupPublicKeys, GetUnjoinedGroups,
Group, InsertGroup, InsertGroupHistory, InsertGroupMember, UpdateGroup, UpdateGroupMemberState,
};
pub use key_verification::{KeyVerification, KeyVerificationType, NewKeyVerification};
pub use mediafiles::MediaFile;

View file

@ -38,7 +38,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
default_rust_auto_opaque = RustAutoOpaqueMoi,
);
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0";
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 101877534;
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -2068471902;
// Section: executor
@ -198,6 +198,43 @@ fn wire__crate__bridge__webxdc__cache_update_impl(
},
)
}
fn wire__crate__bridge__groups__can_restore_flames_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
rust_vec_len_: i32,
data_len_: i32,
) {
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::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(
port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
@ -539,6 +576,78 @@ fn wire__crate__bridge__groups__fetch_missing_group_public_keys_impl(
},
)
}
fn wire__crate__bridge__groups__flame_state_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
rust_vec_len_: i32,
data_len_: i32,
) {
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::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(
port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
@ -1084,6 +1193,42 @@ fn wire__crate__bridge__groups__remove_member_from_group_impl(
},
)
}
fn wire__crate__bridge__groups__restore_flames_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
rust_vec_len_: i32,
data_len_: i32,
) {
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::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(
port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
@ -6229,6 +6374,16 @@ fn wire__crate__bridge__logging__write_log_impl(
)
}
// Section: static_checks
#[allow(clippy::unnecessary_literal_unwrap)]
const _: fn() = || {
let FlameState = None::<crate::bridge::FlameState>.unwrap();
let _: i64 = FlameState.counter;
let _: bool = FlameState.is_expiring;
let _: bool = FlameState.is_best_friend;
};
// Section: related_funcs
fn decode_DartFn_Inputs_i_64_Output_unit_AnyhowException(
@ -6338,6 +6493,14 @@ impl SseDecode for std::collections::HashMap<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>> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
@ -6449,6 +6612,20 @@ impl SseDecode for f64 {
}
}
impl SseDecode for crate::bridge::FlameState {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
let mut var_counter = <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 {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
@ -6813,6 +6990,20 @@ impl SseDecode for Vec<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>)> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
@ -7158,6 +7349,15 @@ impl SseDecode for (Vec<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>) {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
@ -7631,167 +7831,171 @@ fn pde_ffi_dispatcher_primary_impl(
2 => wire__crate__bridge__groups__add_new_group_members_impl(port, ptr, rust_vec_len, data_len),
3 => wire__crate__bridge__webxdc__address_for_impl(port, ptr, rust_vec_len, data_len),
4 => wire__crate__bridge__webxdc__cache_update_impl(port, ptr, rust_vec_len, data_len),
5 => wire__crate__bridge__webxdc__catalog_impl(port, ptr, rust_vec_len, data_len),
6 => wire__crate__bridge__logging__clean_log_file_impl(port, ptr, rust_vec_len, data_len),
7 => wire__crate__bridge__logging__clear_log_file_impl(port, ptr, rust_vec_len, data_len),
8 => wire__crate__bridge__webxdc__create_instance_impl(port, ptr, rust_vec_len, data_len),
9 => wire__crate__bridge__groups__create_new_group_impl(port, ptr, rust_vec_len, data_len),
10 => wire__crate__bridge__webxdc__delete_instance_impl(port, ptr, rust_vec_len, data_len),
11 => wire__crate__bridge__groups__fetch_group_state_impl(port, ptr, rust_vec_len, data_len),
12 => wire__crate__bridge__groups__fetch_group_states_for_unjoined_groups_impl(port, ptr, rust_vec_len, data_len),
13 => wire__crate__bridge__groups__fetch_missing_group_public_keys_impl(port, ptr, rust_vec_len, data_len),
14 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_change_exclusion_for_contact_impl(port, ptr, rust_vec_len, data_len),
15 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_get_current_version_impl(port, ptr, rust_vec_len, data_len),
16 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_update_verification_state_for_user_impl(port, ptr, rust_vec_len, data_len),
17 => wire__crate__bridge__callbacks__init_flutter_callbacks_impl(port, ptr, rust_vec_len, data_len),
18 => wire__crate__bridge__initialize_twonly_flutter_impl(port, ptr, rust_vec_len, data_len),
19 => wire__crate__bridge__initialize_twonly_standalone_impl(port, ptr, rust_vec_len, data_len),
20 => wire__crate__bridge__webxdc__instance_impl(port, ptr, rust_vec_len, data_len),
21 => wire__crate__bridge__groups__leave_group_impl(port, ptr, rust_vec_len, data_len),
22 => wire__crate__bridge__logging__load_log_file_impl(port, ptr, rust_vec_len, data_len),
23 => wire__crate__bridge__groups__manage_admin_state_impl(port, ptr, rust_vec_len, data_len),
24 => wire__crate__bridge__webxdc__members_impl(port, ptr, rust_vec_len, data_len),
25 => wire__crate__bridge__webxdc__one_time_instances_impl(port, ptr, rust_vec_len, data_len),
26 => wire__crate__bridge__webxdc__prepare_bundle_impl(port, ptr, rust_vec_len, data_len),
27 => wire__crate__bridge__logging__read_last_log_lines_impl(port, ptr, rust_vec_len, data_len),
28 => wire__crate__bridge__webxdc__refresh_catalog_impl(port, ptr, rust_vec_len, data_len),
29 => wire__crate__bridge__groups__remove_member_from_group_impl(port, ptr, rust_vec_len, data_len),
30 => wire__crate__bridge__api__rust_api_abandon_media_impl(port, ptr, rust_vec_len, data_len),
31 => wire__crate__bridge__api__rust_api_add_additional_user_impl(port, ptr, rust_vec_len, data_len),
32 => wire__crate__bridge__api__rust_api_allocate_sequence_impl(port, ptr, rust_vec_len, data_len),
34 => wire__crate__bridge__api__rust_api_authentication_headers_impl(port, ptr, rust_vec_len, data_len),
36 => wire__crate__bridge__api__rust_api_change_username_impl(port, ptr, rust_vec_len, data_len),
37 => wire__crate__bridge__api__rust_api_check_for_deleted_usernames_impl(port, ptr, rust_vec_len, data_len),
38 => wire__crate__bridge__api__rust_api_check_for_passwordless_notification_impl(port, ptr, rust_vec_len, data_len),
39 => wire__crate__bridge__api__rust_api_clear_contact_request_notifications_impl(port, ptr, rust_vec_len, data_len),
40 => wire__crate__bridge__api__rust_api_clear_conversation_notifications_impl(port, ptr, rust_vec_len, data_len),
41 => wire__crate__bridge__api__rust_api_close_impl(port, ptr, rust_vec_len, data_len),
42 => wire__crate__bridge__api__rust_api_confirm_memories_upload_impl(port, ptr, rust_vec_len, data_len),
43 => wire__crate__bridge__api__rust_api_connect_impl(port, ptr, rust_vec_len, data_len),
44 => wire__crate__bridge__api__rust_api_connection_state_impl(port, ptr, rust_vec_len, data_len),
45 => wire__crate__bridge__api__rust_api_crop_media_transparent_borders_impl(port, ptr, rust_vec_len, data_len),
46 => wire__crate__bridge__api__rust_api_current_user_avatar_path_impl(port, ptr, rust_vec_len, data_len),
48 => wire__crate__bridge__api__rust_api_delete_account_impl(port, ptr, rust_vec_len, data_len),
49 => wire__crate__bridge__api__rust_api_delete_memory_impl(port, ptr, rust_vec_len, data_len),
50 => wire__crate__bridge__api__rust_api_delete_widget_media_impl(port, ptr, rust_vec_len, data_len),
51 => wire__crate__bridge__api__rust_api_disable_memories_backup_impl(port, ptr, rust_vec_len, data_len),
52 => wire__crate__bridge__api__rust_api_download_done_impl(port, ptr, rust_vec_len, data_len),
53 => wire__crate__bridge__api__rust_api_download_media_impl(port, ptr, rust_vec_len, data_len),
54 => wire__crate__bridge__api__rust_api_download_pending_media_impl(port, ptr, rust_vec_len, data_len),
55 => wire__crate__bridge__api__rust_api_ensure_avatar_png_impl(port, ptr, rust_vec_len, data_len),
56 => wire__crate__bridge__api__rust_api_establish_signal_session_impl(port, ptr, rust_vec_len, data_len),
57 => wire__crate__bridge__api__rust_api_events_impl(port, ptr, rust_vec_len, data_len),
58 => wire__crate__bridge__api__rust_api_finish_started_media_uploads_impl(port, ptr, rust_vec_len, data_len),
59 => wire__crate__bridge__api__rust_api_force_ipa_check_impl(port, ptr, rust_vec_len, data_len),
60 => wire__crate__bridge__api__rust_api_get_memories_url_impl(port, ptr, rust_vec_len, data_len),
61 => wire__crate__bridge__api__rust_api_get_memories_usage_impl(port, ptr, rust_vec_len, data_len),
62 => wire__crate__bridge__api__rust_api_get_plan_balance_impl(port, ptr, rust_vec_len, data_len),
63 => wire__crate__bridge__api__rust_api_get_proof_of_work_impl(port, ptr, rust_vec_len, data_len),
64 => wire__crate__bridge__api__rust_api_get_server_key_for_passwordless_recovery_impl(port, ptr, rust_vec_len, data_len),
65 => wire__crate__bridge__api__rust_api_get_user_by_id_impl(port, ptr, rust_vec_len, data_len),
66 => wire__crate__bridge__api__rust_api_get_user_data_impl(port, ptr, rust_vec_len, data_len),
67 => wire__crate__bridge__api__rust_api_get_user_id_from_username_impl(port, ptr, rust_vec_len, data_len),
68 => wire__crate__bridge__api__rust_api_hand_outbox_to_os_impl(port, ptr, rust_vec_len, data_len),
69 => wire__crate__bridge__api__rust_api_initialize_media_upload_impl(port, ptr, rust_vec_len, data_len),
70 => wire__crate__bridge__api__rust_api_initialize_memory_location_impl(port, ptr, rust_vec_len, data_len),
71 => wire__crate__bridge__api__rust_api_insert_and_send_additional_data_impl(port, ptr, rust_vec_len, data_len),
72 => wire__crate__bridge__api__rust_api_insert_and_send_ask_about_user_impl(port, ptr, rust_vec_len, data_len),
73 => wire__crate__bridge__api__rust_api_insert_and_send_contact_share_impl(port, ptr, rust_vec_len, data_len),
74 => wire__crate__bridge__api__rust_api_insert_and_send_text_impl(port, ptr, rust_vec_len, data_len),
75 => wire__crate__bridge__api__rust_api_ipa_purchase_impl(port, ptr, rust_vec_len, data_len),
76 => wire__crate__bridge__api__rust_api_load_plan_balance_impl(port, ptr, rust_vec_len, data_len),
77 => wire__crate__bridge__api__rust_api_media_size_limit_report_impl(port, ptr, rust_vec_len, data_len),
78 => wire__crate__bridge__api__rust_api_media_step_finished_impl(port, ptr, rust_vec_len, data_len),
79 => wire__crate__bridge__api__rust_api_notification_badge_count_impl(port, ptr, rust_vec_len, data_len),
80 => wire__crate__bridge__api__rust_api_notify_messages_opened_impl(port, ptr, rust_vec_len, data_len),
81 => wire__crate__bridge__api__rust_api_perform_passwordless_recovery_heartbeat_impl(port, ptr, rust_vec_len, data_len),
82 => wire__crate__bridge__api__rust_api_prerender_media_impl(port, ptr, rust_vec_len, data_len),
83 => wire__crate__bridge__api__rust_api_prewarm_memory_location_impl(port, ptr, rust_vec_len, data_len),
84 => wire__crate__bridge__api__rust_api_purge_media_temp_folder_impl(port, ptr, rust_vec_len, data_len),
85 => wire__crate__bridge__api__rust_api_purge_widget_media_impl(port, ptr, rust_vec_len, data_len),
86 => wire__crate__bridge__api__rust_api_refresh_widget_manifest_impl(port, ptr, rust_vec_len, data_len),
87 => wire__crate__bridge__api__rust_api_register_impl(port, ptr, rust_vec_len, data_len),
88 => wire__crate__bridge__api__rust_api_register_home_widget_impl(port, ptr, rust_vec_len, data_len),
89 => wire__crate__bridge__api__rust_api_register_passwordless_notification_impl(port, ptr, rust_vec_len, data_len),
90 => wire__crate__bridge__api__rust_api_register_passwordless_recovery_impl(port, ptr, rust_vec_len, data_len),
91 => wire__crate__bridge__api__rust_api_reload_configuration_impl(port, ptr, rust_vec_len, data_len),
92 => wire__crate__bridge__api__rust_api_remove_additional_user_impl(port, ptr, rust_vec_len, data_len),
93 => wire__crate__bridge__api__rust_api_remove_media_files_impl(port, ptr, rust_vec_len, data_len),
94 => wire__crate__bridge__api__rust_api_report_user_impl(port, ptr, rust_vec_len, data_len),
95 => wire__crate__bridge__api__rust_api_request_binary_impl(port, ptr, rust_vec_len, data_len),
96 => wire__crate__bridge__api__rust_api_request_contact_by_username_impl(port, ptr, rust_vec_len, data_len),
97 => wire__crate__bridge__api__rust_api_request_media_reupload_impl(port, ptr, rust_vec_len, data_len),
98 => wire__crate__bridge__api__rust_api_request_memories_upload_impl(port, ptr, rust_vec_len, data_len),
99 => wire__crate__bridge__api__rust_api_retransmit_all_messages_impl(port, ptr, rust_vec_len, data_len),
100 => wire__crate__bridge__api__rust_api_retry_pending_media_reuploads_impl(port, ptr, rust_vec_len, data_len),
101 => wire__crate__bridge__api__rust_api_reupload_pending_media_impl(port, ptr, rust_vec_len, data_len),
102 => wire__crate__bridge__api__rust_api_save_media_to_gallery_impl(port, ptr, rust_vec_len, data_len),
103 => wire__crate__bridge__api__rust_api_send_binary_impl(port, ptr, rust_vec_len, data_len),
104 => wire__crate__bridge__api__rust_api_send_contact_profile_impl(port, ptr, rust_vec_len, data_len),
105 => wire__crate__bridge__api__rust_api_send_encrypted_content_impl(port, ptr, rust_vec_len, data_len),
106 => wire__crate__bridge__api__rust_api_send_encrypted_content_to_group_impl(port, ptr, rust_vec_len, data_len),
107 => wire__crate__bridge__api__rust_api_send_media_to_groups_impl(port, ptr, rust_vec_len, data_len),
108 => wire__crate__bridge__api__rust_api_send_queued_message_impl(port, ptr, rust_vec_len, data_len),
109 => wire__crate__bridge__api__rust_api_send_text_message_impl(port, ptr, rust_vec_len, data_len),
110 => wire__crate__bridge__api__rust_api_send_typing_impl(port, ptr, rust_vec_len, data_len),
111 => wire__crate__bridge__api__rust_api_set_background_impl(port, ptr, rust_vec_len, data_len),
112 => wire__crate__bridge__api__rust_api_set_home_widget_groups_impl(port, ptr, rust_vec_len, data_len),
113 => wire__crate__bridge__api__rust_api_set_login_token_impl(port, ptr, rust_vec_len, data_len),
114 => wire__crate__bridge__api__rust_api_set_media_display_limit_impl(port, ptr, rust_vec_len, data_len),
115 => wire__crate__bridge__api__rust_api_set_media_requires_authentication_impl(port, ptr, rust_vec_len, data_len),
116 => wire__crate__bridge__api__rust_api_set_media_trim_impl(port, ptr, rust_vec_len, data_len),
117 => wire__crate__bridge__api__rust_api_set_network_available_impl(port, ptr, rust_vec_len, data_len),
118 => wire__crate__bridge__api__rust_api_store_media_impl(port, ptr, rust_vec_len, data_len),
119 => wire__crate__bridge__api__rust_api_submit_recovery_share_impl(port, ptr, rust_vec_len, data_len),
120 => wire__crate__bridge__api__rust_api_sync_widget_permissions_impl(port, ptr, rust_vec_len, data_len),
121 => wire__crate__bridge__api__rust_api_toggle_media_remove_audio_impl(port, ptr, rust_vec_len, data_len),
122 => wire__crate__bridge__api__rust_api_try_request_contact_by_id_impl(port, ptr, rust_vec_len, data_len),
123 => wire__crate__bridge__api__rust_api_unregister_home_widget_impl(port, ptr, rust_vec_len, data_len),
124 => wire__crate__bridge__api__rust_api_update_fcm_token_impl(port, ptr, rust_vec_len, data_len),
125 => wire__crate__bridge__api__rust_api_update_signed_pre_key_impl(port, ptr, rust_vec_len, data_len),
126 => wire__crate__bridge__api__rust_api_upload_pqc_pre_keys_impl(port, ptr, rust_vec_len, data_len),
127 => wire__crate__bridge__wrapper__app_database__rust_app_database_changes_impl(port, ptr, rust_vec_len, data_len),
128 => wire__crate__bridge__wrapper__app_database__rust_app_database_execute_impl(port, ptr, rust_vec_len, data_len),
129 => wire__crate__bridge__wrapper__app_database__rust_app_database_legacy_import_complete_impl(port, ptr, rust_vec_len, data_len),
130 => wire__crate__bridge__wrapper__app_database__rust_app_database_migrate_legacy_database_impl(port, ptr, rust_vec_len, data_len),
131 => wire__crate__bridge__wrapper__app_database__rust_app_database_select_impl(port, ptr, rust_vec_len, data_len),
132 => wire__crate__bridge__wrapper__backup__rust_backup_archive_create_backup_archive_impl(port, ptr, rust_vec_len, data_len),
133 => wire__crate__bridge__wrapper__backup__rust_backup_archive_get_backup_download_token_impl(port, ptr, rust_vec_len, data_len),
134 => wire__crate__bridge__wrapper__backup__rust_backup_archive_restore_backup_archive_impl(port, ptr, rust_vec_len, data_len),
135 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_id_impl(port, ptr, rust_vec_len, data_len),
136 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_password_keys_impl(port, ptr, rust_vec_len, data_len),
137 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_identity_backup_bytes_impl(port, ptr, rust_vec_len, data_len),
138 => wire__crate__bridge__wrapper__backup__rust_backup_identity_import_backup_password_keys_impl(port, ptr, rust_vec_len, data_len),
139 => wire__crate__bridge__wrapper__backup__rust_backup_identity_restore_identity_backup_impl(port, ptr, rust_vec_len, data_len),
140 => wire__crate__bridge__wrapper__backup__rust_backup_identity_set_backup_password_keys_impl(port, ptr, rust_vec_len, data_len),
141 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_decrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len),
142 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_encrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len),
143 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_signal_identity_impl(port, ptr, rust_vec_len, data_len),
144 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_user_id_impl(port, ptr, rust_vec_len, data_len),
145 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_serialized_impl(port, ptr, rust_vec_len, data_len),
146 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_identity_impl(port, ptr, rust_vec_len, data_len),
147 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_local_credentials_impl(port, ptr, rust_vec_len, data_len),
148 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_serialize_impl(port, ptr, rust_vec_len, data_len),
149 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_set_user_id_impl(port, ptr, rust_vec_len, data_len),
150 => wire__crate__bridge__wrapper__signal__rust_signal_decrypt_impl(port, ptr, rust_vec_len, data_len),
151 => wire__crate__bridge__wrapper__signal__rust_signal_encrypt_impl(port, ptr, rust_vec_len, data_len),
152 => wire__crate__bridge__wrapper__signal__rust_signal_generate_bundle_impl(port, ptr, rust_vec_len, data_len),
153 => wire__crate__bridge__wrapper__signal__rust_signal_generate_pqc_prekeys_impl(port, ptr, rust_vec_len, data_len),
154 => wire__crate__bridge__wrapper__signal__rust_signal_get_contact_public_key_impl(port, ptr, rust_vec_len, data_len),
155 => wire__crate__bridge__wrapper__signal__rust_signal_get_user_public_key_impl(port, ptr, rust_vec_len, data_len),
156 => wire__crate__bridge__wrapper__signal__rust_signal_process_prekey_bundle_impl(port, ptr, rust_vec_len, data_len),
157 => wire__crate__bridge__wrapper__rust_utils_generate_shares_impl(port, ptr, rust_vec_len, data_len),
158 => wire__crate__bridge__wrapper__rust_utils_recover_secret_impl(port, ptr, rust_vec_len, data_len),
159 => wire__crate__bridge__webxdc__send_update_impl(port, ptr, rust_vec_len, data_len),
160 => wire__crate__bridge__webxdc__serve_impl(port, ptr, rust_vec_len, data_len),
161 => wire__crate__bridge__groups__update_chat_deletion_time_impl(port, ptr, rust_vec_len, data_len),
162 => wire__crate__bridge__groups__update_group_name_impl(port, ptr, rust_vec_len, data_len),
163 => wire__crate__bridge__webxdc__updates_after_impl(port, ptr, rust_vec_len, data_len),
165 => wire__crate__bridge__user_config__user_config_api_create_impl(port, ptr, rust_vec_len, data_len),
166 => wire__crate__bridge__user_config__user_config_api_import_json_impl(port, ptr, rust_vec_len, data_len),
167 => wire__crate__bridge__user_config__user_config_api_load_impl(port, ptr, rust_vec_len, data_len),
168 => wire__crate__bridge__user_config__user_config_api_save_impl(port, ptr, rust_vec_len, data_len),
169 => wire__crate__bridge__user_config__user_config_api_update_impl(port, ptr, rust_vec_len, data_len),
5 => wire__crate__bridge__groups__can_restore_flames_impl(port, ptr, rust_vec_len, data_len),
6 => wire__crate__bridge__webxdc__catalog_impl(port, ptr, rust_vec_len, data_len),
7 => wire__crate__bridge__logging__clean_log_file_impl(port, ptr, rust_vec_len, data_len),
8 => wire__crate__bridge__logging__clear_log_file_impl(port, ptr, rust_vec_len, data_len),
9 => wire__crate__bridge__webxdc__create_instance_impl(port, ptr, rust_vec_len, data_len),
10 => wire__crate__bridge__groups__create_new_group_impl(port, ptr, rust_vec_len, data_len),
11 => wire__crate__bridge__webxdc__delete_instance_impl(port, ptr, rust_vec_len, data_len),
12 => wire__crate__bridge__groups__fetch_group_state_impl(port, ptr, rust_vec_len, data_len),
13 => wire__crate__bridge__groups__fetch_group_states_for_unjoined_groups_impl(port, ptr, rust_vec_len, data_len),
14 => wire__crate__bridge__groups__fetch_missing_group_public_keys_impl(port, ptr, rust_vec_len, data_len),
15 => wire__crate__bridge__groups__flame_state_impl(port, ptr, rust_vec_len, data_len),
16 => wire__crate__bridge__groups__flame_states_impl(port, ptr, rust_vec_len, data_len),
17 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_change_exclusion_for_contact_impl(port, ptr, rust_vec_len, data_len),
18 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_get_current_version_impl(port, ptr, rust_vec_len, data_len),
19 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_update_verification_state_for_user_impl(port, ptr, rust_vec_len, data_len),
20 => wire__crate__bridge__callbacks__init_flutter_callbacks_impl(port, ptr, rust_vec_len, data_len),
21 => wire__crate__bridge__initialize_twonly_flutter_impl(port, ptr, rust_vec_len, data_len),
22 => wire__crate__bridge__initialize_twonly_standalone_impl(port, ptr, rust_vec_len, data_len),
23 => wire__crate__bridge__webxdc__instance_impl(port, ptr, rust_vec_len, data_len),
24 => wire__crate__bridge__groups__leave_group_impl(port, ptr, rust_vec_len, data_len),
25 => wire__crate__bridge__logging__load_log_file_impl(port, ptr, rust_vec_len, data_len),
26 => wire__crate__bridge__groups__manage_admin_state_impl(port, ptr, rust_vec_len, data_len),
27 => wire__crate__bridge__webxdc__members_impl(port, ptr, rust_vec_len, data_len),
28 => wire__crate__bridge__webxdc__one_time_instances_impl(port, ptr, rust_vec_len, data_len),
29 => wire__crate__bridge__webxdc__prepare_bundle_impl(port, ptr, rust_vec_len, data_len),
30 => wire__crate__bridge__logging__read_last_log_lines_impl(port, ptr, rust_vec_len, data_len),
31 => wire__crate__bridge__webxdc__refresh_catalog_impl(port, ptr, rust_vec_len, data_len),
32 => wire__crate__bridge__groups__remove_member_from_group_impl(port, ptr, rust_vec_len, data_len),
33 => wire__crate__bridge__groups__restore_flames_impl(port, ptr, rust_vec_len, data_len),
34 => wire__crate__bridge__api__rust_api_abandon_media_impl(port, ptr, rust_vec_len, data_len),
35 => wire__crate__bridge__api__rust_api_add_additional_user_impl(port, ptr, rust_vec_len, data_len),
36 => wire__crate__bridge__api__rust_api_allocate_sequence_impl(port, ptr, rust_vec_len, data_len),
38 => wire__crate__bridge__api__rust_api_authentication_headers_impl(port, ptr, rust_vec_len, data_len),
40 => wire__crate__bridge__api__rust_api_change_username_impl(port, ptr, rust_vec_len, data_len),
41 => wire__crate__bridge__api__rust_api_check_for_deleted_usernames_impl(port, ptr, rust_vec_len, data_len),
42 => wire__crate__bridge__api__rust_api_check_for_passwordless_notification_impl(port, ptr, rust_vec_len, data_len),
43 => wire__crate__bridge__api__rust_api_clear_contact_request_notifications_impl(port, ptr, rust_vec_len, data_len),
44 => wire__crate__bridge__api__rust_api_clear_conversation_notifications_impl(port, ptr, rust_vec_len, data_len),
45 => wire__crate__bridge__api__rust_api_close_impl(port, ptr, rust_vec_len, data_len),
46 => wire__crate__bridge__api__rust_api_confirm_memories_upload_impl(port, ptr, rust_vec_len, data_len),
47 => wire__crate__bridge__api__rust_api_connect_impl(port, ptr, rust_vec_len, data_len),
48 => wire__crate__bridge__api__rust_api_connection_state_impl(port, ptr, rust_vec_len, data_len),
49 => wire__crate__bridge__api__rust_api_crop_media_transparent_borders_impl(port, ptr, rust_vec_len, data_len),
50 => wire__crate__bridge__api__rust_api_current_user_avatar_path_impl(port, ptr, rust_vec_len, data_len),
52 => wire__crate__bridge__api__rust_api_delete_account_impl(port, ptr, rust_vec_len, data_len),
53 => wire__crate__bridge__api__rust_api_delete_memory_impl(port, ptr, rust_vec_len, data_len),
54 => wire__crate__bridge__api__rust_api_delete_widget_media_impl(port, ptr, rust_vec_len, data_len),
55 => wire__crate__bridge__api__rust_api_disable_memories_backup_impl(port, ptr, rust_vec_len, data_len),
56 => wire__crate__bridge__api__rust_api_download_done_impl(port, ptr, rust_vec_len, data_len),
57 => wire__crate__bridge__api__rust_api_download_media_impl(port, ptr, rust_vec_len, data_len),
58 => wire__crate__bridge__api__rust_api_download_pending_media_impl(port, ptr, rust_vec_len, data_len),
59 => wire__crate__bridge__api__rust_api_ensure_avatar_png_impl(port, ptr, rust_vec_len, data_len),
60 => wire__crate__bridge__api__rust_api_establish_signal_session_impl(port, ptr, rust_vec_len, data_len),
61 => wire__crate__bridge__api__rust_api_events_impl(port, ptr, rust_vec_len, data_len),
62 => wire__crate__bridge__api__rust_api_finish_started_media_uploads_impl(port, ptr, rust_vec_len, data_len),
63 => wire__crate__bridge__api__rust_api_force_ipa_check_impl(port, ptr, rust_vec_len, data_len),
64 => wire__crate__bridge__api__rust_api_get_memories_url_impl(port, ptr, rust_vec_len, data_len),
65 => wire__crate__bridge__api__rust_api_get_memories_usage_impl(port, ptr, rust_vec_len, data_len),
66 => wire__crate__bridge__api__rust_api_get_plan_balance_impl(port, ptr, rust_vec_len, data_len),
67 => wire__crate__bridge__api__rust_api_get_proof_of_work_impl(port, ptr, rust_vec_len, data_len),
68 => wire__crate__bridge__api__rust_api_get_server_key_for_passwordless_recovery_impl(port, ptr, rust_vec_len, data_len),
69 => wire__crate__bridge__api__rust_api_get_user_by_id_impl(port, ptr, rust_vec_len, data_len),
70 => wire__crate__bridge__api__rust_api_get_user_data_impl(port, ptr, rust_vec_len, data_len),
71 => wire__crate__bridge__api__rust_api_get_user_id_from_username_impl(port, ptr, rust_vec_len, data_len),
72 => wire__crate__bridge__api__rust_api_hand_outbox_to_os_impl(port, ptr, rust_vec_len, data_len),
73 => wire__crate__bridge__api__rust_api_initialize_media_upload_impl(port, ptr, rust_vec_len, data_len),
74 => wire__crate__bridge__api__rust_api_initialize_memory_location_impl(port, ptr, rust_vec_len, data_len),
75 => wire__crate__bridge__api__rust_api_insert_and_send_additional_data_impl(port, ptr, rust_vec_len, data_len),
76 => wire__crate__bridge__api__rust_api_insert_and_send_ask_about_user_impl(port, ptr, rust_vec_len, data_len),
77 => wire__crate__bridge__api__rust_api_insert_and_send_contact_share_impl(port, ptr, rust_vec_len, data_len),
78 => wire__crate__bridge__api__rust_api_insert_and_send_text_impl(port, ptr, rust_vec_len, data_len),
79 => wire__crate__bridge__api__rust_api_ipa_purchase_impl(port, ptr, rust_vec_len, data_len),
80 => wire__crate__bridge__api__rust_api_load_plan_balance_impl(port, ptr, rust_vec_len, data_len),
81 => wire__crate__bridge__api__rust_api_media_size_limit_report_impl(port, ptr, rust_vec_len, data_len),
82 => wire__crate__bridge__api__rust_api_media_step_finished_impl(port, ptr, rust_vec_len, data_len),
83 => wire__crate__bridge__api__rust_api_notification_badge_count_impl(port, ptr, rust_vec_len, data_len),
84 => wire__crate__bridge__api__rust_api_notify_messages_opened_impl(port, ptr, rust_vec_len, data_len),
85 => wire__crate__bridge__api__rust_api_perform_passwordless_recovery_heartbeat_impl(port, ptr, rust_vec_len, data_len),
86 => wire__crate__bridge__api__rust_api_prerender_media_impl(port, ptr, rust_vec_len, data_len),
87 => wire__crate__bridge__api__rust_api_prewarm_memory_location_impl(port, ptr, rust_vec_len, data_len),
88 => wire__crate__bridge__api__rust_api_purge_media_temp_folder_impl(port, ptr, rust_vec_len, data_len),
89 => wire__crate__bridge__api__rust_api_purge_widget_media_impl(port, ptr, rust_vec_len, data_len),
90 => wire__crate__bridge__api__rust_api_refresh_widget_manifest_impl(port, ptr, rust_vec_len, data_len),
91 => wire__crate__bridge__api__rust_api_register_impl(port, ptr, rust_vec_len, data_len),
92 => wire__crate__bridge__api__rust_api_register_home_widget_impl(port, ptr, rust_vec_len, data_len),
93 => wire__crate__bridge__api__rust_api_register_passwordless_notification_impl(port, ptr, rust_vec_len, data_len),
94 => wire__crate__bridge__api__rust_api_register_passwordless_recovery_impl(port, ptr, rust_vec_len, data_len),
95 => wire__crate__bridge__api__rust_api_reload_configuration_impl(port, ptr, rust_vec_len, data_len),
96 => wire__crate__bridge__api__rust_api_remove_additional_user_impl(port, ptr, rust_vec_len, data_len),
97 => wire__crate__bridge__api__rust_api_remove_media_files_impl(port, ptr, rust_vec_len, data_len),
98 => wire__crate__bridge__api__rust_api_report_user_impl(port, ptr, rust_vec_len, data_len),
99 => wire__crate__bridge__api__rust_api_request_binary_impl(port, ptr, rust_vec_len, data_len),
100 => wire__crate__bridge__api__rust_api_request_contact_by_username_impl(port, ptr, rust_vec_len, data_len),
101 => wire__crate__bridge__api__rust_api_request_media_reupload_impl(port, ptr, rust_vec_len, data_len),
102 => wire__crate__bridge__api__rust_api_request_memories_upload_impl(port, ptr, rust_vec_len, data_len),
103 => wire__crate__bridge__api__rust_api_retransmit_all_messages_impl(port, ptr, rust_vec_len, data_len),
104 => wire__crate__bridge__api__rust_api_retry_pending_media_reuploads_impl(port, ptr, rust_vec_len, data_len),
105 => wire__crate__bridge__api__rust_api_reupload_pending_media_impl(port, ptr, rust_vec_len, data_len),
106 => wire__crate__bridge__api__rust_api_save_media_to_gallery_impl(port, ptr, rust_vec_len, data_len),
107 => wire__crate__bridge__api__rust_api_send_binary_impl(port, ptr, rust_vec_len, data_len),
108 => wire__crate__bridge__api__rust_api_send_contact_profile_impl(port, ptr, rust_vec_len, data_len),
109 => wire__crate__bridge__api__rust_api_send_encrypted_content_impl(port, ptr, rust_vec_len, data_len),
110 => wire__crate__bridge__api__rust_api_send_encrypted_content_to_group_impl(port, ptr, rust_vec_len, data_len),
111 => wire__crate__bridge__api__rust_api_send_media_to_groups_impl(port, ptr, rust_vec_len, data_len),
112 => wire__crate__bridge__api__rust_api_send_queued_message_impl(port, ptr, rust_vec_len, data_len),
113 => wire__crate__bridge__api__rust_api_send_text_message_impl(port, ptr, rust_vec_len, data_len),
114 => wire__crate__bridge__api__rust_api_send_typing_impl(port, ptr, rust_vec_len, data_len),
115 => wire__crate__bridge__api__rust_api_set_background_impl(port, ptr, rust_vec_len, data_len),
116 => wire__crate__bridge__api__rust_api_set_home_widget_groups_impl(port, ptr, rust_vec_len, data_len),
117 => wire__crate__bridge__api__rust_api_set_login_token_impl(port, ptr, rust_vec_len, data_len),
118 => wire__crate__bridge__api__rust_api_set_media_display_limit_impl(port, ptr, rust_vec_len, data_len),
119 => wire__crate__bridge__api__rust_api_set_media_requires_authentication_impl(port, ptr, rust_vec_len, data_len),
120 => wire__crate__bridge__api__rust_api_set_media_trim_impl(port, ptr, rust_vec_len, data_len),
121 => wire__crate__bridge__api__rust_api_set_network_available_impl(port, ptr, rust_vec_len, data_len),
122 => wire__crate__bridge__api__rust_api_store_media_impl(port, ptr, rust_vec_len, data_len),
123 => wire__crate__bridge__api__rust_api_submit_recovery_share_impl(port, ptr, rust_vec_len, data_len),
124 => wire__crate__bridge__api__rust_api_sync_widget_permissions_impl(port, ptr, rust_vec_len, data_len),
125 => wire__crate__bridge__api__rust_api_toggle_media_remove_audio_impl(port, ptr, rust_vec_len, data_len),
126 => wire__crate__bridge__api__rust_api_try_request_contact_by_id_impl(port, ptr, rust_vec_len, data_len),
127 => wire__crate__bridge__api__rust_api_unregister_home_widget_impl(port, ptr, rust_vec_len, data_len),
128 => wire__crate__bridge__api__rust_api_update_fcm_token_impl(port, ptr, rust_vec_len, data_len),
129 => wire__crate__bridge__api__rust_api_update_signed_pre_key_impl(port, ptr, rust_vec_len, data_len),
130 => wire__crate__bridge__api__rust_api_upload_pqc_pre_keys_impl(port, ptr, rust_vec_len, data_len),
131 => wire__crate__bridge__wrapper__app_database__rust_app_database_changes_impl(port, ptr, rust_vec_len, data_len),
132 => wire__crate__bridge__wrapper__app_database__rust_app_database_execute_impl(port, ptr, rust_vec_len, data_len),
133 => wire__crate__bridge__wrapper__app_database__rust_app_database_legacy_import_complete_impl(port, ptr, rust_vec_len, data_len),
134 => wire__crate__bridge__wrapper__app_database__rust_app_database_migrate_legacy_database_impl(port, ptr, rust_vec_len, data_len),
135 => wire__crate__bridge__wrapper__app_database__rust_app_database_select_impl(port, ptr, rust_vec_len, data_len),
136 => wire__crate__bridge__wrapper__backup__rust_backup_archive_create_backup_archive_impl(port, ptr, rust_vec_len, data_len),
137 => wire__crate__bridge__wrapper__backup__rust_backup_archive_get_backup_download_token_impl(port, ptr, rust_vec_len, data_len),
138 => wire__crate__bridge__wrapper__backup__rust_backup_archive_restore_backup_archive_impl(port, ptr, rust_vec_len, data_len),
139 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_id_impl(port, ptr, rust_vec_len, data_len),
140 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_password_keys_impl(port, ptr, rust_vec_len, data_len),
141 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_identity_backup_bytes_impl(port, ptr, rust_vec_len, data_len),
142 => wire__crate__bridge__wrapper__backup__rust_backup_identity_import_backup_password_keys_impl(port, ptr, rust_vec_len, data_len),
143 => wire__crate__bridge__wrapper__backup__rust_backup_identity_restore_identity_backup_impl(port, ptr, rust_vec_len, data_len),
144 => wire__crate__bridge__wrapper__backup__rust_backup_identity_set_backup_password_keys_impl(port, ptr, rust_vec_len, data_len),
145 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_decrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len),
146 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_encrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len),
147 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_signal_identity_impl(port, ptr, rust_vec_len, data_len),
148 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_user_id_impl(port, ptr, rust_vec_len, data_len),
149 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_serialized_impl(port, ptr, rust_vec_len, data_len),
150 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_identity_impl(port, ptr, rust_vec_len, data_len),
151 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_local_credentials_impl(port, ptr, rust_vec_len, data_len),
152 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_serialize_impl(port, ptr, rust_vec_len, data_len),
153 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_set_user_id_impl(port, ptr, rust_vec_len, data_len),
154 => wire__crate__bridge__wrapper__signal__rust_signal_decrypt_impl(port, ptr, rust_vec_len, data_len),
155 => wire__crate__bridge__wrapper__signal__rust_signal_encrypt_impl(port, ptr, rust_vec_len, data_len),
156 => wire__crate__bridge__wrapper__signal__rust_signal_generate_bundle_impl(port, ptr, rust_vec_len, data_len),
157 => wire__crate__bridge__wrapper__signal__rust_signal_generate_pqc_prekeys_impl(port, ptr, rust_vec_len, data_len),
158 => wire__crate__bridge__wrapper__signal__rust_signal_get_contact_public_key_impl(port, ptr, rust_vec_len, data_len),
159 => wire__crate__bridge__wrapper__signal__rust_signal_get_user_public_key_impl(port, ptr, rust_vec_len, data_len),
160 => wire__crate__bridge__wrapper__signal__rust_signal_process_prekey_bundle_impl(port, ptr, rust_vec_len, data_len),
161 => wire__crate__bridge__wrapper__rust_utils_generate_shares_impl(port, ptr, rust_vec_len, data_len),
162 => wire__crate__bridge__wrapper__rust_utils_recover_secret_impl(port, ptr, rust_vec_len, data_len),
163 => wire__crate__bridge__webxdc__send_update_impl(port, ptr, rust_vec_len, data_len),
164 => wire__crate__bridge__webxdc__serve_impl(port, ptr, rust_vec_len, data_len),
165 => wire__crate__bridge__groups__update_chat_deletion_time_impl(port, ptr, rust_vec_len, data_len),
166 => wire__crate__bridge__groups__update_group_name_impl(port, ptr, rust_vec_len, data_len),
167 => wire__crate__bridge__webxdc__updates_after_impl(port, ptr, rust_vec_len, data_len),
169 => wire__crate__bridge__user_config__user_config_api_create_impl(port, ptr, rust_vec_len, data_len),
170 => wire__crate__bridge__user_config__user_config_api_import_json_impl(port, ptr, rust_vec_len, data_len),
171 => wire__crate__bridge__user_config__user_config_api_load_impl(port, ptr, rust_vec_len, data_len),
172 => wire__crate__bridge__user_config__user_config_api_save_impl(port, ptr, rust_vec_len, data_len),
173 => wire__crate__bridge__user_config__user_config_api_update_impl(port, ptr, rust_vec_len, data_len),
_ => unreachable!(),
}
}
@ -7804,17 +8008,17 @@ fn pde_ffi_dispatcher_sync_impl(
) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse {
// Codec=Pde (Serialization + dispatch), see doc to use other codecs
match func_id {
33 => wire__crate__bridge__api__rust_api_api_base_url_impl(ptr, rust_vec_len, data_len),
35 => wire__crate__bridge__api__rust_api_avatar_png_path_impl(ptr, rust_vec_len, data_len),
47 => {
37 => wire__crate__bridge__api__rust_api_api_base_url_impl(ptr, rust_vec_len, data_len),
39 => wire__crate__bridge__api__rust_api_avatar_png_path_impl(ptr, rust_vec_len, data_len),
51 => {
wire__crate__bridge__api__rust_api_decode_avatar_svg_impl(ptr, rust_vec_len, data_len)
}
164 => wire__crate__bridge__user_config__user_config_api_clone_impl(
168 => wire__crate__bridge__user_config__user_config_api_clone_impl(
ptr,
rust_vec_len,
data_len,
),
170 => wire__crate__bridge__logging__write_log_impl(ptr, rust_vec_len, data_len),
174 => wire__crate__bridge__logging__write_log_impl(ptr, rust_vec_len, data_len),
_ => unreachable!(),
}
}
@ -7916,6 +8120,28 @@ impl flutter_rust_bridge::IntoIntoDart<crate::keys::backup_password_keys::Backup
}
}
// 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
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>> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
@ -8998,6 +9234,15 @@ impl SseEncode for f64 {
}
}
impl SseEncode for crate::bridge::FlameState {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
<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 {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {}
@ -9270,6 +9515,16 @@ impl SseEncode for Vec<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>)> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
@ -9552,6 +9807,14 @@ impl SseEncode for (Vec<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>) {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {

View file

@ -13,7 +13,7 @@ use crate::api::messages::incoming::messages::{
use crate::api::messages::outgoing::send_c2c_message_to_contact;
use crate::api::proto::client::encrypted_content::GroupJoin;
use crate::api::proto::client::{
encrypted_appended_group_state, encrypted_content, EncryptedAppendedGroupState,
self as proto, encrypted_appended_group_state, encrypted_content, EncryptedAppendedGroupState,
EncryptedContent, EncryptedGroupState,
};
use crate::api::proto::http_requests::{append_group_state, AppendGroupState, NewGroupState};
@ -26,7 +26,7 @@ use crate::database::app::tables::{
};
use crate::error::{Result, TwonlyError};
use crate::services::messages::MessageService;
use crate::utils::{current_time, new_uuid_v4};
use crate::utils::{current_time, new_uuid_v4, start_of_local_day};
use model::GroupRecord;
use prost::Message;
use rand::{RngCore, SeedableRng};
@ -150,35 +150,46 @@ impl GroupService {
pub async fn on_connected(&self) -> Result<()> {
self.fetch_group_states_for_unjoined_groups().await?;
self.fetch_missing_group_public_keys(None, false).await?;
self.sync_flame_counters().await
self.sync_flame_counters(None).await
}
async fn sync_flame_counters(&self) -> Result<()> {
/// Tells each peer where their shared streak stands. Normally at most once
/// per day per chat; `force_for_group` overrides that guard and asks the
/// peer to take our value, which is how a restore reaches the other side.
pub(crate) async fn sync_flame_counters(&self, force_for_group: Option<&str>) -> Result<()> {
let db = self.ctx.app_db.read().await.clone();
let groups = Group::flame_sync_candidates(&db.pool).await?;
let Some(best_friend) = groups.iter().max_by_key(|group| group.total_media_counter) else {
if groups.is_empty() {
return Ok(());
};
}
let best_friend_id = best_friend.group_id.clone();
let best_friend_id = Group::best_friend_group_id(&db.pool).await?;
let states = Group::flame_states(&db.pool, None).await?;
let now = current_time().timestamp();
let start_today = now - now.rem_euclid(86_400);
let start_today = start_of_local_day()?;
for group in groups {
let Some(changed) = group.last_flame_counter_change else {
continue;
};
let forced = force_for_group == Some(group.group_id.as_str());
if changed < start_today
|| group
if changed < start_today {
continue;
}
if !forced
&& group
.last_flame_sync
.is_some_and(|sync| sync >= start_today)
{
continue;
}
if group.flame_counter <= 2 && group.group_id != best_friend_id {
let is_best_friend = best_friend_id.as_deref() == Some(group.group_id.as_str());
// The stored column can still hold a streak that today's derivation
// has already let expire, so send what the user actually sees.
let counter = states.get(&group.group_id).map_or(0, |state| state.counter);
if counter <= 2 && !is_best_friend {
continue;
}
@ -187,10 +198,10 @@ impl GroupService {
group.group_id.clone(),
EncryptedContent {
flame_sync: Some(encrypted_content::FlameSync {
flame_counter: group.flame_counter,
flame_counter: counter,
last_flame_counter_change: changed * 1000,
best_friend: group.group_id == best_friend_id,
force_update: false,
best_friend: is_best_friend,
force_update: forced,
}),
..Default::default()
}
@ -205,6 +216,45 @@ impl GroupService {
Ok(())
}
/// Puts a lost streak back, tells the chat it happened, and pushes the
/// restored value to the peer so both sides agree again.
pub(crate) async fn restore_flames(&self, group_id: String) -> Result<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 {
Self { ctx: ctx.clone() }
}

View file

@ -256,6 +256,9 @@ pub struct UserConfig {
#[serde(default = "defaults::true_value")]
#[frb(non_final)]
pub show_restore_flame: bool,
/// Unused: the best friend is derived from `total_media_counter` whenever
/// it is needed (`Group::best_friend_group_id`). Kept so configurations
/// written by older versions still deserialize.
#[frb(non_final)]
pub my_best_friend_group_id: Option<String>,
#[serde(default, with = "optional_datetime")]

View file

@ -7,6 +7,8 @@ use chrono::{DateTime, Local, TimeZone};
use std::sync::Arc;
use tokio::sync::{RwLock, RwLockReadGuard};
use crate::error::{Result, TwonlyError};
pub(crate) fn milliseconds_to_seconds(timestamp: i64) -> i64 {
timestamp.div_euclid(1_000)
}
@ -22,6 +24,22 @@ pub(crate) fn is_today(timestamp: i64) -> bool {
.is_some_and(|date| date.date_naive() == current_time().date_naive())
}
/// Midnight of the current day in the user's timezone. Flame counters roll over
/// at the local day boundary, so every comparison against "today" has to start
/// here instead of at a UTC boundary.
pub(crate) fn start_of_local_day() -> Result<i64> {
Ok(Local
.from_local_datetime(
&current_time()
.date_naive()
.and_hms_opt(0, 0, 0)
.ok_or_else(|| TwonlyError::Generic("invalid local date".into()))?,
)
.earliest()
.ok_or_else(|| TwonlyError::Generic("local day has no midnight".into()))?
.timestamp())
}
pub(crate) fn new_uuid_v4() -> String {
uuid::Uuid::new_v4().to_string()
}

View file

@ -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();
});
}