diff --git a/android/build/reports/problems/problems-report.html b/android/build/reports/problems/problems-report.html
new file mode 100644
index 00000000..e633b03e
--- /dev/null
+++ b/android/build/reports/problems/problems-report.html
@@ -0,0 +1,663 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Gradle Configuration Cache
+
+
+
+
+
+
+ Loading...
+
+
+
+
+
+
+
diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift
index d5bb7aef..c0f7cd81 100644
--- a/ios/Runner/AppDelegate.swift
+++ b/ios/Runner/AppDelegate.swift
@@ -81,12 +81,13 @@ import workmanager_apple
}
/// Withdraws only the native notifications whose message IDs Dart reports as
-/// opened. The notification service extension's final alert keeps APNs' request
-/// identifier, so both the identifier and our `notification_id` user-info field
-/// have to be considered.
+/// opened, and keeps the app icon badge in step with them. The notification
+/// service extension's final alert keeps APNs' request identifier, so both the
+/// identifier and our `notification_id` user-info field have to be considered.
class NativeNotificationChannel {
private static let channelName = "eu.twonly/notificationTap"
private static let notificationIdsKey = "notification_ids"
+ private static let badgeCountKey = "badge_count"
static func register(with registry: FlutterPluginRegistry) {
guard let registrar = registry.registrar(forPlugin: "TwonlyNativeNotifications") else {
@@ -97,23 +98,67 @@ class NativeNotificationChannel {
binaryMessenger: registrar.messenger()
)
channel.setMethodCallHandler { call, result in
- guard call.method == "cancelNotifications" else {
+ switch call.method {
+ case "cancelNotifications":
+ guard
+ let arguments = call.arguments as? [String: Any],
+ let values = arguments[notificationIdsKey] as? [String]
+ else {
+ result(
+ FlutterError(
+ code: "invalid_notification_ids",
+ message: "notification_ids must be a list of strings",
+ details: nil
+ ))
+ return
+ }
+ removeNotifications(Set(values), completion: result)
+ case "setBadgeCount":
+ guard
+ let arguments = call.arguments as? [String: Any],
+ let count = arguments[badgeCountKey] as? Int
+ else {
+ result(
+ FlutterError(
+ code: "invalid_badge_count",
+ message: "badge_count must be an integer",
+ details: nil
+ ))
+ return
+ }
+ setBadgeCount(count, completion: result)
+ default:
result(FlutterMethodNotImplemented)
- return
}
- guard
- let arguments = call.arguments as? [String: Any],
- let values = arguments[notificationIdsKey] as? [String]
- else {
- result(
- FlutterError(
- code: "invalid_notification_ids",
- message: "notification_ids must be a list of strings",
- details: nil
- ))
- return
+ }
+ }
+
+ /// iOS only takes an app icon badge from a notification payload, so a badge
+ /// set by the notification service extension survives until the app itself
+ /// overwrites it. Dart pushes the pending event count here whenever the
+ /// notification outbox changes and on every resume.
+ private static func setBadgeCount(_ count: Int, completion: @escaping FlutterResult) {
+ let badge = max(0, count)
+ guard #available(iOS 16.0, *) else {
+ DispatchQueue.main.async {
+ UIApplication.shared.applicationIconBadgeNumber = badge
+ completion(nil)
+ }
+ return
+ }
+ UNUserNotificationCenter.current().setBadgeCount(badge) { error in
+ DispatchQueue.main.async {
+ if let error {
+ completion(
+ FlutterError(
+ code: "badge_count_failed",
+ message: error.localizedDescription,
+ details: nil
+ ))
+ } else {
+ completion(nil)
+ }
}
- removeNotifications(Set(values), completion: result)
}
}
diff --git a/lib/app.dart b/lib/app.dart
index db0bc7ae..b6786418 100644
--- a/lib/app.dart
+++ b/lib/app.dart
@@ -17,6 +17,7 @@ import 'package:twonly/src/model/json/onboarding_state.model.dart';
import 'package:twonly/src/providers/routing.provider.dart';
import 'package:twonly/src/providers/settings.provider.dart';
import 'package:twonly/src/services/intent/links.intent.dart';
+import 'package:twonly/src/services/notifications/native.notifications.dart';
import 'package:twonly/src/utils/keyvalue.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/pow.dart';
@@ -63,6 +64,10 @@ class _AppState extends State with WidgetsBindingObserver {
unawaited(
rust_api.RustApi.setBackground(inBackground: false),
);
+ // The notification service extension wrote to the outbox while the app
+ // was suspended, and its Rust change broadcast never reached this
+ // process, so the badge has to be re-read on the way back in.
+ unawaited(NativeNotificationService.refreshBadgeCount());
}
} else if (state == AppLifecycleState.paused) {
_wasPaused = true;
diff --git a/lib/core/bridge/api.dart b/lib/core/bridge/api.dart
index eb9016b3..848fabbe 100644
--- a/lib/core/bridge/api.dart
+++ b/lib/core/bridge/api.dart
@@ -293,6 +293,22 @@ class RustApi {
alreadyReceivedMessageIds: alreadyReceivedMessageIds,
);
+ /// Acknowledges the pending contact-request notifications and returns the
+ /// native notification IDs to withdraw.
+ static Future> clearContactRequestNotifications() => RustLib
+ .instance
+ .api
+ .crateBridgeApiRustApiClearContactRequestNotifications();
+
+ /// Acknowledges every pending notification of a conversation the user just
+ /// opened and returns the native notification IDs to withdraw.
+ static Future> clearConversationNotifications({
+ required String conversationId,
+ }) =>
+ RustLib.instance.api.crateBridgeApiRustApiClearConversationNotifications(
+ conversationId: conversationId,
+ );
+
static Future close() =>
RustLib.instance.api.crateBridgeApiRustApiClose();
@@ -432,6 +448,12 @@ class RustApi {
static Future loadPlanBalance() =>
RustLib.instance.api.crateBridgeApiRustApiLoadPlanBalance();
+ /// The number of pending notification events. iOS cannot derive its app
+ /// icon badge from the delivered alerts, so the running app pushes this
+ /// into `UNUserNotificationCenter` whenever the outbox changes.
+ static Future notificationBadgeCount() =>
+ RustLib.instance.api.crateBridgeApiRustApiNotificationBadgeCount();
+
static Future notifyMessagesOpened({
required PlatformInt64 contactId,
required List messageIds,
diff --git a/lib/core/bridge/callbacks/log.dart b/lib/core/bridge/callbacks/log.dart
new file mode 100644
index 00000000..42fb1c69
--- /dev/null
+++ b/lib/core/bridge/callbacks/log.dart
@@ -0,0 +1,14 @@
+// This file is automatically generated, so please do not edit it.
+// @generated by `flutter_rust_bridge`@ 2.12.0.
+
+// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
+
+import '../../frb_generated.dart';
+import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
+
+// These functions are ignored because they are not marked as `pub`: `set_dart_sink`
+// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `DartWriter`
+// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `flush`, `make_writer`, `write`
+
+Future stripAnsi({required String input}) =>
+ RustLib.instance.api.crateBridgeCallbacksLogStripAnsi(input: input);
diff --git a/lib/core/bridge/groups.dart b/lib/core/bridge/groups.dart
index e1126d60..a4d8e5e1 100644
--- a/lib/core/bridge/groups.dart
+++ b/lib/core/bridge/groups.dart
@@ -25,17 +25,20 @@ Future addHiddenContact({required PlatformInt64 contactId}) => RustLib
Future fetchGroupStatesForUnjoinedGroups() =>
RustLib.instance.api.crateBridgeGroupsFetchGroupStatesForUnjoinedGroups();
-Future fetchMissingGroupPublicKeys() =>
- RustLib.instance.api.crateBridgeGroupsFetchMissingGroupPublicKeys();
+Future fetchMissingGroupPublicKeys({
+ String? groupId,
+ required bool force,
+}) => RustLib.instance.api.crateBridgeGroupsFetchMissingGroupPublicKeys(
+ groupId: groupId,
+ force: force,
+);
Future manageAdminState({
required String groupId,
- required List groupPublicKey,
required PlatformInt64 contactId,
required bool remove,
}) => RustLib.instance.api.crateBridgeGroupsManageAdminState(
groupId: groupId,
- groupPublicKey: groupPublicKey,
contactId: contactId,
remove: remove,
);
@@ -66,11 +69,9 @@ Future addNewGroupMembers({
Future removeMemberFromGroup({
required String groupId,
- required List groupPublicKey,
required PlatformInt64 contactId,
}) => RustLib.instance.api.crateBridgeGroupsRemoveMemberFromGroup(
groupId: groupId,
- groupPublicKey: groupPublicKey,
contactId: contactId,
);
diff --git a/lib/core/frb_generated.dart b/lib/core/frb_generated.dart
index c681f6ec..2dce09d2 100644
--- a/lib/core/frb_generated.dart
+++ b/lib/core/frb_generated.dart
@@ -7,6 +7,7 @@ import 'api/server/prekeys.dart';
import 'bridge.dart';
import 'bridge/api.dart';
import 'bridge/callbacks.dart';
+import 'bridge/callbacks/log.dart';
import 'bridge/groups.dart';
import 'bridge/user_config.dart';
import 'bridge/wrapper.dart';
@@ -84,7 +85,7 @@ class RustLib extends BaseEntrypoint {
String get codegenVersion => '2.12.0';
@override
- int get rustContentHash => -1780439173;
+ int get rustContentHash => 463166747;
static const kDefaultExternalLibraryLoaderConfig =
ExternalLibraryLoaderConfig(
@@ -114,7 +115,10 @@ abstract class RustLibApi extends BaseApi {
Future crateBridgeGroupsFetchGroupStatesForUnjoinedGroups();
- Future crateBridgeGroupsFetchMissingGroupPublicKeys();
+ Future crateBridgeGroupsFetchMissingGroupPublicKeys({
+ String? groupId,
+ required bool force,
+ });
Future
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryChangeExclusionForContact({
@@ -157,14 +161,12 @@ abstract class RustLibApi extends BaseApi {
Future crateBridgeGroupsManageAdminState({
required String groupId,
- required List groupPublicKey,
required PlatformInt64 contactId,
required bool remove,
});
Future crateBridgeGroupsRemoveMemberFromGroup({
required String groupId,
- required List groupPublicKey,
required PlatformInt64 contactId,
});
@@ -187,6 +189,12 @@ abstract class RustLibApi extends BaseApi {
required Int64List alreadyReceivedMessageIds,
});
+ Future> crateBridgeApiRustApiClearContactRequestNotifications();
+
+ Future> crateBridgeApiRustApiClearConversationNotifications({
+ required String conversationId,
+ });
+
Future crateBridgeApiRustApiClose();
Future crateBridgeApiRustApiConfirmMemoriesUpload({
@@ -279,6 +287,8 @@ abstract class RustLibApi extends BaseApi {
Future crateBridgeApiRustApiLoadPlanBalance();
+ Future crateBridgeApiRustApiNotificationBadgeCount();
+
Future crateBridgeApiRustApiNotifyMessagesOpened({
required PlatformInt64 contactId,
required List messageIds,
@@ -541,6 +551,8 @@ abstract class RustLibApi extends BaseApi {
required int threshold,
});
+ Future crateBridgeCallbacksLogStripAnsi({required String input});
+
Future crateBridgeGroupsUpdateChatDeletionTime({
required String groupId,
required PlatformInt64 deleteMessagesAfterMilliseconds,
@@ -753,11 +765,16 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
);
@override
- Future crateBridgeGroupsFetchMissingGroupPublicKeys() {
+ Future crateBridgeGroupsFetchMissingGroupPublicKeys({
+ String? groupId,
+ required bool force,
+ }) {
return handler.executeNormal(
NormalTask(
callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding);
+ sse_encode_opt_String(groupId, serializer);
+ sse_encode_bool(force, serializer);
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
@@ -770,7 +787,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
decodeErrorData: sse_decode_AnyhowException,
),
constMeta: kCrateBridgeGroupsFetchMissingGroupPublicKeysConstMeta,
- argValues: [],
+ argValues: [groupId, force],
apiImpl: this,
),
);
@@ -779,7 +796,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
TaskConstMeta get kCrateBridgeGroupsFetchMissingGroupPublicKeysConstMeta =>
const TaskConstMeta(
debugName: "fetch_missing_group_public_keys",
- argNames: [],
+ argNames: ["groupId", "force"],
);
@override
@@ -1082,7 +1099,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
@override
Future crateBridgeGroupsManageAdminState({
required String groupId,
- required List groupPublicKey,
required PlatformInt64 contactId,
required bool remove,
}) {
@@ -1091,7 +1107,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_String(groupId, serializer);
- sse_encode_list_prim_u_8_loose(groupPublicKey, serializer);
sse_encode_i_64(contactId, serializer);
sse_encode_bool(remove, serializer);
pdeCallFfi(
@@ -1106,7 +1121,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
decodeErrorData: sse_decode_AnyhowException,
),
constMeta: kCrateBridgeGroupsManageAdminStateConstMeta,
- argValues: [groupId, groupPublicKey, contactId, remove],
+ argValues: [groupId, contactId, remove],
apiImpl: this,
),
);
@@ -1115,13 +1130,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
TaskConstMeta get kCrateBridgeGroupsManageAdminStateConstMeta =>
const TaskConstMeta(
debugName: "manage_admin_state",
- argNames: ["groupId", "groupPublicKey", "contactId", "remove"],
+ argNames: ["groupId", "contactId", "remove"],
);
@override
Future crateBridgeGroupsRemoveMemberFromGroup({
required String groupId,
- required List groupPublicKey,
required PlatformInt64 contactId,
}) {
return handler.executeNormal(
@@ -1129,7 +1143,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_String(groupId, serializer);
- sse_encode_list_prim_u_8_loose(groupPublicKey, serializer);
sse_encode_i_64(contactId, serializer);
pdeCallFfi(
generalizedFrbRustBinding,
@@ -1143,7 +1156,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
decodeErrorData: sse_decode_AnyhowException,
),
constMeta: kCrateBridgeGroupsRemoveMemberFromGroupConstMeta,
- argValues: [groupId, groupPublicKey, contactId],
+ argValues: [groupId, contactId],
apiImpl: this,
),
);
@@ -1152,7 +1165,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
TaskConstMeta get kCrateBridgeGroupsRemoveMemberFromGroupConstMeta =>
const TaskConstMeta(
debugName: "remove_member_from_group",
- argNames: ["groupId", "groupPublicKey", "contactId"],
+ argNames: ["groupId", "contactId"],
);
@override
@@ -1358,7 +1371,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
);
@override
- Future crateBridgeApiRustApiClose() {
+ Future> crateBridgeApiRustApiClearContactRequestNotifications() {
return handler.executeNormal(
NormalTask(
callFfi: (port_) {
@@ -1370,6 +1383,73 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
port: port_,
);
},
+ codec: SseCodec(
+ decodeSuccessData: sse_decode_list_String,
+ decodeErrorData: sse_decode_AnyhowException,
+ ),
+ constMeta:
+ kCrateBridgeApiRustApiClearContactRequestNotificationsConstMeta,
+ argValues: [],
+ apiImpl: this,
+ ),
+ );
+ }
+
+ TaskConstMeta
+ get kCrateBridgeApiRustApiClearContactRequestNotificationsConstMeta =>
+ const TaskConstMeta(
+ debugName: "rust_api_clear_contact_request_notifications",
+ argNames: [],
+ );
+
+ @override
+ Future> crateBridgeApiRustApiClearConversationNotifications({
+ required String conversationId,
+ }) {
+ return handler.executeNormal(
+ NormalTask(
+ callFfi: (port_) {
+ final serializer = SseSerializer(generalizedFrbRustBinding);
+ sse_encode_String(conversationId, serializer);
+ pdeCallFfi(
+ generalizedFrbRustBinding,
+ serializer,
+ funcId: 23,
+ port: port_,
+ );
+ },
+ codec: SseCodec(
+ decodeSuccessData: sse_decode_list_String,
+ decodeErrorData: sse_decode_AnyhowException,
+ ),
+ constMeta:
+ kCrateBridgeApiRustApiClearConversationNotificationsConstMeta,
+ argValues: [conversationId],
+ apiImpl: this,
+ ),
+ );
+ }
+
+ TaskConstMeta
+ get kCrateBridgeApiRustApiClearConversationNotificationsConstMeta =>
+ const TaskConstMeta(
+ debugName: "rust_api_clear_conversation_notifications",
+ argNames: ["conversationId"],
+ );
+
+ @override
+ Future crateBridgeApiRustApiClose() {
+ return handler.executeNormal(
+ NormalTask(
+ callFfi: (port_) {
+ final serializer = SseSerializer(generalizedFrbRustBinding);
+ pdeCallFfi(
+ generalizedFrbRustBinding,
+ serializer,
+ funcId: 24,
+ port: port_,
+ );
+ },
codec: SseCodec(
decodeSuccessData: sse_decode_unit,
decodeErrorData: sse_decode_AnyhowException,
@@ -1398,7 +1478,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 23,
+ funcId: 25,
port: port_,
);
},
@@ -1428,7 +1508,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 24,
+ funcId: 26,
port: port_,
);
},
@@ -1458,7 +1538,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 25,
+ funcId: 27,
port: port_,
);
},
@@ -1488,7 +1568,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 26,
+ funcId: 28,
port: port_,
);
},
@@ -1519,7 +1599,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 27,
+ funcId: 29,
port: port_,
);
},
@@ -1549,7 +1629,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 28,
+ funcId: 30,
port: port_,
);
},
@@ -1580,7 +1660,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 29,
+ funcId: 31,
port: port_,
);
},
@@ -1611,7 +1691,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 30,
+ funcId: 32,
port: port_,
);
},
@@ -1641,7 +1721,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 31,
+ funcId: 33,
port: port_,
);
},
@@ -1676,7 +1756,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 32,
+ funcId: 34,
port: port_,
);
},
@@ -1709,7 +1789,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 33,
+ funcId: 35,
port: port_,
);
},
@@ -1741,7 +1821,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 34,
+ funcId: 36,
port: port_,
);
},
@@ -1776,7 +1856,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 35,
+ funcId: 37,
port: port_,
);
},
@@ -1806,7 +1886,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 36,
+ funcId: 38,
port: port_,
);
},
@@ -1836,7 +1916,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 37,
+ funcId: 39,
port: port_,
);
},
@@ -1866,7 +1946,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 38,
+ funcId: 40,
port: port_,
);
},
@@ -1907,7 +1987,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 39,
+ funcId: 41,
port: port_,
);
},
@@ -1954,7 +2034,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 40,
+ funcId: 42,
port: port_,
);
},
@@ -1987,7 +2067,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 41,
+ funcId: 43,
port: port_,
);
},
@@ -2020,7 +2100,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 42,
+ funcId: 44,
port: port_,
);
},
@@ -2057,7 +2137,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 43,
+ funcId: 45,
port: port_,
);
},
@@ -2093,7 +2173,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 44,
+ funcId: 46,
port: port_,
);
},
@@ -2128,7 +2208,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 45,
+ funcId: 47,
port: port_,
);
},
@@ -2165,7 +2245,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 46,
+ funcId: 48,
port: port_,
);
},
@@ -2202,7 +2282,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 47,
+ funcId: 49,
port: port_,
);
},
@@ -2232,7 +2312,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 48,
+ funcId: 50,
port: port_,
);
},
@@ -2253,6 +2333,36 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
argNames: [],
);
+ @override
+ Future crateBridgeApiRustApiNotificationBadgeCount() {
+ return handler.executeNormal(
+ NormalTask(
+ callFfi: (port_) {
+ final serializer = SseSerializer(generalizedFrbRustBinding);
+ pdeCallFfi(
+ generalizedFrbRustBinding,
+ serializer,
+ funcId: 51,
+ port: port_,
+ );
+ },
+ codec: SseCodec(
+ decodeSuccessData: sse_decode_i_64,
+ decodeErrorData: sse_decode_AnyhowException,
+ ),
+ constMeta: kCrateBridgeApiRustApiNotificationBadgeCountConstMeta,
+ argValues: [],
+ apiImpl: this,
+ ),
+ );
+ }
+
+ TaskConstMeta get kCrateBridgeApiRustApiNotificationBadgeCountConstMeta =>
+ const TaskConstMeta(
+ debugName: "rust_api_notification_badge_count",
+ argNames: [],
+ );
+
@override
Future crateBridgeApiRustApiNotifyMessagesOpened({
required PlatformInt64 contactId,
@@ -2267,7 +2377,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 49,
+ funcId: 52,
port: port_,
);
},
@@ -2297,7 +2407,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 50,
+ funcId: 53,
port: port_,
);
},
@@ -2332,7 +2442,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 51,
+ funcId: 54,
port: port_,
);
},
@@ -2371,7 +2481,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 52,
+ funcId: 55,
port: port_,
);
},
@@ -2410,7 +2520,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 53,
+ funcId: 56,
port: port_,
);
},
@@ -2452,7 +2562,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 54,
+ funcId: 57,
port: port_,
);
},
@@ -2483,7 +2593,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 55,
+ funcId: 58,
port: port_,
);
},
@@ -2516,7 +2626,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 56,
+ funcId: 59,
port: port_,
);
},
@@ -2551,7 +2661,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 57,
+ funcId: 60,
port: port_,
);
},
@@ -2584,7 +2694,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 58,
+ funcId: 61,
port: port_,
);
},
@@ -2617,7 +2727,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 59,
+ funcId: 62,
port: port_,
);
},
@@ -2650,7 +2760,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 60,
+ funcId: 63,
port: port_,
);
},
@@ -2687,7 +2797,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 61,
+ funcId: 64,
port: port_,
);
},
@@ -2717,7 +2827,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 62,
+ funcId: 65,
port: port_,
);
},
@@ -2748,7 +2858,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 63,
+ funcId: 66,
port: port_,
);
},
@@ -2781,7 +2891,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 64,
+ funcId: 67,
port: port_,
);
},
@@ -2827,7 +2937,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 65,
+ funcId: 68,
port: port_,
);
},
@@ -2883,7 +2993,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 66,
+ funcId: 69,
port: port_,
);
},
@@ -2922,7 +3032,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 67,
+ funcId: 70,
port: port_,
);
},
@@ -2957,7 +3067,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 68,
+ funcId: 71,
port: port_,
);
},
@@ -2992,7 +3102,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 69,
+ funcId: 72,
port: port_,
);
},
@@ -3025,7 +3135,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 70,
+ funcId: 73,
port: port_,
);
},
@@ -3056,7 +3166,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 71,
+ funcId: 74,
port: port_,
);
},
@@ -3089,7 +3199,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 72,
+ funcId: 75,
port: port_,
);
},
@@ -3124,7 +3234,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 73,
+ funcId: 76,
port: port_,
);
},
@@ -3155,7 +3265,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 74,
+ funcId: 77,
port: port_,
);
},
@@ -3192,7 +3302,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 75,
+ funcId: 78,
port: port_,
);
},
@@ -3244,7 +3354,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 76,
+ funcId: 79,
port: port_,
);
},
@@ -3297,7 +3407,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 77,
+ funcId: 80,
port: port_,
);
},
@@ -3337,7 +3447,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 78,
+ funcId: 81,
port: port_,
);
},
@@ -3370,7 +3480,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 79,
+ funcId: 82,
port: port_,
);
},
@@ -3403,7 +3513,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 80,
+ funcId: 83,
port: port_,
);
},
@@ -3440,7 +3550,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 81,
+ funcId: 84,
port: port_,
);
},
@@ -3472,7 +3582,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 82,
+ funcId: 85,
port: port_,
);
},
@@ -3505,7 +3615,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 83,
+ funcId: 86,
port: port_,
);
},
@@ -3540,7 +3650,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 84,
+ funcId: 87,
port: port_,
);
},
@@ -3572,7 +3682,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 85,
+ funcId: 88,
port: port_,
);
},
@@ -3610,7 +3720,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 86,
+ funcId: 89,
port: port_,
);
},
@@ -3643,7 +3753,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 87,
+ funcId: 90,
port: port_,
);
},
@@ -3681,7 +3791,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 88,
+ funcId: 91,
port: port_,
);
},
@@ -3718,7 +3828,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 89,
+ funcId: 92,
port: port_,
);
},
@@ -3755,7 +3865,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 90,
+ funcId: 93,
port: port_,
);
},
@@ -3793,7 +3903,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 91,
+ funcId: 94,
port: port_,
);
},
@@ -3831,7 +3941,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 92,
+ funcId: 95,
port: port_,
);
},
@@ -3863,7 +3973,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 93,
+ funcId: 96,
port: port_,
);
},
@@ -3896,7 +4006,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 94,
+ funcId: 97,
port: port_,
);
},
@@ -3928,7 +4038,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 95,
+ funcId: 98,
port: port_,
);
},
@@ -3963,7 +4073,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 96,
+ funcId: 99,
port: port_,
);
},
@@ -4000,7 +4110,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 97,
+ funcId: 100,
port: port_,
);
},
@@ -4032,7 +4142,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 98,
+ funcId: 101,
port: port_,
);
},
@@ -4064,7 +4174,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 99,
+ funcId: 102,
port: port_,
);
},
@@ -4099,7 +4209,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 100,
+ funcId: 103,
port: port_,
);
},
@@ -4138,7 +4248,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 101,
+ funcId: 104,
port: port_,
);
},
@@ -4175,7 +4285,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 102,
+ funcId: 105,
port: port_,
);
},
@@ -4205,7 +4315,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 103,
+ funcId: 106,
port: port_,
);
},
@@ -4237,7 +4347,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 104,
+ funcId: 107,
port: port_,
);
},
@@ -4272,7 +4382,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 105,
+ funcId: 108,
port: port_,
);
},
@@ -4304,7 +4414,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 106,
+ funcId: 109,
port: port_,
);
},
@@ -4342,7 +4452,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 107,
+ funcId: 110,
port: port_,
);
},
@@ -4381,7 +4491,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 108,
+ funcId: 111,
port: port_,
);
},
@@ -4416,7 +4526,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 109,
+ funcId: 112,
port: port_,
);
},
@@ -4437,6 +4547,37 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
argNames: ["shares", "threshold"],
);
+ @override
+ Future crateBridgeCallbacksLogStripAnsi({required String input}) {
+ return handler.executeNormal(
+ NormalTask(
+ callFfi: (port_) {
+ final serializer = SseSerializer(generalizedFrbRustBinding);
+ sse_encode_String(input, serializer);
+ pdeCallFfi(
+ generalizedFrbRustBinding,
+ serializer,
+ funcId: 113,
+ port: port_,
+ );
+ },
+ codec: SseCodec(
+ decodeSuccessData: sse_decode_String,
+ decodeErrorData: null,
+ ),
+ constMeta: kCrateBridgeCallbacksLogStripAnsiConstMeta,
+ argValues: [input],
+ apiImpl: this,
+ ),
+ );
+ }
+
+ TaskConstMeta get kCrateBridgeCallbacksLogStripAnsiConstMeta =>
+ const TaskConstMeta(
+ debugName: "strip_ansi",
+ argNames: ["input"],
+ );
+
@override
Future crateBridgeGroupsUpdateChatDeletionTime({
required String groupId,
@@ -4451,7 +4592,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 110,
+ funcId: 114,
port: port_,
);
},
@@ -4486,7 +4627,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 111,
+ funcId: 115,
port: port_,
);
},
@@ -4519,7 +4660,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 112,
+ funcId: 116,
)!;
},
codec: SseCodec(
@@ -4559,7 +4700,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 113,
+ funcId: 117,
port: port_,
);
},
@@ -4604,7 +4745,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 114,
+ funcId: 118,
port: port_,
);
},
@@ -4634,7 +4775,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 115,
+ funcId: 119,
port: port_,
);
},
@@ -4667,7 +4808,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 116,
+ funcId: 120,
port: port_,
);
},
@@ -4702,7 +4843,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 117,
+ funcId: 121,
port: port_,
);
},
diff --git a/lib/core/frb_generated.io.dart b/lib/core/frb_generated.io.dart
index 3d1c6a89..d1e685b0 100644
--- a/lib/core/frb_generated.io.dart
+++ b/lib/core/frb_generated.io.dart
@@ -7,6 +7,7 @@ import 'api/server/prekeys.dart';
import 'bridge.dart';
import 'bridge/api.dart';
import 'bridge/callbacks.dart';
+import 'bridge/callbacks/log.dart';
import 'bridge/groups.dart';
import 'bridge/user_config.dart';
import 'bridge/wrapper.dart';
diff --git a/lib/core/frb_generated.web.dart b/lib/core/frb_generated.web.dart
index fa5079d9..5efa4165 100644
--- a/lib/core/frb_generated.web.dart
+++ b/lib/core/frb_generated.web.dart
@@ -10,6 +10,7 @@ import 'api/server/prekeys.dart';
import 'bridge.dart';
import 'bridge/api.dart';
import 'bridge/callbacks.dart';
+import 'bridge/callbacks/log.dart';
import 'bridge/groups.dart';
import 'bridge/user_config.dart';
import 'bridge/wrapper.dart';
diff --git a/lib/src/services/notifications/native.notifications.dart b/lib/src/services/notifications/native.notifications.dart
index 80bdb905..83b2b08b 100644
--- a/lib/src/services/notifications/native.notifications.dart
+++ b/lib/src/services/notifications/native.notifications.dart
@@ -2,6 +2,8 @@ import 'dart:async';
import 'dart:io' show Platform;
import 'package:flutter/services.dart';
+import 'package:twonly/core/bridge/api.dart';
+import 'package:twonly/core/bridge/wrapper/app_database.dart';
import 'package:twonly/src/utils/log.dart';
/// Taps on notifications rendered natively (Android `MessagingStyle`) arrive
@@ -17,16 +19,27 @@ class NativeNotificationService {
static const String _conversationIdKey = 'conversation_id';
static const String _notificationIdsKey = 'notification_ids';
+ static const String _badgeCountKey = 'badge_count';
+
+ /// The table Rust commits to whenever a notification event is recorded,
+ /// delivered or cleared.
+ static const String _outboxTable = 'notification_outbox';
static final StreamController _taps =
StreamController.broadcast();
+ /// Lives for the whole process: the badge has to follow the outbox for as
+ /// long as the app runs.
+ // ignore: cancel_subscriptions
+ static StreamSubscription>? _outboxChanges;
+
/// Emits the conversation id of every notification tapped while the app is
/// running. A `null` value means the notification had no specific
/// conversation and should only open the chats tab.
static Stream get taps => _taps.stream;
static void init() {
+ _startBadgeSync();
if (!Platform.isAndroid) return;
_channel.setMethodCallHandler((call) async {
if (call.method != 'onNotificationTapped') return;
@@ -34,6 +47,69 @@ class NativeNotificationService {
});
}
+ /// Keeps the iOS app icon badge in sync with the pending notification
+ /// events. Only the notification service extension ever sets a badge on a
+ /// delivered alert, so without this the number stays at whatever the last
+ /// push reported even after every message has been read.
+ static void _startBadgeSync() {
+ if (!Platform.isIOS || _outboxChanges != null) return;
+ try {
+ _outboxChanges = RustAppDatabase.changes().listen(
+ (tables) {
+ // An empty batch means Rust could not say what changed, so the
+ // outbox has to be assumed stale as well.
+ if (tables.isNotEmpty && !tables.contains(_outboxTable)) return;
+ unawaited(refreshBadgeCount());
+ },
+ onError: (Object error) {
+ Log.error('Notification badge change stream failed: $error');
+ },
+ );
+ } catch (e) {
+ Log.error('Could not watch the notification outbox: $e');
+ return;
+ }
+ unawaited(refreshBadgeCount());
+ }
+
+ /// Reads the pending event count from Rust and writes it to the app icon.
+ static Future refreshBadgeCount() async {
+ if (!Platform.isIOS) return;
+ try {
+ final count = await RustApi.notificationBadgeCount();
+ await _channel.invokeMethod('setBadgeCount', {
+ _badgeCountKey: count,
+ });
+ } catch (e) {
+ Log.error('Could not update the app icon badge: $e');
+ }
+ }
+
+ /// Acknowledges every pending notification of a conversation the user just
+ /// opened and withdraws the alerts still on screen.
+ static Future clearConversation(String conversationId) async {
+ if (conversationId.isEmpty) return;
+ try {
+ final notificationIds = await RustApi.clearConversationNotifications(
+ conversationId: conversationId,
+ );
+ await cancelNotifications(notificationIds);
+ } catch (e) {
+ Log.error('Could not clear the notifications of a conversation: $e');
+ }
+ }
+
+ /// Acknowledges the contact requests the user just looked at. They belong to
+ /// no conversation, so this is the only moment they can be cleared.
+ static Future clearContactRequests() async {
+ try {
+ final notificationIds = await RustApi.clearContactRequestNotifications();
+ await cancelNotifications(notificationIds);
+ } catch (e) {
+ Log.error('Could not clear the contact request notifications: $e');
+ }
+ }
+
/// Returns the tap that launched the app, or `null` when the app was not
/// started from a native notification. The result is consumed once.
static Future<({String? conversationId})?> consumeInitialTap() async {
diff --git a/lib/src/visual/views/chats/chat_messages.view.dart b/lib/src/visual/views/chats/chat_messages.view.dart
index 541d9669..7050d979 100644
--- a/lib/src/visual/views/chats/chat_messages.view.dart
+++ b/lib/src/visual/views/chats/chat_messages.view.dart
@@ -118,9 +118,23 @@ class _ChatMessagesViewState extends State
textFieldFocus = FocusNode();
WidgetsBinding.instance.addObserver(this);
itemPositionsListener.itemPositions.addListener(_loadOlderWhenNeeded);
+ // Opening a conversation acknowledges everything it has pending, including
+ // the events that survive `notifyMessagesOpened` such as reactions and
+ // media status updates. Without this they keep inflating the app badge.
+ unawaited(NativeNotificationService.clearConversation(widget.groupId));
initStreams();
}
+ @override
+ void didChangeAppLifecycleState(AppLifecycleState state) {
+ super.didChangeAppLifecycleState(state);
+ if (state != AppLifecycleState.resumed) return;
+ if (!mounted || !(ModalRoute.of(context)?.isCurrent ?? false)) return;
+ // Notifications that arrived while this chat sat in the background are
+ // acknowledged as soon as the user looks at it again.
+ unawaited(NativeNotificationService.clearConversation(widget.groupId));
+ }
+
@override
void dispose() {
_subscriptions.cancelAll();
diff --git a/lib/src/visual/views/contact/add_new_contact.view.dart b/lib/src/visual/views/contact/add_new_contact.view.dart
index c9b11f40..1b85c17e 100644
--- a/lib/src/visual/views/contact/add_new_contact.view.dart
+++ b/lib/src/visual/views/contact/add_new_contact.view.dart
@@ -15,6 +15,7 @@ import 'package:twonly/src/database/daos/user_discovery.dao.dart';
import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/api/utils.api.dart';
+import 'package:twonly/src/services/notifications/native.notifications.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/alert.dialog.dart';
import 'package:twonly/src/visual/components/profile_qr_code.comp.dart';
@@ -55,6 +56,9 @@ class _SearchUsernameView extends State {
@override
void initState() {
super.initState();
+ // Contact requests belong to no conversation, so this list is the only
+ // place they can be acknowledged and taken off the app badge.
+ unawaited(NativeNotificationService.clearContactRequests());
_contactsStream = twonlyDB.contactsDao.watchNotAcceptedContacts().listen(
(update) {
if (mounted) {
diff --git a/lib/src/visual/views/groups/group.view.dart b/lib/src/visual/views/groups/group.view.dart
index 1eeead3e..57225a81 100644
--- a/lib/src/visual/views/groups/group.view.dart
+++ b/lib/src/visual/views/groups/group.view.dart
@@ -54,6 +54,16 @@ class _GroupViewState extends State {
}
Future initAsync() async {
+ // Opening the group is the user's way of retrying: ask for every member
+ // key that is still missing, ignoring the per-member request interval.
+ // Without a member's key the admin actions on them stay hidden.
+ unawaited(
+ rust_groups.fetchMissingGroupPublicKeys(
+ groupId: widget.groupId,
+ force: true,
+ ),
+ );
+
final groupStream = twonlyDB.groupsDao.watchGroup(widget.groupId);
groupSub = groupStream.listen((update) {
if (update != null) {
@@ -67,7 +77,9 @@ class _GroupViewState extends State {
setState(() {
members = update;
members.sort(
- (b, a) => a.$2.memberState!.index.compareTo(b.$2.memberState!.index),
+ (b, a) => (a.$2.memberState ?? MemberState.normal).index.compareTo(
+ (b.$2.memberState ?? MemberState.normal).index,
+ ),
);
});
});
diff --git a/lib/src/visual/views/groups/group_member.context.dart b/lib/src/visual/views/groups/group_member.context.dart
index c3c9bfae..f6b4f24e 100644
--- a/lib/src/visual/views/groups/group_member.context.dart
+++ b/lib/src/visual/views/groups/group_member.context.dart
@@ -38,7 +38,6 @@ class GroupMemberContextMenu extends StatelessWidget {
if (ok) {
if (!await rust_groups.manageAdminState(
groupId: group.groupId,
- groupPublicKey: member.groupPublicKey!,
contactId: contact.userId,
remove: false,
)) {
@@ -59,7 +58,6 @@ class GroupMemberContextMenu extends StatelessWidget {
if (ok) {
if (!await rust_groups.manageAdminState(
groupId: group.groupId,
- groupPublicKey: member.groupPublicKey!,
contactId: contact.userId,
remove: true,
)) {
@@ -79,7 +77,6 @@ class GroupMemberContextMenu extends StatelessWidget {
if (ok) {
if (!await rust_groups.removeMemberFromGroup(
groupId: group.groupId,
- groupPublicKey: member.groupPublicKey!,
contactId: contact.userId,
)) {
if (context.mounted) {
@@ -149,7 +146,7 @@ class GroupMemberContextMenu extends StatelessWidget {
),
if (member.groupPublicKey != null &&
group.isGroupAdmin &&
- member.memberState == MemberState.normal)
+ (member.memberState ?? MemberState.normal) == MemberState.normal)
ContextMenuItem(
title: context.lang.makeAdmin,
onTap: () => _makeContactAdmin(navigator.context),
@@ -163,7 +160,7 @@ class GroupMemberContextMenu extends StatelessWidget {
onTap: () => _removeContactAsAdmin(navigator.context),
icon: FontAwesomeIcons.key,
),
- if (group.isGroupAdmin && member.groupPublicKey != null)
+ if (group.isGroupAdmin)
ContextMenuItem(
title: context.lang.removeFromGroup,
onTap: () => _removeContactFromGroup(navigator.context),
diff --git a/rust/.sqlx/query-2b5ae0640585d4a0c5c1c521103d2279469d8645feca81bdadb717b39ad7ccd1.json b/rust/.sqlx/query-2b5ae0640585d4a0c5c1c521103d2279469d8645feca81bdadb717b39ad7ccd1.json
new file mode 100644
index 00000000..1a14c3ae
--- /dev/null
+++ b/rust/.sqlx/query-2b5ae0640585d4a0c5c1c521103d2279469d8645feca81bdadb717b39ad7ccd1.json
@@ -0,0 +1,12 @@
+{
+ "db_name": "SQLite",
+ "query": "\n UPDATE notification_outbox SET cleared_at = ?\n WHERE kind IN ('contact_request', 'accept_request') AND cleared_at IS NULL\n ",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": []
+ },
+ "hash": "2b5ae0640585d4a0c5c1c521103d2279469d8645feca81bdadb717b39ad7ccd1"
+}
diff --git a/rust/.sqlx/query-411a854abb8d23c104b7290c25313c679ef0361348c17d2ee740d30ad8c5a424.json b/rust/.sqlx/query-411a854abb8d23c104b7290c25313c679ef0361348c17d2ee740d30ad8c5a424.json
deleted file mode 100644
index 52de7ccd..00000000
--- a/rust/.sqlx/query-411a854abb8d23c104b7290c25313c679ef0361348c17d2ee740d30ad8c5a424.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "db_name": "SQLite",
- "query": "UPDATE group_members SET last_message = CAST(strftime('%s','now') AS INTEGER) WHERE group_id = ? AND contact_id = ?",
- "describe": {
- "columns": [],
- "parameters": {
- "Right": 2
- },
- "nullable": []
- },
- "hash": "411a854abb8d23c104b7290c25313c679ef0361348c17d2ee740d30ad8c5a424"
-}
diff --git a/rust/.sqlx/query-5164aba96e6ee8f761644482263b30e96adf6a2704e98310de21fc832f7a187f.json b/rust/.sqlx/query-5164aba96e6ee8f761644482263b30e96adf6a2704e98310de21fc832f7a187f.json
new file mode 100644
index 00000000..d09f1226
--- /dev/null
+++ b/rust/.sqlx/query-5164aba96e6ee8f761644482263b30e96adf6a2704e98310de21fc832f7a187f.json
@@ -0,0 +1,26 @@
+{
+ "db_name": "SQLite",
+ "query": "SELECT signal_version FROM contacts WHERE user_id = ?",
+ "describe": {
+ "columns": [
+ {
+ "name": "signal_version",
+ "ordinal": 0,
+ "type_info": "Text",
+ "origin": {
+ "Table": {
+ "table": "contacts",
+ "name": "signal_version"
+ }
+ }
+ }
+ ],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": [
+ false
+ ]
+ },
+ "hash": "5164aba96e6ee8f761644482263b30e96adf6a2704e98310de21fc832f7a187f"
+}
diff --git a/rust/.sqlx/query-7cc39bf45c3395a1f71a40d027afb0b2b7a14f18fa0ed0c66e5c9cac5678f4d4.json b/rust/.sqlx/query-7cc39bf45c3395a1f71a40d027afb0b2b7a14f18fa0ed0c66e5c9cac5678f4d4.json
new file mode 100644
index 00000000..61e62cec
--- /dev/null
+++ b/rust/.sqlx/query-7cc39bf45c3395a1f71a40d027afb0b2b7a14f18fa0ed0c66e5c9cac5678f4d4.json
@@ -0,0 +1,12 @@
+{
+ "db_name": "SQLite",
+ "query": "UPDATE contacts SET signal_version = 'v2', account_deleted = 0 WHERE user_id = ?",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Right": 1
+ },
+ "nullable": []
+ },
+ "hash": "7cc39bf45c3395a1f71a40d027afb0b2b7a14f18fa0ed0c66e5c9cac5678f4d4"
+}
diff --git a/rust/.sqlx/query-9841b7394adf2125090839e8381f8db55945d8230b5450aacc1620c975a59387.json b/rust/.sqlx/query-9841b7394adf2125090839e8381f8db55945d8230b5450aacc1620c975a59387.json
new file mode 100644
index 00000000..6a35564a
--- /dev/null
+++ b/rust/.sqlx/query-9841b7394adf2125090839e8381f8db55945d8230b5450aacc1620c975a59387.json
@@ -0,0 +1,26 @@
+{
+ "db_name": "SQLite",
+ "query": "\n SELECT notification_id FROM notification_outbox\n WHERE kind IN ('contact_request', 'accept_request') AND cleared_at IS NULL\n ",
+ "describe": {
+ "columns": [
+ {
+ "name": "notification_id",
+ "ordinal": 0,
+ "type_info": "Text",
+ "origin": {
+ "Table": {
+ "table": "notification_outbox",
+ "name": "notification_id"
+ }
+ }
+ }
+ ],
+ "parameters": {
+ "Right": 0
+ },
+ "nullable": [
+ false
+ ]
+ },
+ "hash": "9841b7394adf2125090839e8381f8db55945d8230b5450aacc1620c975a59387"
+}
diff --git a/rust/.sqlx/query-6d6a5fb10742309ab6e4242179ef86b64ac9f751f1c6d647d4ecfa5ecac5d332.json b/rust/.sqlx/query-e15b811d31ca59847e3d4ee2eff71410100258a0a5fe894233d63b6ac770368f.json
similarity index 51%
rename from rust/.sqlx/query-6d6a5fb10742309ab6e4242179ef86b64ac9f751f1c6d647d4ecfa5ecac5d332.json
rename to rust/.sqlx/query-e15b811d31ca59847e3d4ee2eff71410100258a0a5fe894233d63b6ac770368f.json
index 98aec940..d05ed6ef 100644
--- a/rust/.sqlx/query-6d6a5fb10742309ab6e4242179ef86b64ac9f751f1c6d647d4ecfa5ecac5d332.json
+++ b/rust/.sqlx/query-e15b811d31ca59847e3d4ee2eff71410100258a0a5fe894233d63b6ac770368f.json
@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
- "query": "\n SELECT group_id, contact_id \n FROM group_members \n WHERE group_public_key IS NULL \n AND last_message >= CAST(strftime('%s','now','-2 days') AS INTEGER)\n ",
+ "query": "\n SELECT members.group_id, members.contact_id\n FROM group_members AS members\n JOIN groups ON groups.group_id = members.group_id\n WHERE members.group_public_key IS NULL\n AND (members.member_state IS NULL OR members.member_state != 'leftGroup')\n AND groups.is_direct_chat = 0\n AND groups.left_group = 0\n AND (? IS NULL OR members.group_id = ?)\n ",
"describe": {
"columns": [
{
@@ -27,12 +27,12 @@
}
],
"parameters": {
- "Right": 0
+ "Right": 2
},
"nullable": [
false,
false
]
},
- "hash": "6d6a5fb10742309ab6e4242179ef86b64ac9f751f1c6d647d4ecfa5ecac5d332"
+ "hash": "e15b811d31ca59847e3d4ee2eff71410100258a0a5fe894233d63b6ac770368f"
}
diff --git a/rust/src/api/messages/incoming.rs b/rust/src/api/messages/incoming.rs
index ce44d193..caca9e6c 100644
--- a/rust/src/api/messages/incoming.rs
+++ b/rust/src/api/messages/incoming.rs
@@ -203,6 +203,65 @@ pub(crate) async fn handle_request_new_pqc_prekeys(
))
}
+/// Rebuilds a v2 Signal session for a peer that still speaks the legacy
+/// protocol, so the decryption error queued for the message can be answered
+/// with a session the peer can upgrade to.
+///
+/// Runs before the inbound transaction opens: it awaits a server round-trip and
+/// `establish_signal_session` writes through the pool, and the app database
+/// allows a single connection, so doing this under an open transaction would
+/// deadlock until the acquire timeout.
+async fn upgrade_legacy_session_to_v2(
+ ctx: &Arc,
+ database: &Arc,
+ from_user_id: i64,
+) -> Result<()> {
+ tracing::info!("Received legacy signal message; rejecting and upgrading session to v2");
+
+ let has_v2_session = {
+ let rust_database = ctx.rust_db.read().await.clone();
+ sqlx::query_scalar!(
+ "SELECT EXISTS(SELECT 1 FROM signal_sessions WHERE name = ? AND device_id = 1)",
+ from_user_id.to_string(),
+ )
+ .fetch_one(&rust_database.pool)
+ .await?
+ != 0
+ };
+
+ let is_v2_contact = sqlx::query_scalar!(
+ "SELECT signal_version FROM contacts WHERE user_id = ?",
+ from_user_id,
+ )
+ .fetch_optional(&database.pool)
+ .await?
+ .is_some_and(|version| version == "v2");
+
+ if has_v2_session && is_v2_contact {
+ return Ok(());
+ }
+
+ if let Err(error) = ContactService::new(ctx)
+ .establish_signal_session(from_user_id, None)
+ .await
+ {
+ tracing::warn!(
+ from_user_id,
+ "failed to establish v2 signal session from server: {error}"
+ );
+ }
+
+ sqlx::query!(
+ "UPDATE contacts SET signal_version = 'v2' WHERE user_id = ?",
+ from_user_id,
+ )
+ .execute(&database.pool)
+ .await?;
+ database.notify_committed(["contacts"]);
+
+ Ok(())
+}
+
#[tracing::instrument(
skip_all,
fields(
@@ -242,6 +301,13 @@ pub(crate) async fn handle_decoded_server_message(
let database = ctx.app_db.read().await.clone();
+ // Upgrading a legacy peer to a v2 session needs a server round-trip and
+ // writes through the pool itself, so it has to finish before the
+ // transaction below claims the single app-database connection.
+ if matches!(message_type, Type::Ciphertext | Type::PrekeyBundle) {
+ upgrade_legacy_session_to_v2(ctx, &database, from_user_id).await?;
+ }
+
let mut t = database.pool.begin().await?;
let claimed = Receipt::claim_received(&mut t, &message.receipt_id).await?;
@@ -277,42 +343,6 @@ pub(crate) async fn handle_decoded_server_message(
handle_sender_delivery_receipt(&mut t, from_user_id, &message.receipt_id).await?;
}
Type::Ciphertext | Type::PrekeyBundle => {
- tracing::info!("Received legacy signal message; rejecting and upgrading session to v2");
-
- let has_v2_session = {
- let rust_database = ctx.rust_db.read().await.clone();
- sqlx::query_scalar!(
- "SELECT EXISTS(SELECT 1 FROM signal_sessions WHERE name = ? AND device_id = 1)",
- from_user_id.to_string(),
- )
- .fetch_one(&rust_database.pool)
- .await?
- != 0
- };
-
- let is_v2_contact = {
- let contact = Contact::get_contact_by_id(&mut t, from_user_id).await?;
- contact.as_ref().is_some_and(|c| c.signal_version == "v2")
- };
-
- if !has_v2_session || !is_v2_contact {
- if let Err(error) = ContactService::new(ctx)
- .establish_signal_session(from_user_id, None)
- .await
- {
- tracing::warn!(
- from_user_id,
- "failed to establish v2 signal session from server: {error}"
- );
- }
- sqlx::query!(
- "UPDATE contacts SET signal_version = 'v2' WHERE user_id = ?",
- from_user_id,
- )
- .execute(&mut *t)
- .await?;
- }
-
queue_decryption_error(&mut t, from_user_id, &message.receipt_id, 0).await?;
sends_error_response = true;
}
diff --git a/rust/src/api/messages/incoming/errors.rs b/rust/src/api/messages/incoming/errors.rs
index e0ce33fe..12dd14b8 100644
--- a/rust/src/api/messages/incoming/errors.rs
+++ b/rust/src/api/messages/incoming/errors.rs
@@ -46,14 +46,19 @@ pub(crate) async fn handle_error_message(
}
Type::GroupNotFoundOrNotAMember => {
if let Some(group_id) = group_id {
- GroupService::new(ctx)
- .handle_membership_error(
- t,
- from_user_id,
- group_id.to_owned(),
- error.related_receipt_id,
- )
- .await?;
+ // The repair refreshes the group from the server and owns its
+ // own transaction, so it has to run once this one commits.
+ let ctx = ctx.clone();
+ let group_id = group_id.to_owned();
+ let related_receipt_id = error.related_receipt_id;
+ tokio::spawn(async move {
+ if let Err(error) = GroupService::new(&ctx)
+ .handle_membership_error(from_user_id, group_id, related_receipt_id)
+ .await
+ {
+ tracing::warn!("group membership repair failed: {error}");
+ }
+ });
}
}
Type::SessionOutOfSync | Type::UnknownMessageType => {}
diff --git a/rust/src/api/messages/incoming/groups.rs b/rust/src/api/messages/incoming/groups.rs
index 3baecbf5..c69824c3 100644
--- a/rust/src/api/messages/incoming/groups.rs
+++ b/rust/src/api/messages/incoming/groups.rs
@@ -91,9 +91,7 @@ pub(crate) async fn handle_group_create(
.execute(&mut **t)
.await?;
- GroupService::new(ctx)
- .refresh_group_state(t, group_id.to_owned(), true)
- .await;
+ GroupService::spawn_state_refresh_and_announce(ctx, group_id.to_owned());
Ok(())
}
@@ -175,9 +173,7 @@ pub(crate) async fn handle_group_update(
let is_direct = Group::is_direct_chat(t, group_id).await?;
if !is_direct {
- GroupService::new(ctx)
- .refresh_group_state(t, group_id.to_owned(), false)
- .await;
+ GroupService::spawn_state_refresh(ctx, Some(group_id.to_owned()));
}
if update.group_action_type == "updatedGroupName" {
diff --git a/rust/src/api/messages/incoming/messages.rs b/rust/src/api/messages/incoming/messages.rs
index 219f2a01..1c96acfb 100644
--- a/rust/src/api/messages/incoming/messages.rs
+++ b/rust/src/api/messages/incoming/messages.rs
@@ -298,7 +298,6 @@ pub(crate) struct PreparedQueuedReceipt {
pub contact_id: i64,
pub message_id: Option,
pub contact_will_sends_receipt: i64,
- pub account_deleted: i64,
pub payload: Vec,
pub wake_receiver: bool,
}
@@ -313,12 +312,11 @@ struct PreparedQueuedReceiptRow {
signal_version: String,
}
-pub(crate) async fn prepare_queued_receipt_details(
- ctx: &Arc,
+async fn load_queued_receipt_row(
+ pool: &sqlx::SqlitePool,
receipt_id: &str,
-) -> Result