Merge pull request #424 from twonlyapp/dev

Dev
This commit is contained in:
Tobi 2026-08-05 22:30:37 +02:00 committed by GitHub
commit d19f4ce3c9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
139 changed files with 42166 additions and 2690 deletions

View file

@ -1,5 +1,12 @@
# Changelog
## 0.5.0
- New: Update to Signal's new PQC-ready key agreement PQXDH
- New: Contact labels
- Fix: Multiple bug fixes
- Fix: Multiple black screens
## 0.4.0
- New: Encrypted Cloud Backup of Memories

View file

@ -89,15 +89,17 @@ class _AppState extends State<App> with WidgetsBindingObserver {
Locale('de', ''),
];
final settings = context.watch<SettingsChangeProvider>();
if (widget.storageError) {
return MaterialApp(
localizationsDelegates: localizationsDelegates,
debugShowCheckedModeBanner: false,
supportedLocales: supportedLocales,
title: 'twonly',
theme: lightTheme,
darkTheme: darkTheme,
themeMode: context.read<SettingsChangeProvider>().themeMode,
theme: getLightTheme(settings.primaryColor),
darkTheme: getDarkTheme(settings.primaryColor),
themeMode: settings.themeMode,
home: const CriticalErrorView(),
);
}
@ -108,9 +110,9 @@ class _AppState extends State<App> with WidgetsBindingObserver {
debugShowCheckedModeBanner: false,
supportedLocales: supportedLocales,
title: 'twonly',
theme: lightTheme,
darkTheme: darkTheme,
themeMode: context.read<SettingsChangeProvider>().themeMode,
theme: getLightTheme(settings.primaryColor),
darkTheme: getDarkTheme(settings.primaryColor),
themeMode: settings.themeMode,
home: const RecoveryView(),
);
}
@ -121,9 +123,9 @@ class _AppState extends State<App> with WidgetsBindingObserver {
debugShowCheckedModeBanner: false,
supportedLocales: supportedLocales,
title: 'twonly',
theme: lightTheme,
darkTheme: darkTheme,
themeMode: context.read<SettingsChangeProvider>().themeMode,
theme: getLightTheme(settings.primaryColor),
darkTheme: getDarkTheme(settings.primaryColor),
themeMode: settings.themeMode,
);
},
);
@ -161,6 +163,7 @@ class _AppMainWidgetState extends State<AppMainWidget> {
initAsync();
void handleShareLink(Uri uri) {
HomeViewState.pendingSharedLink = uri;
routerProvider.go(Routes.home);
HomeViewState.streamHomeViewPageIndex.add(1);
HomeViewState.streamSharedLink.add(uri);

View file

@ -0,0 +1,57 @@
// 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 '../../signal/engine.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
class RustSignal {
const RustSignal();
static Future<Uint8List> decrypt({
required String name,
required int deviceId,
required List<int> ciphertext,
}) => RustLib.instance.api.crateBridgeWrapperSignalRustSignalDecrypt(
name: name,
deviceId: deviceId,
ciphertext: ciphertext,
);
static Future<Uint8List> encrypt({
required String name,
required int deviceId,
required List<int> plaintext,
}) => RustLib.instance.api.crateBridgeWrapperSignalRustSignalEncrypt(
name: name,
deviceId: deviceId,
plaintext: plaintext,
);
static Future<FrbPreKeyBundle> generateBundle() =>
RustLib.instance.api.crateBridgeWrapperSignalRustSignalGenerateBundle();
static Future<List<FrbPqcPreKey>> generatePqcPrekeys() => RustLib.instance.api
.crateBridgeWrapperSignalRustSignalGeneratePqcPrekeys();
static Future<void> processPrekeyBundle({
required String name,
required int deviceId,
required FrbPreKeyBundle bundle,
}) => RustLib.instance.api
.crateBridgeWrapperSignalRustSignalProcessPrekeyBundle(
name: name,
deviceId: deviceId,
bundle: bundle,
);
@override
int get hashCode => 0;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RustSignal && runtimeType == other.runtimeType;
}

View file

@ -9,6 +9,7 @@ import 'bridge/callbacks/user_discovery.dart';
import 'bridge/wrapper.dart';
import 'bridge/wrapper/backup.dart';
import 'bridge/wrapper/key_manager.dart';
import 'bridge/wrapper/signal.dart';
import 'bridge/wrapper/user_discovery.dart';
import 'dart:async';
import 'dart:convert';
@ -18,6 +19,7 @@ import 'frb_generated.io.dart'
import 'keys/backup_password_keys.dart';
import 'lib.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
import 'signal/engine.dart';
/// Main entrypoint of the Rust API
class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
@ -76,7 +78,7 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
String get codegenVersion => '2.12.0';
@override
int get rustContentHash => 1788847092;
int get rustContentHash => 1109927244;
static const kDefaultExternalLibraryLoaderConfig =
ExternalLibraryLoaderConfig(
@ -259,6 +261,29 @@ abstract class RustLibApi extends BaseApi {
required List<int> record,
});
Future<Uint8List> crateBridgeWrapperSignalRustSignalDecrypt({
required String name,
required int deviceId,
required List<int> ciphertext,
});
Future<Uint8List> crateBridgeWrapperSignalRustSignalEncrypt({
required String name,
required int deviceId,
required List<int> plaintext,
});
Future<FrbPreKeyBundle> crateBridgeWrapperSignalRustSignalGenerateBundle();
Future<List<FrbPqcPreKey>>
crateBridgeWrapperSignalRustSignalGeneratePqcPrekeys();
Future<void> crateBridgeWrapperSignalRustSignalProcessPrekeyBundle({
required String name,
required int deviceId,
required FrbPreKeyBundle bundle,
});
Future<List<Uint8List>> crateBridgeWrapperRustUtilsGenerateShares({
required List<int> secret,
required int total,
@ -1651,6 +1676,183 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
argNames: ["signedPreKeyId", "record"],
);
@override
Future<Uint8List> crateBridgeWrapperSignalRustSignalDecrypt({
required String name,
required int deviceId,
required List<int> ciphertext,
}) {
return handler.executeNormal(
NormalTask(
callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_String(name, serializer);
sse_encode_u_32(deviceId, serializer);
sse_encode_list_prim_u_8_loose(ciphertext, serializer);
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 32,
port: port_,
);
},
codec: SseCodec(
decodeSuccessData: sse_decode_list_prim_u_8_strict,
decodeErrorData: sse_decode_AnyhowException,
),
constMeta: kCrateBridgeWrapperSignalRustSignalDecryptConstMeta,
argValues: [name, deviceId, ciphertext],
apiImpl: this,
),
);
}
TaskConstMeta get kCrateBridgeWrapperSignalRustSignalDecryptConstMeta =>
const TaskConstMeta(
debugName: "rust_signal_decrypt",
argNames: ["name", "deviceId", "ciphertext"],
);
@override
Future<Uint8List> crateBridgeWrapperSignalRustSignalEncrypt({
required String name,
required int deviceId,
required List<int> plaintext,
}) {
return handler.executeNormal(
NormalTask(
callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_String(name, serializer);
sse_encode_u_32(deviceId, serializer);
sse_encode_list_prim_u_8_loose(plaintext, serializer);
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 33,
port: port_,
);
},
codec: SseCodec(
decodeSuccessData: sse_decode_list_prim_u_8_strict,
decodeErrorData: sse_decode_AnyhowException,
),
constMeta: kCrateBridgeWrapperSignalRustSignalEncryptConstMeta,
argValues: [name, deviceId, plaintext],
apiImpl: this,
),
);
}
TaskConstMeta get kCrateBridgeWrapperSignalRustSignalEncryptConstMeta =>
const TaskConstMeta(
debugName: "rust_signal_encrypt",
argNames: ["name", "deviceId", "plaintext"],
);
@override
Future<FrbPreKeyBundle> crateBridgeWrapperSignalRustSignalGenerateBundle() {
return handler.executeNormal(
NormalTask(
callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding);
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 34,
port: port_,
);
},
codec: SseCodec(
decodeSuccessData: sse_decode_frb_pre_key_bundle,
decodeErrorData: sse_decode_AnyhowException,
),
constMeta: kCrateBridgeWrapperSignalRustSignalGenerateBundleConstMeta,
argValues: [],
apiImpl: this,
),
);
}
TaskConstMeta
get kCrateBridgeWrapperSignalRustSignalGenerateBundleConstMeta =>
const TaskConstMeta(
debugName: "rust_signal_generate_bundle",
argNames: [],
);
@override
Future<List<FrbPqcPreKey>>
crateBridgeWrapperSignalRustSignalGeneratePqcPrekeys() {
return handler.executeNormal(
NormalTask(
callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding);
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 35,
port: port_,
);
},
codec: SseCodec(
decodeSuccessData: sse_decode_list_frb_pqc_pre_key,
decodeErrorData: sse_decode_AnyhowException,
),
constMeta:
kCrateBridgeWrapperSignalRustSignalGeneratePqcPrekeysConstMeta,
argValues: [],
apiImpl: this,
),
);
}
TaskConstMeta
get kCrateBridgeWrapperSignalRustSignalGeneratePqcPrekeysConstMeta =>
const TaskConstMeta(
debugName: "rust_signal_generate_pqc_prekeys",
argNames: [],
);
@override
Future<void> crateBridgeWrapperSignalRustSignalProcessPrekeyBundle({
required String name,
required int deviceId,
required FrbPreKeyBundle bundle,
}) {
return handler.executeNormal(
NormalTask(
callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_String(name, serializer);
sse_encode_u_32(deviceId, serializer);
sse_encode_box_autoadd_frb_pre_key_bundle(bundle, serializer);
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 36,
port: port_,
);
},
codec: SseCodec(
decodeSuccessData: sse_decode_unit,
decodeErrorData: sse_decode_AnyhowException,
),
constMeta:
kCrateBridgeWrapperSignalRustSignalProcessPrekeyBundleConstMeta,
argValues: [name, deviceId, bundle],
apiImpl: this,
),
);
}
TaskConstMeta
get kCrateBridgeWrapperSignalRustSignalProcessPrekeyBundleConstMeta =>
const TaskConstMeta(
debugName: "rust_signal_process_prekey_bundle",
argNames: ["name", "deviceId", "bundle"],
);
@override
Future<List<Uint8List>> crateBridgeWrapperRustUtilsGenerateShares({
required List<int> secret,
@ -1667,7 +1869,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 32,
funcId: 37,
port: port_,
);
},
@ -1702,7 +1904,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 33,
funcId: 38,
port: port_,
);
},
@ -1738,7 +1940,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 34,
funcId: 39,
port: port_,
);
},
@ -1775,7 +1977,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 35,
funcId: 40,
port: port_,
);
},
@ -1813,7 +2015,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 36,
funcId: 41,
port: port_,
);
},
@ -1851,7 +2053,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 37,
funcId: 42,
port: port_,
);
},
@ -1889,7 +2091,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 38,
funcId: 43,
port: port_,
);
},
@ -1928,7 +2130,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 39,
funcId: 44,
port: port_,
);
},
@ -1967,7 +2169,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 40,
funcId: 45,
port: port_,
);
},
@ -2012,7 +2214,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 41,
funcId: 46,
port: port_,
);
},
@ -2064,7 +2266,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 42,
funcId: 47,
port: port_,
);
},
@ -2105,7 +2307,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 43,
funcId: 48,
port: port_,
);
},
@ -2143,7 +2345,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 44,
funcId: 49,
port: port_,
);
},
@ -2181,7 +2383,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 45,
funcId: 50,
port: port_,
);
},
@ -2219,7 +2421,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 46,
funcId: 51,
port: port_,
);
},
@ -2257,7 +2459,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 47,
funcId: 52,
port: port_,
);
},
@ -2299,7 +2501,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 48,
funcId: 53,
port: port_,
);
},
@ -2339,7 +2541,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 49,
funcId: 54,
port: port_,
);
},
@ -2978,6 +3180,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return dco_decode_backup_password_keys(raw);
}
@protected
FrbPreKeyBundle dco_decode_box_autoadd_frb_pre_key_bundle(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
return dco_decode_frb_pre_key_bundle(raw);
}
@protected
PlatformInt64 dco_decode_box_autoadd_i_64(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
@ -2996,6 +3204,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return dco_decode_other_promotion(raw);
}
@protected
int dco_decode_box_autoadd_u_32(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
return raw as int;
}
@protected
UserDiscoveryStoreFlutter dco_decode_box_autoadd_user_discovery_store_flutter(
dynamic raw,
@ -3021,6 +3235,42 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return FlutterUserDiscovery();
}
@protected
FrbPqcPreKey dco_decode_frb_pqc_pre_key(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
final arr = raw as List<dynamic>;
if (arr.length != 5)
throw Exception('unexpected arr length: expect 5 but see ${arr.length}');
return FrbPqcPreKey(
eccPreKeyId: dco_decode_u_32(arr[0]),
eccPreKey: dco_decode_list_prim_u_8_strict(arr[1]),
kyberPreKeyId: dco_decode_u_32(arr[2]),
kyberPreKey: dco_decode_list_prim_u_8_strict(arr[3]),
kyberPreKeySignature: dco_decode_list_prim_u_8_strict(arr[4]),
);
}
@protected
FrbPreKeyBundle dco_decode_frb_pre_key_bundle(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
final arr = raw as List<dynamic>;
if (arr.length != 11)
throw Exception('unexpected arr length: expect 11 but see ${arr.length}');
return FrbPreKeyBundle(
registrationId: dco_decode_u_32(arr[0]),
deviceId: dco_decode_u_32(arr[1]),
preKeyId: dco_decode_opt_box_autoadd_u_32(arr[2]),
preKeyPublic: dco_decode_opt_list_prim_u_8_strict(arr[3]),
signedPreKeyId: dco_decode_u_32(arr[4]),
signedPreKeyPublic: dco_decode_list_prim_u_8_strict(arr[5]),
signedPreKeySignature: dco_decode_list_prim_u_8_strict(arr[6]),
kyberPreKeyId: dco_decode_u_32(arr[7]),
kyberPreKeyPublic: dco_decode_list_prim_u_8_strict(arr[8]),
kyberPreKeySignature: dco_decode_list_prim_u_8_strict(arr[9]),
identityKey: dco_decode_list_prim_u_8_strict(arr[10]),
);
}
@protected
PlatformInt64 dco_decode_i_64(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
@ -3045,6 +3295,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return dcoDecodeI64(raw);
}
@protected
List<FrbPqcPreKey> dco_decode_list_frb_pqc_pre_key(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
return (raw as List<dynamic>).map(dco_decode_frb_pqc_pre_key).toList();
}
@protected
List<Uint8List> dco_decode_list_list_prim_u_8_strict(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
@ -3096,6 +3352,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return raw == null ? null : dco_decode_box_autoadd_i_64(raw);
}
@protected
int? dco_decode_opt_box_autoadd_u_32(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
return raw == null ? null : dco_decode_box_autoadd_u_32(raw);
}
@protected
List<Uint8List>? dco_decode_opt_list_list_prim_u_8_strict(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
@ -3200,6 +3462,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return RustKeyManager();
}
@protected
RustSignal dco_decode_rust_signal(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
final arr = raw as List<dynamic>;
if (arr.length != 0)
throw Exception('unexpected arr length: expect 0 but see ${arr.length}');
return RustSignal();
}
@protected
RustUtils dco_decode_rust_utils(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
@ -3347,6 +3618,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return (sse_decode_backup_password_keys(deserializer));
}
@protected
FrbPreKeyBundle sse_decode_box_autoadd_frb_pre_key_bundle(
SseDeserializer deserializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
return (sse_decode_frb_pre_key_bundle(deserializer));
}
@protected
PlatformInt64 sse_decode_box_autoadd_i_64(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
@ -3367,6 +3646,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return (sse_decode_other_promotion(deserializer));
}
@protected
int sse_decode_box_autoadd_u_32(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
return (sse_decode_u_32(deserializer));
}
@protected
UserDiscoveryStoreFlutter sse_decode_box_autoadd_user_discovery_store_flutter(
SseDeserializer deserializer,
@ -3391,6 +3676,58 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return FlutterUserDiscovery();
}
@protected
FrbPqcPreKey sse_decode_frb_pqc_pre_key(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
var var_eccPreKeyId = sse_decode_u_32(deserializer);
var var_eccPreKey = sse_decode_list_prim_u_8_strict(deserializer);
var var_kyberPreKeyId = sse_decode_u_32(deserializer);
var var_kyberPreKey = sse_decode_list_prim_u_8_strict(deserializer);
var var_kyberPreKeySignature = sse_decode_list_prim_u_8_strict(
deserializer,
);
return FrbPqcPreKey(
eccPreKeyId: var_eccPreKeyId,
eccPreKey: var_eccPreKey,
kyberPreKeyId: var_kyberPreKeyId,
kyberPreKey: var_kyberPreKey,
kyberPreKeySignature: var_kyberPreKeySignature,
);
}
@protected
FrbPreKeyBundle sse_decode_frb_pre_key_bundle(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
var var_registrationId = sse_decode_u_32(deserializer);
var var_deviceId = sse_decode_u_32(deserializer);
var var_preKeyId = sse_decode_opt_box_autoadd_u_32(deserializer);
var var_preKeyPublic = sse_decode_opt_list_prim_u_8_strict(deserializer);
var var_signedPreKeyId = sse_decode_u_32(deserializer);
var var_signedPreKeyPublic = sse_decode_list_prim_u_8_strict(deserializer);
var var_signedPreKeySignature = sse_decode_list_prim_u_8_strict(
deserializer,
);
var var_kyberPreKeyId = sse_decode_u_32(deserializer);
var var_kyberPreKeyPublic = sse_decode_list_prim_u_8_strict(deserializer);
var var_kyberPreKeySignature = sse_decode_list_prim_u_8_strict(
deserializer,
);
var var_identityKey = sse_decode_list_prim_u_8_strict(deserializer);
return FrbPreKeyBundle(
registrationId: var_registrationId,
deviceId: var_deviceId,
preKeyId: var_preKeyId,
preKeyPublic: var_preKeyPublic,
signedPreKeyId: var_signedPreKeyId,
signedPreKeyPublic: var_signedPreKeyPublic,
signedPreKeySignature: var_signedPreKeySignature,
kyberPreKeyId: var_kyberPreKeyId,
kyberPreKeyPublic: var_kyberPreKeyPublic,
kyberPreKeySignature: var_kyberPreKeySignature,
identityKey: var_identityKey,
);
}
@protected
PlatformInt64 sse_decode_i_64(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
@ -3411,6 +3748,20 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return deserializer.buffer.getPlatformInt64();
}
@protected
List<FrbPqcPreKey> sse_decode_list_frb_pqc_pre_key(
SseDeserializer deserializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
var len_ = sse_decode_i_32(deserializer);
var ans_ = <FrbPqcPreKey>[];
for (var idx_ = 0; idx_ < len_; ++idx_) {
ans_.add(sse_decode_frb_pqc_pre_key(deserializer));
}
return ans_;
}
@protected
List<Uint8List> sse_decode_list_list_prim_u_8_strict(
SseDeserializer deserializer,
@ -3503,6 +3854,17 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
}
}
@protected
int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
if (sse_decode_bool(deserializer)) {
return (sse_decode_box_autoadd_u_32(deserializer));
} else {
return null;
}
}
@protected
List<Uint8List>? sse_decode_opt_list_list_prim_u_8_strict(
SseDeserializer deserializer,
@ -3613,6 +3975,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return RustKeyManager();
}
@protected
RustSignal sse_decode_rust_signal(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
return RustSignal();
}
@protected
RustUtils sse_decode_rust_utils(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
@ -3946,6 +4314,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
sse_encode_backup_password_keys(self, serializer);
}
@protected
void sse_encode_box_autoadd_frb_pre_key_bundle(
FrbPreKeyBundle self,
SseSerializer serializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_frb_pre_key_bundle(self, serializer);
}
@protected
void sse_encode_box_autoadd_i_64(
PlatformInt64 self,
@ -3973,6 +4350,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
sse_encode_other_promotion(self, serializer);
}
@protected
void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_u_32(self, serializer);
}
@protected
void sse_encode_box_autoadd_user_discovery_store_flutter(
UserDiscoveryStoreFlutter self,
@ -3999,6 +4382,35 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
// Codec=Sse (Serialization based), see doc to use other codecs
}
@protected
void sse_encode_frb_pqc_pre_key(FrbPqcPreKey self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_u_32(self.eccPreKeyId, serializer);
sse_encode_list_prim_u_8_strict(self.eccPreKey, serializer);
sse_encode_u_32(self.kyberPreKeyId, serializer);
sse_encode_list_prim_u_8_strict(self.kyberPreKey, serializer);
sse_encode_list_prim_u_8_strict(self.kyberPreKeySignature, serializer);
}
@protected
void sse_encode_frb_pre_key_bundle(
FrbPreKeyBundle self,
SseSerializer serializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_u_32(self.registrationId, serializer);
sse_encode_u_32(self.deviceId, serializer);
sse_encode_opt_box_autoadd_u_32(self.preKeyId, serializer);
sse_encode_opt_list_prim_u_8_strict(self.preKeyPublic, serializer);
sse_encode_u_32(self.signedPreKeyId, serializer);
sse_encode_list_prim_u_8_strict(self.signedPreKeyPublic, serializer);
sse_encode_list_prim_u_8_strict(self.signedPreKeySignature, serializer);
sse_encode_u_32(self.kyberPreKeyId, serializer);
sse_encode_list_prim_u_8_strict(self.kyberPreKeyPublic, serializer);
sse_encode_list_prim_u_8_strict(self.kyberPreKeySignature, serializer);
sse_encode_list_prim_u_8_strict(self.identityKey, serializer);
}
@protected
void sse_encode_i_64(PlatformInt64 self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
@ -4018,6 +4430,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
serializer.buffer.putPlatformInt64(self);
}
@protected
void sse_encode_list_frb_pqc_pre_key(
List<FrbPqcPreKey> self,
SseSerializer serializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_i_32(self.length, serializer);
for (final item in self) {
sse_encode_frb_pqc_pre_key(item, serializer);
}
}
@protected
void sse_encode_list_list_prim_u_8_strict(
List<Uint8List> self,
@ -4112,6 +4536,16 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
}
}
@protected
void sse_encode_opt_box_autoadd_u_32(int? self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_bool(self != null, serializer);
if (self != null) {
sse_encode_box_autoadd_u_32(self, serializer);
}
}
@protected
void sse_encode_opt_list_list_prim_u_8_strict(
List<Uint8List>? self,
@ -4222,6 +4656,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
// Codec=Sse (Serialization based), see doc to use other codecs
}
@protected
void sse_encode_rust_signal(RustSignal self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
}
@protected
void sse_encode_rust_utils(RustUtils self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs

View file

@ -9,6 +9,7 @@ import 'bridge/callbacks/user_discovery.dart';
import 'bridge/wrapper.dart';
import 'bridge/wrapper/backup.dart';
import 'bridge/wrapper/key_manager.dart';
import 'bridge/wrapper/signal.dart';
import 'bridge/wrapper/user_discovery.dart';
import 'dart:async';
import 'dart:convert';
@ -17,6 +18,7 @@ import 'frb_generated.dart';
import 'keys/backup_password_keys.dart';
import 'lib.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_io.dart';
import 'signal/engine.dart';
abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
RustLibApiImplPlatform({
@ -130,6 +132,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
BackupPasswordKeys dco_decode_box_autoadd_backup_password_keys(dynamic raw);
@protected
FrbPreKeyBundle dco_decode_box_autoadd_frb_pre_key_bundle(dynamic raw);
@protected
PlatformInt64 dco_decode_box_autoadd_i_64(dynamic raw);
@ -139,6 +144,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
OtherPromotion dco_decode_box_autoadd_other_promotion(dynamic raw);
@protected
int dco_decode_box_autoadd_u_32(dynamic raw);
@protected
UserDiscoveryStoreFlutter dco_decode_box_autoadd_user_discovery_store_flutter(
dynamic raw,
@ -152,6 +160,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
FlutterUserDiscovery dco_decode_flutter_user_discovery(dynamic raw);
@protected
FrbPqcPreKey dco_decode_frb_pqc_pre_key(dynamic raw);
@protected
FrbPreKeyBundle dco_decode_frb_pre_key_bundle(dynamic raw);
@protected
PlatformInt64 dco_decode_i_64(dynamic raw);
@ -161,6 +175,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
PlatformInt64 dco_decode_isize(dynamic raw);
@protected
List<FrbPqcPreKey> dco_decode_list_frb_pqc_pre_key(dynamic raw);
@protected
List<Uint8List> dco_decode_list_list_prim_u_8_strict(dynamic raw);
@ -186,6 +203,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
PlatformInt64? dco_decode_opt_box_autoadd_i_64(dynamic raw);
@protected
int? dco_decode_opt_box_autoadd_u_32(dynamic raw);
@protected
List<Uint8List>? dco_decode_opt_list_list_prim_u_8_strict(dynamic raw);
@ -220,6 +240,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
RustKeyManager dco_decode_rust_key_manager(dynamic raw);
@protected
RustSignal dco_decode_rust_signal(dynamic raw);
@protected
RustUtils dco_decode_rust_utils(dynamic raw);
@ -288,6 +311,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
FrbPreKeyBundle sse_decode_box_autoadd_frb_pre_key_bundle(
SseDeserializer deserializer,
);
@protected
PlatformInt64 sse_decode_box_autoadd_i_64(SseDeserializer deserializer);
@ -299,6 +327,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
int sse_decode_box_autoadd_u_32(SseDeserializer deserializer);
@protected
UserDiscoveryStoreFlutter sse_decode_box_autoadd_user_discovery_store_flutter(
SseDeserializer deserializer,
@ -314,6 +345,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
FrbPqcPreKey sse_decode_frb_pqc_pre_key(SseDeserializer deserializer);
@protected
FrbPreKeyBundle sse_decode_frb_pre_key_bundle(SseDeserializer deserializer);
@protected
PlatformInt64 sse_decode_i_64(SseDeserializer deserializer);
@ -323,6 +360,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
PlatformInt64 sse_decode_isize(SseDeserializer deserializer);
@protected
List<FrbPqcPreKey> sse_decode_list_frb_pqc_pre_key(
SseDeserializer deserializer,
);
@protected
List<Uint8List> sse_decode_list_list_prim_u_8_strict(
SseDeserializer deserializer,
@ -356,6 +398,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
PlatformInt64? sse_decode_opt_box_autoadd_i_64(SseDeserializer deserializer);
@protected
int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer);
@protected
List<Uint8List>? sse_decode_opt_list_list_prim_u_8_strict(
SseDeserializer deserializer,
@ -400,6 +445,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
RustKeyManager sse_decode_rust_key_manager(SseDeserializer deserializer);
@protected
RustSignal sse_decode_rust_signal(SseDeserializer deserializer);
@protected
RustUtils sse_decode_rust_utils(SseDeserializer deserializer);
@ -561,6 +609,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_frb_pre_key_bundle(
FrbPreKeyBundle self,
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_i_64(
PlatformInt64 self,
@ -579,6 +633,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer);
@protected
void sse_encode_box_autoadd_user_discovery_store_flutter(
UserDiscoveryStoreFlutter self,
@ -597,6 +654,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_frb_pqc_pre_key(FrbPqcPreKey self, SseSerializer serializer);
@protected
void sse_encode_frb_pre_key_bundle(
FrbPreKeyBundle self,
SseSerializer serializer,
);
@protected
void sse_encode_i_64(PlatformInt64 self, SseSerializer serializer);
@ -606,6 +672,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_isize(PlatformInt64 self, SseSerializer serializer);
@protected
void sse_encode_list_frb_pqc_pre_key(
List<FrbPqcPreKey> self,
SseSerializer serializer,
);
@protected
void sse_encode_list_list_prim_u_8_strict(
List<Uint8List> self,
@ -648,6 +720,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_opt_box_autoadd_u_32(int? self, SseSerializer serializer);
@protected
void sse_encode_opt_list_list_prim_u_8_strict(
List<Uint8List>? self,
@ -708,6 +783,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_rust_signal(RustSignal self, SseSerializer serializer);
@protected
void sse_encode_rust_utils(RustUtils self, SseSerializer serializer);

View file

@ -12,6 +12,7 @@ import 'bridge/callbacks/user_discovery.dart';
import 'bridge/wrapper.dart';
import 'bridge/wrapper/backup.dart';
import 'bridge/wrapper/key_manager.dart';
import 'bridge/wrapper/signal.dart';
import 'bridge/wrapper/user_discovery.dart';
import 'dart:async';
import 'dart:convert';
@ -19,6 +20,7 @@ import 'frb_generated.dart';
import 'keys/backup_password_keys.dart';
import 'lib.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_web.dart';
import 'signal/engine.dart';
abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
RustLibApiImplPlatform({
@ -132,6 +134,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
BackupPasswordKeys dco_decode_box_autoadd_backup_password_keys(dynamic raw);
@protected
FrbPreKeyBundle dco_decode_box_autoadd_frb_pre_key_bundle(dynamic raw);
@protected
PlatformInt64 dco_decode_box_autoadd_i_64(dynamic raw);
@ -141,6 +146,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
OtherPromotion dco_decode_box_autoadd_other_promotion(dynamic raw);
@protected
int dco_decode_box_autoadd_u_32(dynamic raw);
@protected
UserDiscoveryStoreFlutter dco_decode_box_autoadd_user_discovery_store_flutter(
dynamic raw,
@ -154,6 +162,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
FlutterUserDiscovery dco_decode_flutter_user_discovery(dynamic raw);
@protected
FrbPqcPreKey dco_decode_frb_pqc_pre_key(dynamic raw);
@protected
FrbPreKeyBundle dco_decode_frb_pre_key_bundle(dynamic raw);
@protected
PlatformInt64 dco_decode_i_64(dynamic raw);
@ -163,6 +177,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
PlatformInt64 dco_decode_isize(dynamic raw);
@protected
List<FrbPqcPreKey> dco_decode_list_frb_pqc_pre_key(dynamic raw);
@protected
List<Uint8List> dco_decode_list_list_prim_u_8_strict(dynamic raw);
@ -188,6 +205,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
PlatformInt64? dco_decode_opt_box_autoadd_i_64(dynamic raw);
@protected
int? dco_decode_opt_box_autoadd_u_32(dynamic raw);
@protected
List<Uint8List>? dco_decode_opt_list_list_prim_u_8_strict(dynamic raw);
@ -222,6 +242,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
RustKeyManager dco_decode_rust_key_manager(dynamic raw);
@protected
RustSignal dco_decode_rust_signal(dynamic raw);
@protected
RustUtils dco_decode_rust_utils(dynamic raw);
@ -290,6 +313,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
FrbPreKeyBundle sse_decode_box_autoadd_frb_pre_key_bundle(
SseDeserializer deserializer,
);
@protected
PlatformInt64 sse_decode_box_autoadd_i_64(SseDeserializer deserializer);
@ -301,6 +329,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
int sse_decode_box_autoadd_u_32(SseDeserializer deserializer);
@protected
UserDiscoveryStoreFlutter sse_decode_box_autoadd_user_discovery_store_flutter(
SseDeserializer deserializer,
@ -316,6 +347,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
FrbPqcPreKey sse_decode_frb_pqc_pre_key(SseDeserializer deserializer);
@protected
FrbPreKeyBundle sse_decode_frb_pre_key_bundle(SseDeserializer deserializer);
@protected
PlatformInt64 sse_decode_i_64(SseDeserializer deserializer);
@ -325,6 +362,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
PlatformInt64 sse_decode_isize(SseDeserializer deserializer);
@protected
List<FrbPqcPreKey> sse_decode_list_frb_pqc_pre_key(
SseDeserializer deserializer,
);
@protected
List<Uint8List> sse_decode_list_list_prim_u_8_strict(
SseDeserializer deserializer,
@ -358,6 +400,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
PlatformInt64? sse_decode_opt_box_autoadd_i_64(SseDeserializer deserializer);
@protected
int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer);
@protected
List<Uint8List>? sse_decode_opt_list_list_prim_u_8_strict(
SseDeserializer deserializer,
@ -402,6 +447,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
RustKeyManager sse_decode_rust_key_manager(SseDeserializer deserializer);
@protected
RustSignal sse_decode_rust_signal(SseDeserializer deserializer);
@protected
RustUtils sse_decode_rust_utils(SseDeserializer deserializer);
@ -563,6 +611,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_frb_pre_key_bundle(
FrbPreKeyBundle self,
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_i_64(
PlatformInt64 self,
@ -581,6 +635,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer);
@protected
void sse_encode_box_autoadd_user_discovery_store_flutter(
UserDiscoveryStoreFlutter self,
@ -599,6 +656,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_frb_pqc_pre_key(FrbPqcPreKey self, SseSerializer serializer);
@protected
void sse_encode_frb_pre_key_bundle(
FrbPreKeyBundle self,
SseSerializer serializer,
);
@protected
void sse_encode_i_64(PlatformInt64 self, SseSerializer serializer);
@ -608,6 +674,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_isize(PlatformInt64 self, SseSerializer serializer);
@protected
void sse_encode_list_frb_pqc_pre_key(
List<FrbPqcPreKey> self,
SseSerializer serializer,
);
@protected
void sse_encode_list_list_prim_u_8_strict(
List<Uint8List> self,
@ -650,6 +722,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_opt_box_autoadd_u_32(int? self, SseSerializer serializer);
@protected
void sse_encode_opt_list_list_prim_u_8_strict(
List<Uint8List>? self,
@ -710,6 +785,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_rust_signal(RustSignal self, SseSerializer serializer);
@protected
void sse_encode_rust_utils(RustUtils self, SseSerializer serializer);

101
lib/core/signal/engine.dart Normal file
View file

@ -0,0 +1,101 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.12.0.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import '../frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
class FrbPqcPreKey {
final int eccPreKeyId;
final Uint8List eccPreKey;
final int kyberPreKeyId;
final Uint8List kyberPreKey;
final Uint8List kyberPreKeySignature;
const FrbPqcPreKey({
required this.eccPreKeyId,
required this.eccPreKey,
required this.kyberPreKeyId,
required this.kyberPreKey,
required this.kyberPreKeySignature,
});
@override
int get hashCode =>
eccPreKeyId.hashCode ^
eccPreKey.hashCode ^
kyberPreKeyId.hashCode ^
kyberPreKey.hashCode ^
kyberPreKeySignature.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is FrbPqcPreKey &&
runtimeType == other.runtimeType &&
eccPreKeyId == other.eccPreKeyId &&
eccPreKey == other.eccPreKey &&
kyberPreKeyId == other.kyberPreKeyId &&
kyberPreKey == other.kyberPreKey &&
kyberPreKeySignature == other.kyberPreKeySignature;
}
class FrbPreKeyBundle {
final int registrationId;
final int deviceId;
final int? preKeyId;
final Uint8List? preKeyPublic;
final int signedPreKeyId;
final Uint8List signedPreKeyPublic;
final Uint8List signedPreKeySignature;
final int kyberPreKeyId;
final Uint8List kyberPreKeyPublic;
final Uint8List kyberPreKeySignature;
final Uint8List identityKey;
const FrbPreKeyBundle({
required this.registrationId,
required this.deviceId,
this.preKeyId,
this.preKeyPublic,
required this.signedPreKeyId,
required this.signedPreKeyPublic,
required this.signedPreKeySignature,
required this.kyberPreKeyId,
required this.kyberPreKeyPublic,
required this.kyberPreKeySignature,
required this.identityKey,
});
@override
int get hashCode =>
registrationId.hashCode ^
deviceId.hashCode ^
preKeyId.hashCode ^
preKeyPublic.hashCode ^
signedPreKeyId.hashCode ^
signedPreKeyPublic.hashCode ^
signedPreKeySignature.hashCode ^
kyberPreKeyId.hashCode ^
kyberPreKeyPublic.hashCode ^
kyberPreKeySignature.hashCode ^
identityKey.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is FrbPreKeyBundle &&
runtimeType == other.runtimeType &&
registrationId == other.registrationId &&
deviceId == other.deviceId &&
preKeyId == other.preKeyId &&
preKeyPublic == other.preKeyPublic &&
signedPreKeyId == other.signedPreKeyId &&
signedPreKeyPublic == other.signedPreKeyPublic &&
signedPreKeySignature == other.signedPreKeySignature &&
kyberPreKeyId == other.kyberPreKeyId &&
kyberPreKeyPublic == other.kyberPreKeyPublic &&
kyberPreKeySignature == other.kyberPreKeySignature &&
identityKey == other.identityKey;
}

View file

@ -175,13 +175,15 @@ Future<void> postStartupTasks() async {
// 2. Service initializations
unawaited(finishStartedPreprocessing());
unawaited(createPushAvatars());
unawaited(newsService.init().then((_) {
unawaited(
newsService.init().then((_) {
final lastDownload = newsService.lastDownloadedAt;
if (lastDownload == null ||
DateTime.now().difference(lastDownload) >= const Duration(days: 7)) {
newsService.fetchFeed();
}
}));
}),
);
unawaited(UserDiscoveryService.verifyInitializationOnStartup());

View file

@ -340,6 +340,24 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
.write(GroupsCompanion(lastMessageExchange: Value(clampedLastMessage)));
}
Future<void> increaseMemberLastMessage(
String groupId,
int contactId,
DateTime newLastMessage,
) async {
final now = clock.now();
final clampedLastMessage =
newLastMessage.isAfter(now) ? now : newLastMessage;
await (update(groupMembers)..where(
(t) =>
t.groupId.equals(groupId) &
t.contactId.equals(contactId) &
(t.lastMessage.isNull() |
t.lastMessage.isSmallerThanValue(clampedLastMessage)),
))
.write(GroupMembersCompanion(lastMessage: Value(clampedLastMessage)));
}
Stream<List<Group>> watchNonDirectGroupsForMember(int contactId) {
final query =
select(groups).join([

View file

@ -0,0 +1,91 @@
import 'package:drift/drift.dart';
import 'package:twonly/src/database/tables/labels.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
part 'labels.dao.g.dart';
@DriftAccessor(
tables: [
Labels,
ContactLabels,
],
)
class LabelsDao extends DatabaseAccessor<TwonlyDB> with _$LabelsDaoMixin {
LabelsDao(super.db);
Stream<List<Label>> watchAllLabels() {
return (select(labels)..orderBy([(t) => OrderingTerm(expression: t.name)])).watch();
}
Future<List<Label>> getAllLabels() {
return (select(labels)..orderBy([(t) => OrderingTerm(expression: t.name)])).get();
}
Stream<List<Label>> watchContactLabels(int contactId) {
final query = select(contactLabels).join([
innerJoin(labels, labels.id.equalsExp(contactLabels.labelId)),
])..where(contactLabels.contactId.equals(contactId));
return query.watch().map(
(rows) => rows.map((row) => row.readTable(labels)).toList(),
);
}
Future<List<Label>> getContactLabels(int contactId) {
final query = select(contactLabels).join([
innerJoin(labels, labels.id.equalsExp(contactLabels.labelId)),
])..where(contactLabels.contactId.equals(contactId));
return query.get().then(
(rows) => rows.map((row) => row.readTable(labels)).toList(),
);
}
Future<void> setContactLabels(int contactId, List<int> labelIds) async {
final sanitizedLabelIds = labelIds.take(3).toList();
await transaction(() async {
await (delete(contactLabels)..where((t) => t.contactId.equals(contactId))).go();
if (sanitizedLabelIds.isNotEmpty) {
await batch((b) {
b.insertAll(
contactLabels,
sanitizedLabelIds.map(
(lId) => ContactLabelsCompanion.insert(
contactId: contactId,
labelId: lId,
),
),
);
});
}
});
}
Future<int> createLabel(String name, int textColor, int backgroundColor) {
final sanitizedName = name.length > 8 ? name.substring(0, 8) : name;
return into(labels).insert(
LabelsCompanion.insert(
name: sanitizedName,
textColor: textColor,
backgroundColor: backgroundColor,
),
);
}
Future<bool> updateLabel(int id, String name, int textColor, int backgroundColor) {
final sanitizedName = name.length > 8 ? name.substring(0, 8) : name;
return (update(labels)..where((t) => t.id.equals(id)))
.write(
LabelsCompanion(
name: Value(sanitizedName),
textColor: Value(textColor),
backgroundColor: Value(backgroundColor),
),
)
.then((rows) => rows > 0);
}
Future<int> deleteLabel(int id) {
return (delete(labels)..where((t) => t.id.equals(id))).go();
}
}

View file

@ -0,0 +1,22 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'labels.dao.dart';
// ignore_for_file: type=lint
mixin _$LabelsDaoMixin on DatabaseAccessor<TwonlyDB> {
$LabelsTable get labels => attachedDatabase.labels;
$ContactsTable get contacts => attachedDatabase.contacts;
$ContactLabelsTable get contactLabels => attachedDatabase.contactLabels;
LabelsDaoManager get managers => LabelsDaoManager(this);
}
class LabelsDaoManager {
final _$LabelsDaoMixin _db;
LabelsDaoManager(this._db);
$$LabelsTableTableManager get labels =>
$$LabelsTableTableManager(_db.attachedDatabase, _db.labels);
$$ContactsTableTableManager get contacts =>
$$ContactsTableTableManager(_db.attachedDatabase, _db.contacts);
$$ContactLabelsTableTableManager get contactLabels =>
$$ContactLabelsTableTableManager(_db.attachedDatabase, _db.contactLabels);
}

View file

@ -489,22 +489,28 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
await into(messages).insertOnConflictUpdate(insertMessage);
final msgTime = insertMessage.createdAt.present
? insertMessage.createdAt.value
: clock.now();
await twonlyDB.groupsDao.updateGroup(
message.groupId.value,
GroupsCompanion(
lastMessageExchange: Value(clock.now()),
archived: const Value(false),
deletedContent: const Value(false),
const GroupsCompanion(
archived: Value(false),
deletedContent: Value(false),
),
);
if (message.senderId.present) {
await twonlyDB.groupsDao.updateMember(
await twonlyDB.groupsDao.increaseLastMessageExchange(
message.groupId.value,
msgTime,
);
if (message.senderId.present && message.senderId.value != null) {
await twonlyDB.groupsDao.increaseMemberLastMessage(
message.groupId.value,
message.senderId.value!,
GroupMembersCompanion(
lastMessage: Value(clock.now()),
),
msgTime,
);
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -63,6 +63,7 @@ class SignalSignedPreKeyStore extends SignedPreKeyStore {
.insert(companion, mode: InsertMode.insertOrReplace);
} catch (e) {
Log.error('$e');
rethrow;
}
}

View file

@ -1,5 +1,8 @@
import 'package:drift/drift.dart';
enum SignalVersion { v1, v2 }
@DataClassName('Contact')
class Contacts extends Table {
IntColumn get userId => integer()();
@ -23,6 +26,9 @@ class Contacts extends Table {
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
TextColumn get signalVersion =>
textEnum<SignalVersion>().withDefault(const Constant('v1'))();
// User Discovery
BlobColumn get userDiscoveryVersion => blob().nullable()();
BoolColumn get userDiscoveryExcluded =>

View file

@ -31,7 +31,7 @@ class Groups extends Table {
boolean().withDefault(const Constant(false))();
IntColumn get deleteMessagesAfterMilliseconds => integer().withDefault(
const Constant(defaultDeleteMessagesAfterMilliseconds),
const Constant(1000 * 60 * 60 * 24),
)();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();

View file

@ -0,0 +1,28 @@
import 'package:drift/drift.dart';
import 'package:twonly/src/database/tables/contacts.table.dart';
@DataClassName('Label')
class Labels extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()();
IntColumn get textColor => integer()();
IntColumn get backgroundColor => integer()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
}
@DataClassName('ContactLabel')
class ContactLabels extends Table {
IntColumn get contactId => integer().references(
Contacts,
#userId,
onDelete: KeyAction.cascade,
)();
IntColumn get labelId => integer().references(
Labels,
#id,
onDelete: KeyAction.cascade,
)();
@override
Set<Column> get primaryKey => {contactId, labelId};
}

View file

@ -6,6 +6,7 @@ import 'package:twonly/locator.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/daos/groups.dao.dart';
import 'package:twonly/src/database/daos/key_verification.dao.dart';
import 'package:twonly/src/database/daos/labels.dao.dart';
import 'package:twonly/src/database/daos/mediafiles.dao.dart';
import 'package:twonly/src/database/daos/messages.dao.dart';
import 'package:twonly/src/database/daos/reactions.dao.dart';
@ -15,6 +16,7 @@ import 'package:twonly/src/database/daos/user_discovery.dao.dart';
import 'package:twonly/src/database/drift_logging_interceptor.dart';
import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/tables/groups.table.dart';
import 'package:twonly/src/database/tables/labels.table.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/tables/reactions.table.dart';
@ -59,6 +61,8 @@ part 'twonly.db.g.dart';
UserDiscoveryShares,
Shortcuts,
ShortcutMembers,
Labels,
ContactLabels,
],
daos: [
MessagesDao,
@ -70,6 +74,7 @@ part 'twonly.db.g.dart';
UserDiscoveryDao,
KeyVerificationDao,
ShortcutsDao,
LabelsDao,
],
)
class TwonlyDB extends _$TwonlyDB {
@ -82,7 +87,7 @@ class TwonlyDB extends _$TwonlyDB {
TwonlyDB.forTesting(DatabaseConnection super.connection);
@override
int get schemaVersion => 23;
int get schemaVersion => 25;
static QueryExecutor _openConnection() {
final connection = driftDatabase(
@ -285,6 +290,13 @@ class TwonlyDB extends _$TwonlyDB {
schema.mediaFiles.blurhash,
);
},
from23To24: (m, schema) async {
await m.createTable(schema.labels);
await m.createTable(schema.contactLabels);
},
from24To25: (m, schema) async {
await m.addColumn(schema.contacts, schema.contacts.signalVersion);
},
)(m, from, to);
},
);

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1424,6 +1424,12 @@ abstract class AppLocalizations {
/// **'Delete for me'**
String get deleteOkBtnForMe;
/// No description provided for @deleteOnlyForMe.
///
/// In en, this message translates to:
/// **'Delete only for me'**
String get deleteOnlyForMe;
/// No description provided for @deleteImageTitle.
///
/// In en, this message translates to:
@ -1541,7 +1547,7 @@ abstract class AppLocalizations {
/// No description provided for @backupArchiveHeader.
///
/// In en, this message translates to:
/// **'Contacts, Settings and Messages'**
/// **'Contacts & Messages'**
String get backupArchiveHeader;
/// No description provided for @backupLastBackupDate.
@ -1595,7 +1601,7 @@ abstract class AppLocalizations {
/// No description provided for @backupSelectStrongPassword.
///
/// In en, this message translates to:
/// **'Choose a secure password. This is required if you want to restore your twonly Backup.'**
/// **'Choose a secure password. This is required if you want to restore your backup.'**
String get backupSelectStrongPassword;
/// No description provided for @password.
@ -1652,6 +1658,36 @@ abstract class AppLocalizations {
/// **'You can only change your password after you have authenticated!'**
String get backupChangePasswordAuthFailed;
/// No description provided for @backupFreeSpaceWithCloud.
///
/// In en, this message translates to:
/// **'Free Space with Cloud Backup'**
String get backupFreeSpaceWithCloud;
/// No description provided for @backupMemoriesNotEnabled.
///
/// In en, this message translates to:
/// **'Not enabled'**
String get backupMemoriesNotEnabled;
/// No description provided for @backupMemoriesUpgradeRequired.
///
/// In en, this message translates to:
/// **'Upgrade required'**
String get backupMemoriesUpgradeRequired;
/// No description provided for @todayAt.
///
/// In en, this message translates to:
/// **'Today at {time}'**
String todayAt(Object time);
/// No description provided for @yesterdayAt.
///
/// In en, this message translates to:
/// **'Yesterday at {time}'**
String yesterdayAt(Object time);
/// No description provided for @twonlySafeRecoverTitle.
///
/// In en, this message translates to:
@ -3404,6 +3440,12 @@ abstract class AppLocalizations {
/// **'Zero ads. Total privacy.'**
String get subscriptionPledgeSubtitle;
/// No description provided for @subscriptionManage.
///
/// In en, this message translates to:
/// **'Manage subscription'**
String get subscriptionManage;
/// No description provided for @dragToZoom.
///
/// In en, this message translates to:
@ -3671,7 +3713,7 @@ abstract class AppLocalizations {
/// No description provided for @passwordlessRecovery.
///
/// In en, this message translates to:
/// **'Passwordless Recovery'**
/// **'Password Recovery'**
String get passwordlessRecovery;
/// No description provided for @passwordlessRecoveryNotConfigured.
@ -3917,7 +3959,7 @@ abstract class AppLocalizations {
/// No description provided for @passwordlessRecoveryTrustedFriends.
///
/// In en, this message translates to:
/// **'Trusted Friends'**
/// **'Account recovery'**
String get passwordlessRecoveryTrustedFriends;
/// No description provided for @passwordlessRecoveryDoneBtn.
@ -4097,7 +4139,7 @@ abstract class AppLocalizations {
/// No description provided for @missingRecoveryContactsCardTitle.
///
/// In en, this message translates to:
/// **'Recovery Contacts'**
/// **'Account Recovery'**
String get missingRecoveryContactsCardTitle;
/// No description provided for @missingRecoveryContactsCardDesc.
@ -4193,7 +4235,7 @@ abstract class AppLocalizations {
/// No description provided for @settingsStorageContents.
///
/// In en, this message translates to:
/// **'Storage contents'**
/// **'Free up space'**
String get settingsStorageContents;
/// No description provided for @settingsStorageSortStorage.
@ -4273,6 +4315,126 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'All memories are up to date.'**
String get settingsStorageSyncUpToDate;
/// No description provided for @restoreLostFlames.
///
/// In en, this message translates to:
/// **'Restore your {count} lost flames'**
String restoreLostFlames(int count);
/// No description provided for @settingsShowRestoreFlameTitle.
///
/// In en, this message translates to:
/// **'Show flame restore warning'**
String get settingsShowRestoreFlameTitle;
/// No description provided for @contactLabelsTitle.
///
/// In en, this message translates to:
/// **'Contact Labels'**
String get contactLabelsTitle;
/// No description provided for @contactLabelsSubtitleEmpty.
///
/// In en, this message translates to:
/// **'No labels selected'**
String get contactLabelsSubtitleEmpty;
/// No description provided for @contactLabelsMaxLimit.
///
/// In en, this message translates to:
/// **'Maximum 3 labels per contact'**
String get contactLabelsMaxLimit;
/// No description provided for @createLabel.
///
/// In en, this message translates to:
/// **'Create new label'**
String get createLabel;
/// No description provided for @editLabel.
///
/// In en, this message translates to:
/// **'Edit label'**
String get editLabel;
/// No description provided for @deleteLabel.
///
/// In en, this message translates to:
/// **'Delete label'**
String get deleteLabel;
/// No description provided for @deleteLabelConfirmation.
///
/// In en, this message translates to:
/// **'Are you sure you want to delete this label? It will be removed from all contacts.'**
String get deleteLabelConfirmation;
/// No description provided for @labelNameHint.
///
/// In en, this message translates to:
/// **'Label name'**
String get labelNameHint;
/// No description provided for @labelTextColor.
///
/// In en, this message translates to:
/// **'Text color'**
String get labelTextColor;
/// No description provided for @labelBackgroundColor.
///
/// In en, this message translates to:
/// **'Background color'**
String get labelBackgroundColor;
/// No description provided for @customColor.
///
/// In en, this message translates to:
/// **'Custom Color'**
String get customColor;
/// No description provided for @hue.
///
/// In en, this message translates to:
/// **'Hue'**
String get hue;
/// No description provided for @saturation.
///
/// In en, this message translates to:
/// **'Saturation'**
String get saturation;
/// No description provided for @brightness.
///
/// In en, this message translates to:
/// **'Brightness'**
String get brightness;
/// No description provided for @settingsAppearancePrimaryColor.
///
/// In en, this message translates to:
/// **'Primary Color'**
String get settingsAppearancePrimaryColor;
/// No description provided for @themeSystemDefault.
///
/// In en, this message translates to:
/// **'System default'**
String get themeSystemDefault;
/// No description provided for @themeLight.
///
/// In en, this message translates to:
/// **'Light'**
String get themeLight;
/// No description provided for @themeDark.
///
/// In en, this message translates to:
/// **'Dark'**
String get themeDark;
}
class _AppLocalizationsDelegate

View file

@ -741,6 +741,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get deleteOkBtnForMe => 'Für mich löschen';
@override
String get deleteOnlyForMe => 'Nur für mich löschen';
@override
String get deleteImageTitle => 'Bist du dir sicher?';
@ -817,7 +820,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get backupIdentityHeader => 'Identität';
@override
String get backupArchiveHeader => 'Kontakte, Einstellungen und Nachrichten';
String get backupArchiveHeader => 'Kontakte & Nachrichten';
@override
String get backupLastBackupDate => 'Letztes Backup';
@ -847,7 +850,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get backupSelectStrongPassword =>
'Wähle ein sicheres Passwort. Dies ist erforderlich, wenn du dein twonly Backup wiederherstellen möchtest.';
'Wähle ein sicheres Passwort. Dies ist erforderlich, wenn du dein Backup wiederherstellen möchtest.';
@override
String get password => 'Passwort';
@ -878,6 +881,26 @@ class AppLocalizationsDe extends AppLocalizations {
String get backupChangePasswordAuthFailed =>
'Du kannst dein Passwort nur ändern, wenn du dich authentifiziert hast!';
@override
String get backupFreeSpaceWithCloud =>
'Speicherplatz mit Cloud-Backup freigeben';
@override
String get backupMemoriesNotEnabled => 'Nicht aktiviert';
@override
String get backupMemoriesUpgradeRequired => 'Upgrade erforderlich';
@override
String todayAt(Object time) {
return 'Heute um $time';
}
@override
String yesterdayAt(Object time) {
return 'Gestern um $time';
}
@override
String get twonlySafeRecoverTitle => 'Backup wiederherstellen';
@ -1960,6 +1983,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get subscriptionPledgeSubtitle => 'Keine Werbung. Volle Privatsphäre.';
@override
String get subscriptionManage => 'Abonnement verwalten';
@override
String get dragToZoom => 'Zum Zoomen ziehen';
@ -2118,7 +2144,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get avatarCustomizeReset => 'Zurücksetzen';
@override
String get passwordlessRecovery => 'Passwortloses Backup';
String get passwordlessRecovery => 'Passwort vergessen';
@override
String get passwordlessRecoveryNotConfigured => 'Nicht konfiguriert';
@ -2184,7 +2210,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get passwordlessRecoveryEnableSuccess =>
'Passwortloses Backup erfolgreich aktiviert!';
'\"Passwort vergessen\" erfolgreich aktiviert!';
@override
String get passwordlessRecoveryEnterPin => 'Bitte gib eine PIN ein.';
@ -2194,13 +2220,16 @@ class AppLocalizationsDe extends AppLocalizations {
'Bitte gib eine E-Mail-Adresse ein.';
@override
String get passwordlessRecoveryEnableBtn => 'Passwortloses Backup aktivieren';
String get passwordlessRecoveryEnableBtn =>
'\"Passwort vergessen\" aktivieren';
@override
String get passwordlessRecoveryRecoverBtn => 'Passwortlos wiederherstellen';
String get passwordlessRecoveryRecoverBtn =>
'Mit \"Passwort vergessen\" wiederherstellen';
@override
String get passwordlessRecoveryModifyBtn => 'Passwortloses Backup bearbeiten';
String get passwordlessRecoveryModifyBtn =>
'\"Passwort vergessen\" bearbeiten';
@override
String passwordlessRecoveryStatusEnabled(num count) {
@ -2261,7 +2290,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get passwordlessRecoverySelectFriends => 'Freunde auswählen';
@override
String get passwordlessRecoveryTrustedFriends => 'Vertrauenswürdige Freunde';
String get passwordlessRecoveryTrustedFriends => 'Kontowiederherstellung';
@override
String passwordlessRecoveryDoneBtn(num count) {
@ -2375,7 +2404,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get recoverPasswordlessRecoverNowBtn => 'Jetzt wiederherstellen';
@override
String get missingRecoveryContactsCardTitle => 'Vertrauenswürdige Kontakte';
String get missingRecoveryContactsCardTitle => 'Kontowiederherstellung';
@override
String get missingRecoveryContactsCardDesc =>
@ -2426,7 +2455,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get settingsHelpNews => 'Neuigkeiten';
@override
String get settingsStorageContents => 'Speicherinhalte';
String get settingsStorageContents => 'Speicherplatz freigeben';
@override
String get settingsStorageSortStorage => 'Belegter Speicher';
@ -2475,4 +2504,68 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get settingsStorageSyncUpToDate =>
'Alle Memories sind auf dem neuesten Stand.';
@override
String restoreLostFlames(int count) {
return 'Stelle deine $count verlorenen Flammen wieder her';
}
@override
String get settingsShowRestoreFlameTitle =>
'Hinweis zur Flammen-Wiederherstellung anzeigen';
@override
String get contactLabelsTitle => 'Kontaktlabels';
@override
String get contactLabelsSubtitleEmpty => 'Keine Labels ausgewählt';
@override
String get contactLabelsMaxLimit => 'Maximal 3 Labels pro Kontakt';
@override
String get createLabel => 'Neues Label erstellen';
@override
String get editLabel => 'Label bearbeiten';
@override
String get deleteLabel => 'Label löschen';
@override
String get deleteLabelConfirmation =>
'Möchtest du dieses Label wirklich löschen? Es wird von allen Kontakten entfernt.';
@override
String get labelNameHint => 'Label-Name';
@override
String get labelTextColor => 'Textfarbe';
@override
String get labelBackgroundColor => 'Hintergrundfarbe';
@override
String get customColor => 'Eigene Farbe';
@override
String get hue => 'Farbton';
@override
String get saturation => 'Sättigung';
@override
String get brightness => 'Helligkeit';
@override
String get settingsAppearancePrimaryColor => 'Hauptfarbe';
@override
String get themeSystemDefault => 'Systemstandard';
@override
String get themeLight => 'Hell';
@override
String get themeDark => 'Dunkel';
}

View file

@ -736,6 +736,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get deleteOkBtnForMe => 'Delete for me';
@override
String get deleteOnlyForMe => 'Delete only for me';
@override
String get deleteImageTitle => 'Are you sure?';
@ -812,7 +815,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get backupIdentityHeader => 'Identity';
@override
String get backupArchiveHeader => 'Contacts, Settings and Messages';
String get backupArchiveHeader => 'Contacts & Messages';
@override
String get backupLastBackupDate => 'Last backup';
@ -842,7 +845,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get backupSelectStrongPassword =>
'Choose a secure password. This is required if you want to restore your twonly Backup.';
'Choose a secure password. This is required if you want to restore your backup.';
@override
String get password => 'Password';
@ -873,6 +876,25 @@ class AppLocalizationsEn extends AppLocalizations {
String get backupChangePasswordAuthFailed =>
'You can only change your password after you have authenticated!';
@override
String get backupFreeSpaceWithCloud => 'Free Space with Cloud Backup';
@override
String get backupMemoriesNotEnabled => 'Not enabled';
@override
String get backupMemoriesUpgradeRequired => 'Upgrade required';
@override
String todayAt(Object time) {
return 'Today at $time';
}
@override
String yesterdayAt(Object time) {
return 'Yesterday at $time';
}
@override
String get twonlySafeRecoverTitle => 'Restore backup';
@ -1946,6 +1968,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get subscriptionPledgeSubtitle => 'Zero ads. Total privacy.';
@override
String get subscriptionManage => 'Manage subscription';
@override
String get dragToZoom => 'Drag to Zoom';
@ -2104,7 +2129,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get avatarCustomizeReset => 'Reset';
@override
String get passwordlessRecovery => 'Passwordless Recovery';
String get passwordlessRecovery => 'Password Recovery';
@override
String get passwordlessRecoveryNotConfigured => 'Not configured';
@ -2245,7 +2270,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get passwordlessRecoverySelectFriends => 'Select trusted friends';
@override
String get passwordlessRecoveryTrustedFriends => 'Trusted Friends';
String get passwordlessRecoveryTrustedFriends => 'Account recovery';
@override
String passwordlessRecoveryDoneBtn(num count) {
@ -2354,7 +2379,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get recoverPasswordlessRecoverNowBtn => 'Recover now';
@override
String get missingRecoveryContactsCardTitle => 'Recovery Contacts';
String get missingRecoveryContactsCardTitle => 'Account Recovery';
@override
String get missingRecoveryContactsCardDesc =>
@ -2405,7 +2430,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get settingsHelpNews => 'News';
@override
String get settingsStorageContents => 'Storage contents';
String get settingsStorageContents => 'Free up space';
@override
String get settingsStorageSortStorage => 'Occupied storage';
@ -2453,4 +2478,67 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get settingsStorageSyncUpToDate => 'All memories are up to date.';
@override
String restoreLostFlames(int count) {
return 'Restore your $count lost flames';
}
@override
String get settingsShowRestoreFlameTitle => 'Show flame restore warning';
@override
String get contactLabelsTitle => 'Contact Labels';
@override
String get contactLabelsSubtitleEmpty => 'No labels selected';
@override
String get contactLabelsMaxLimit => 'Maximum 3 labels per contact';
@override
String get createLabel => 'Create new label';
@override
String get editLabel => 'Edit label';
@override
String get deleteLabel => 'Delete label';
@override
String get deleteLabelConfirmation =>
'Are you sure you want to delete this label? It will be removed from all contacts.';
@override
String get labelNameHint => 'Label name';
@override
String get labelTextColor => 'Text color';
@override
String get labelBackgroundColor => 'Background color';
@override
String get customColor => 'Custom Color';
@override
String get hue => 'Hue';
@override
String get saturation => 'Saturation';
@override
String get brightness => 'Brightness';
@override
String get settingsAppearancePrimaryColor => 'Primary Color';
@override
String get themeSystemDefault => 'System default';
@override
String get themeLight => 'Light';
@override
String get themeDark => 'Dark';
}

@ -1 +1 @@
Subproject commit 34c835db2de08af962b39fb79ec11341b8847926
Subproject commit ce51a6d084db162b4dfb7cbba1c0cda6d8c477d5

View file

@ -57,6 +57,12 @@ class UserData {
@JsonKey(defaultValue: ThemeMode.system)
ThemeMode themeMode = ThemeMode.system;
int? primaryColorValue;
Color get primaryColor => primaryColorValue != null
? Color(primaryColorValue!)
: const Color(0xFF57CC99);
int? defaultShowTime;
@JsonKey(defaultValue: false)
@ -93,10 +99,15 @@ class UserData {
@JsonKey(defaultValue: true)
bool typingIndicators = true;
@JsonKey(defaultValue: true)
bool showRestoreFlame = true;
String? myBestFriendGroupId;
DateTime? signalLastSignedPreKeyUpdated;
DateTime? signalLastPqcPreKeysUploaded;
@JsonKey(defaultValue: false)
bool allowErrorTrackingViaSentry = false;

View file

@ -33,6 +33,7 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
..themeMode =
$enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']) ??
ThemeMode.system
..primaryColorValue = (json['primaryColorValue'] as num?)?.toInt()
..defaultShowTime = (json['defaultShowTime'] as num?)?.toInt()
..requestedAudioPermission =
json['requestedAudioPermission'] as bool? ?? false
@ -60,11 +61,16 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
..autoStoreAllSendUnlimitedMediaFiles =
json['autoStoreAllSendUnlimitedMediaFiles'] as bool? ?? false
..typingIndicators = json['typingIndicators'] as bool? ?? true
..showRestoreFlame = json['showRestoreFlame'] as bool? ?? true
..myBestFriendGroupId = json['myBestFriendGroupId'] as String?
..signalLastSignedPreKeyUpdated =
json['signalLastSignedPreKeyUpdated'] == null
? null
: DateTime.parse(json['signalLastSignedPreKeyUpdated'] as String)
..signalLastPqcPreKeysUploaded =
json['signalLastPqcPreKeysUploaded'] == null
? null
: DateTime.parse(json['signalLastPqcPreKeysUploaded'] as String)
..allowErrorTrackingViaSentry =
json['allowErrorTrackingViaSentry'] as bool? ?? false
..screenLockEnabled = json['screenLockEnabled'] as bool? ?? false
@ -136,6 +142,7 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'lastPlanBallance': instance.lastPlanBallance,
'additionalUserInvites': instance.additionalUserInvites,
'themeMode': _$ThemeModeEnumMap[instance.themeMode]!,
'primaryColorValue': instance.primaryColorValue,
'defaultShowTime': instance.defaultShowTime,
'requestedAudioPermission': instance.requestedAudioPermission,
'enableDatabaseLogging': instance.enableDatabaseLogging,
@ -151,9 +158,12 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'autoStoreAllSendUnlimitedMediaFiles':
instance.autoStoreAllSendUnlimitedMediaFiles,
'typingIndicators': instance.typingIndicators,
'showRestoreFlame': instance.showRestoreFlame,
'myBestFriendGroupId': instance.myBestFriendGroupId,
'signalLastSignedPreKeyUpdated': instance.signalLastSignedPreKeyUpdated
?.toIso8601String(),
'signalLastPqcPreKeysUploaded': instance.signalLastPqcPreKeysUploaded
?.toIso8601String(),
'allowErrorTrackingViaSentry': instance.allowErrorTrackingViaSentry,
'screenLockEnabled': instance.screenLockEnabled,
'isCloudBackupEnabled': instance.isCloudBackupEnabled,

View file

@ -2059,6 +2059,250 @@ class ApplicationData_UpdateSignedPreKey extends $pb.GeneratedMessage {
void clearSignedPrekeySignature() => $_clearField(3);
}
class ApplicationData_PqcPreKey extends $pb.GeneratedMessage {
factory ApplicationData_PqcPreKey({
$fixnum.Int64? eccPreKeyId,
$core.List<$core.int>? eccPreKey,
$fixnum.Int64? kyberPreKeyId,
$core.List<$core.int>? kyberPreKey,
$core.List<$core.int>? kyberPreKeySignature,
}) {
final result = create();
if (eccPreKeyId != null) result.eccPreKeyId = eccPreKeyId;
if (eccPreKey != null) result.eccPreKey = eccPreKey;
if (kyberPreKeyId != null) result.kyberPreKeyId = kyberPreKeyId;
if (kyberPreKey != null) result.kyberPreKey = kyberPreKey;
if (kyberPreKeySignature != null)
result.kyberPreKeySignature = kyberPreKeySignature;
return result;
}
ApplicationData_PqcPreKey._();
factory ApplicationData_PqcPreKey.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory ApplicationData_PqcPreKey.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'ApplicationData.PqcPreKey',
package:
const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'),
createEmptyInstance: create)
..aInt64(1, _omitFieldNames ? '' : 'eccPreKeyId')
..a<$core.List<$core.int>>(
2, _omitFieldNames ? '' : 'eccPreKey', $pb.PbFieldType.OY)
..aInt64(3, _omitFieldNames ? '' : 'kyberPreKeyId')
..a<$core.List<$core.int>>(
4, _omitFieldNames ? '' : 'kyberPreKey', $pb.PbFieldType.OY)
..a<$core.List<$core.int>>(
5, _omitFieldNames ? '' : 'kyberPreKeySignature', $pb.PbFieldType.OY)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
ApplicationData_PqcPreKey clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
ApplicationData_PqcPreKey copyWith(
void Function(ApplicationData_PqcPreKey) updates) =>
super.copyWith((message) => updates(message as ApplicationData_PqcPreKey))
as ApplicationData_PqcPreKey;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static ApplicationData_PqcPreKey create() => ApplicationData_PqcPreKey._();
@$core.override
ApplicationData_PqcPreKey createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static ApplicationData_PqcPreKey getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<ApplicationData_PqcPreKey>(create);
static ApplicationData_PqcPreKey? _defaultInstance;
@$pb.TagNumber(1)
$fixnum.Int64 get eccPreKeyId => $_getI64(0);
@$pb.TagNumber(1)
set eccPreKeyId($fixnum.Int64 value) => $_setInt64(0, value);
@$pb.TagNumber(1)
$core.bool hasEccPreKeyId() => $_has(0);
@$pb.TagNumber(1)
void clearEccPreKeyId() => $_clearField(1);
@$pb.TagNumber(2)
$core.List<$core.int> get eccPreKey => $_getN(1);
@$pb.TagNumber(2)
set eccPreKey($core.List<$core.int> value) => $_setBytes(1, value);
@$pb.TagNumber(2)
$core.bool hasEccPreKey() => $_has(1);
@$pb.TagNumber(2)
void clearEccPreKey() => $_clearField(2);
@$pb.TagNumber(3)
$fixnum.Int64 get kyberPreKeyId => $_getI64(2);
@$pb.TagNumber(3)
set kyberPreKeyId($fixnum.Int64 value) => $_setInt64(2, value);
@$pb.TagNumber(3)
$core.bool hasKyberPreKeyId() => $_has(2);
@$pb.TagNumber(3)
void clearKyberPreKeyId() => $_clearField(3);
@$pb.TagNumber(4)
$core.List<$core.int> get kyberPreKey => $_getN(3);
@$pb.TagNumber(4)
set kyberPreKey($core.List<$core.int> value) => $_setBytes(3, value);
@$pb.TagNumber(4)
$core.bool hasKyberPreKey() => $_has(3);
@$pb.TagNumber(4)
void clearKyberPreKey() => $_clearField(4);
@$pb.TagNumber(5)
$core.List<$core.int> get kyberPreKeySignature => $_getN(4);
@$pb.TagNumber(5)
set kyberPreKeySignature($core.List<$core.int> value) => $_setBytes(4, value);
@$pb.TagNumber(5)
$core.bool hasKyberPreKeySignature() => $_has(4);
@$pb.TagNumber(5)
void clearKyberPreKeySignature() => $_clearField(5);
}
class ApplicationData_UploadPqcPreKeys extends $pb.GeneratedMessage {
factory ApplicationData_UploadPqcPreKeys({
$fixnum.Int64? eccSignedPrekeyId,
$core.List<$core.int>? eccSignedPrekey,
$core.List<$core.int>? eccSignedPrekeySignature,
$fixnum.Int64? kyberSignedPrekeyId,
$core.List<$core.int>? kyberSignedPrekey,
$core.List<$core.int>? kyberSignedPrekeySignature,
$core.Iterable<ApplicationData_PqcPreKey>? prekeys,
}) {
final result = create();
if (eccSignedPrekeyId != null) result.eccSignedPrekeyId = eccSignedPrekeyId;
if (eccSignedPrekey != null) result.eccSignedPrekey = eccSignedPrekey;
if (eccSignedPrekeySignature != null)
result.eccSignedPrekeySignature = eccSignedPrekeySignature;
if (kyberSignedPrekeyId != null)
result.kyberSignedPrekeyId = kyberSignedPrekeyId;
if (kyberSignedPrekey != null) result.kyberSignedPrekey = kyberSignedPrekey;
if (kyberSignedPrekeySignature != null)
result.kyberSignedPrekeySignature = kyberSignedPrekeySignature;
if (prekeys != null) result.prekeys.addAll(prekeys);
return result;
}
ApplicationData_UploadPqcPreKeys._();
factory ApplicationData_UploadPqcPreKeys.fromBuffer(
$core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory ApplicationData_UploadPqcPreKeys.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'ApplicationData.UploadPqcPreKeys',
package:
const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'),
createEmptyInstance: create)
..aInt64(1, _omitFieldNames ? '' : 'eccSignedPrekeyId')
..a<$core.List<$core.int>>(
2, _omitFieldNames ? '' : 'eccSignedPrekey', $pb.PbFieldType.OY)
..a<$core.List<$core.int>>(3,
_omitFieldNames ? '' : 'eccSignedPrekeySignature', $pb.PbFieldType.OY)
..aInt64(4, _omitFieldNames ? '' : 'kyberSignedPrekeyId')
..a<$core.List<$core.int>>(
5, _omitFieldNames ? '' : 'kyberSignedPrekey', $pb.PbFieldType.OY)
..a<$core.List<$core.int>>(6,
_omitFieldNames ? '' : 'kyberSignedPrekeySignature', $pb.PbFieldType.OY)
..pPM<ApplicationData_PqcPreKey>(7, _omitFieldNames ? '' : 'prekeys',
subBuilder: ApplicationData_PqcPreKey.create)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
ApplicationData_UploadPqcPreKeys clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
ApplicationData_UploadPqcPreKeys copyWith(
void Function(ApplicationData_UploadPqcPreKeys) updates) =>
super.copyWith(
(message) => updates(message as ApplicationData_UploadPqcPreKeys))
as ApplicationData_UploadPqcPreKeys;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static ApplicationData_UploadPqcPreKeys create() =>
ApplicationData_UploadPqcPreKeys._();
@$core.override
ApplicationData_UploadPqcPreKeys createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static ApplicationData_UploadPqcPreKeys getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<ApplicationData_UploadPqcPreKeys>(
create);
static ApplicationData_UploadPqcPreKeys? _defaultInstance;
@$pb.TagNumber(1)
$fixnum.Int64 get eccSignedPrekeyId => $_getI64(0);
@$pb.TagNumber(1)
set eccSignedPrekeyId($fixnum.Int64 value) => $_setInt64(0, value);
@$pb.TagNumber(1)
$core.bool hasEccSignedPrekeyId() => $_has(0);
@$pb.TagNumber(1)
void clearEccSignedPrekeyId() => $_clearField(1);
@$pb.TagNumber(2)
$core.List<$core.int> get eccSignedPrekey => $_getN(1);
@$pb.TagNumber(2)
set eccSignedPrekey($core.List<$core.int> value) => $_setBytes(1, value);
@$pb.TagNumber(2)
$core.bool hasEccSignedPrekey() => $_has(1);
@$pb.TagNumber(2)
void clearEccSignedPrekey() => $_clearField(2);
@$pb.TagNumber(3)
$core.List<$core.int> get eccSignedPrekeySignature => $_getN(2);
@$pb.TagNumber(3)
set eccSignedPrekeySignature($core.List<$core.int> value) =>
$_setBytes(2, value);
@$pb.TagNumber(3)
$core.bool hasEccSignedPrekeySignature() => $_has(2);
@$pb.TagNumber(3)
void clearEccSignedPrekeySignature() => $_clearField(3);
@$pb.TagNumber(4)
$fixnum.Int64 get kyberSignedPrekeyId => $_getI64(3);
@$pb.TagNumber(4)
set kyberSignedPrekeyId($fixnum.Int64 value) => $_setInt64(3, value);
@$pb.TagNumber(4)
$core.bool hasKyberSignedPrekeyId() => $_has(3);
@$pb.TagNumber(4)
void clearKyberSignedPrekeyId() => $_clearField(4);
@$pb.TagNumber(5)
$core.List<$core.int> get kyberSignedPrekey => $_getN(4);
@$pb.TagNumber(5)
set kyberSignedPrekey($core.List<$core.int> value) => $_setBytes(4, value);
@$pb.TagNumber(5)
$core.bool hasKyberSignedPrekey() => $_has(4);
@$pb.TagNumber(5)
void clearKyberSignedPrekey() => $_clearField(5);
@$pb.TagNumber(6)
$core.List<$core.int> get kyberSignedPrekeySignature => $_getN(5);
@$pb.TagNumber(6)
set kyberSignedPrekeySignature($core.List<$core.int> value) =>
$_setBytes(5, value);
@$pb.TagNumber(6)
$core.bool hasKyberSignedPrekeySignature() => $_has(5);
@$pb.TagNumber(6)
void clearKyberSignedPrekeySignature() => $_clearField(6);
@$pb.TagNumber(7)
$pb.PbList<ApplicationData_PqcPreKey> get prekeys => $_getList(6);
}
class ApplicationData_DownloadDone extends $pb.GeneratedMessage {
factory ApplicationData_DownloadDone({
$core.List<$core.int>? downloadToken,
@ -3085,6 +3329,7 @@ enum ApplicationData_ApplicationData {
getMemoriesUsage,
deleteMemory,
disableMemoriesBackup,
uploadPqcPrekeys,
notSet
}
@ -3117,6 +3362,7 @@ class ApplicationData extends $pb.GeneratedMessage {
ApplicationData_GetMemoriesUsage? getMemoriesUsage,
ApplicationData_DeleteMemory? deleteMemory,
ApplicationData_DisableMemoriesBackup? disableMemoriesBackup,
ApplicationData_UploadPqcPreKeys? uploadPqcPrekeys,
}) {
final result = create();
if (textMessage != null) result.textMessage = textMessage;
@ -3157,6 +3403,7 @@ class ApplicationData extends $pb.GeneratedMessage {
if (deleteMemory != null) result.deleteMemory = deleteMemory;
if (disableMemoriesBackup != null)
result.disableMemoriesBackup = disableMemoriesBackup;
if (uploadPqcPrekeys != null) result.uploadPqcPrekeys = uploadPqcPrekeys;
return result;
}
@ -3198,6 +3445,7 @@ class ApplicationData extends $pb.GeneratedMessage {
37: ApplicationData_ApplicationData.getMemoriesUsage,
38: ApplicationData_ApplicationData.deleteMemory,
39: ApplicationData_ApplicationData.disableMemoriesBackup,
40: ApplicationData_ApplicationData.uploadPqcPrekeys,
0: ApplicationData_ApplicationData.notSet
};
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
@ -3232,7 +3480,8 @@ class ApplicationData extends $pb.GeneratedMessage {
36,
37,
38,
39
39,
40
])
..aOM<ApplicationData_TextMessage>(1, _omitFieldNames ? '' : 'textMessage',
protoName: 'textMessage',
@ -3327,6 +3576,9 @@ class ApplicationData extends $pb.GeneratedMessage {
..aOM<ApplicationData_DisableMemoriesBackup>(
39, _omitFieldNames ? '' : 'disableMemoriesBackup',
subBuilder: ApplicationData_DisableMemoriesBackup.create)
..aOM<ApplicationData_UploadPqcPreKeys>(
40, _omitFieldNames ? '' : 'uploadPqcPrekeys',
subBuilder: ApplicationData_UploadPqcPreKeys.create)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
@ -3375,6 +3627,7 @@ class ApplicationData extends $pb.GeneratedMessage {
@$pb.TagNumber(37)
@$pb.TagNumber(38)
@$pb.TagNumber(39)
@$pb.TagNumber(40)
ApplicationData_ApplicationData whichApplicationData() =>
_ApplicationData_ApplicationDataByTag[$_whichOneof(0)]!;
@$pb.TagNumber(1)
@ -3404,6 +3657,7 @@ class ApplicationData extends $pb.GeneratedMessage {
@$pb.TagNumber(37)
@$pb.TagNumber(38)
@$pb.TagNumber(39)
@$pb.TagNumber(40)
void clearApplicationData() => $_clearField($_whichOneof(0));
@$pb.TagNumber(1)
@ -3737,6 +3991,18 @@ class ApplicationData extends $pb.GeneratedMessage {
@$pb.TagNumber(39)
ApplicationData_DisableMemoriesBackup ensureDisableMemoriesBackup() =>
$_ensure(26);
@$pb.TagNumber(40)
ApplicationData_UploadPqcPreKeys get uploadPqcPrekeys => $_getN(27);
@$pb.TagNumber(40)
set uploadPqcPrekeys(ApplicationData_UploadPqcPreKeys value) =>
$_setField(40, value);
@$pb.TagNumber(40)
$core.bool hasUploadPqcPrekeys() => $_has(27);
@$pb.TagNumber(40)
void clearUploadPqcPrekeys() => $_clearField(40);
@$pb.TagNumber(40)
ApplicationData_UploadPqcPreKeys ensureUploadPqcPrekeys() => $_ensure(27);
}
class Response_PreKey extends $pb.GeneratedMessage {
@ -3857,16 +4123,68 @@ class Response_Prekeys extends $pb.GeneratedMessage {
$pb.PbList<Response_PreKey> get prekeys => $_getList(0);
}
enum Response_Ok_Ok { none, prekeys, notSet }
class Response_PqcPrekeys extends $pb.GeneratedMessage {
factory Response_PqcPrekeys({
$core.Iterable<ApplicationData_PqcPreKey>? prekeys,
}) {
final result = create();
if (prekeys != null) result.prekeys.addAll(prekeys);
return result;
}
Response_PqcPrekeys._();
factory Response_PqcPrekeys.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory Response_PqcPrekeys.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'Response.PqcPrekeys',
package:
const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'),
createEmptyInstance: create)
..pPM<ApplicationData_PqcPreKey>(1, _omitFieldNames ? '' : 'prekeys',
subBuilder: ApplicationData_PqcPreKey.create)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
Response_PqcPrekeys clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
Response_PqcPrekeys copyWith(void Function(Response_PqcPrekeys) updates) =>
super.copyWith((message) => updates(message as Response_PqcPrekeys))
as Response_PqcPrekeys;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static Response_PqcPrekeys create() => Response_PqcPrekeys._();
@$core.override
Response_PqcPrekeys createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static Response_PqcPrekeys getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<Response_PqcPrekeys>(create);
static Response_PqcPrekeys? _defaultInstance;
@$pb.TagNumber(1)
$pb.PbList<ApplicationData_PqcPreKey> get prekeys => $_getList(0);
}
enum Response_Ok_Ok { none, prekeys, prekeysPqc, notSet }
class Response_Ok extends $pb.GeneratedMessage {
factory Response_Ok({
$core.bool? none,
Response_Prekeys? prekeys,
Response_PqcPrekeys? prekeysPqc,
}) {
final result = create();
if (none != null) result.none = none;
if (prekeys != null) result.prekeys = prekeys;
if (prekeysPqc != null) result.prekeysPqc = prekeysPqc;
return result;
}
@ -3882,6 +4200,7 @@ class Response_Ok extends $pb.GeneratedMessage {
static const $core.Map<$core.int, Response_Ok_Ok> _Response_Ok_OkByTag = {
1: Response_Ok_Ok.none,
2: Response_Ok_Ok.prekeys,
3: Response_Ok_Ok.prekeysPqc,
0: Response_Ok_Ok.notSet
};
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
@ -3889,10 +4208,12 @@ class Response_Ok extends $pb.GeneratedMessage {
package:
const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'),
createEmptyInstance: create)
..oo(0, [1, 2])
..oo(0, [1, 2, 3])
..aOB(1, _omitFieldNames ? '' : 'None', protoName: 'None')
..aOM<Response_Prekeys>(2, _omitFieldNames ? '' : 'prekeys',
subBuilder: Response_Prekeys.create)
..aOM<Response_PqcPrekeys>(3, _omitFieldNames ? '' : 'prekeysPqc',
subBuilder: Response_PqcPrekeys.create)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
@ -3916,9 +4237,11 @@ class Response_Ok extends $pb.GeneratedMessage {
@$pb.TagNumber(1)
@$pb.TagNumber(2)
@$pb.TagNumber(3)
Response_Ok_Ok whichOk() => _Response_Ok_OkByTag[$_whichOneof(0)]!;
@$pb.TagNumber(1)
@$pb.TagNumber(2)
@$pb.TagNumber(3)
void clearOk() => $_clearField($_whichOneof(0));
@$pb.TagNumber(1)
@ -3940,6 +4263,17 @@ class Response_Ok extends $pb.GeneratedMessage {
void clearPrekeys() => $_clearField(2);
@$pb.TagNumber(2)
Response_Prekeys ensurePrekeys() => $_ensure(1);
@$pb.TagNumber(3)
Response_PqcPrekeys get prekeysPqc => $_getN(2);
@$pb.TagNumber(3)
set prekeysPqc(Response_PqcPrekeys value) => $_setField(3, value);
@$pb.TagNumber(3)
$core.bool hasPrekeysPqc() => $_has(2);
@$pb.TagNumber(3)
void clearPrekeysPqc() => $_clearField(3);
@$pb.TagNumber(3)
Response_PqcPrekeys ensurePrekeysPqc() => $_ensure(2);
}
enum Response_Response { ok, error, notSet }

View file

@ -724,6 +724,15 @@ const ApplicationData$json = {
'9': 0,
'10': 'disableMemoriesBackup'
},
{
'1': 'upload_pqc_prekeys',
'3': 40,
'4': 1,
'5': 11,
'6': '.client_to_server.ApplicationData.UploadPqcPreKeys',
'9': 0,
'10': 'uploadPqcPrekeys'
},
],
'3': [
ApplicationData_TextMessage$json,
@ -737,6 +746,8 @@ const ApplicationData$json = {
ApplicationData_GetPrekeysByUserId$json,
ApplicationData_GetSignedPreKeyByUserId$json,
ApplicationData_UpdateSignedPreKey$json,
ApplicationData_PqcPreKey$json,
ApplicationData_UploadPqcPreKeys$json,
ApplicationData_DownloadDone$json,
ApplicationData_ReportUser$json,
ApplicationData_IPAPurchase$json,
@ -868,6 +879,81 @@ const ApplicationData_UpdateSignedPreKey$json = {
],
};
@$core.Deprecated('Use applicationDataDescriptor instead')
const ApplicationData_PqcPreKey$json = {
'1': 'PqcPreKey',
'2': [
{'1': 'ecc_pre_key_id', '3': 1, '4': 1, '5': 3, '10': 'eccPreKeyId'},
{'1': 'ecc_pre_key', '3': 2, '4': 1, '5': 12, '10': 'eccPreKey'},
{'1': 'kyber_pre_key_id', '3': 3, '4': 1, '5': 3, '10': 'kyberPreKeyId'},
{'1': 'kyber_pre_key', '3': 4, '4': 1, '5': 12, '10': 'kyberPreKey'},
{
'1': 'kyber_pre_key_signature',
'3': 5,
'4': 1,
'5': 12,
'10': 'kyberPreKeySignature'
},
],
};
@$core.Deprecated('Use applicationDataDescriptor instead')
const ApplicationData_UploadPqcPreKeys$json = {
'1': 'UploadPqcPreKeys',
'2': [
{
'1': 'ecc_signed_prekey_id',
'3': 1,
'4': 1,
'5': 3,
'10': 'eccSignedPrekeyId'
},
{
'1': 'ecc_signed_prekey',
'3': 2,
'4': 1,
'5': 12,
'10': 'eccSignedPrekey'
},
{
'1': 'ecc_signed_prekey_signature',
'3': 3,
'4': 1,
'5': 12,
'10': 'eccSignedPrekeySignature'
},
{
'1': 'kyber_signed_prekey_id',
'3': 4,
'4': 1,
'5': 3,
'10': 'kyberSignedPrekeyId'
},
{
'1': 'kyber_signed_prekey',
'3': 5,
'4': 1,
'5': 12,
'10': 'kyberSignedPrekey'
},
{
'1': 'kyber_signed_prekey_signature',
'3': 6,
'4': 1,
'5': 12,
'10': 'kyberSignedPrekeySignature'
},
{
'1': 'prekeys',
'3': 7,
'4': 3,
'5': 11,
'6': '.client_to_server.ApplicationData.PqcPreKey',
'10': 'prekeys'
},
],
};
@$core.Deprecated('Use applicationDataDescriptor instead')
const ApplicationData_DownloadDone$json = {
'1': 'DownloadDone',
@ -1073,38 +1159,51 @@ final $typed_data.Uint8List applicationDataDescriptor = $convert.base64Decode(
'CzIuLmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkRlbGV0ZU1lbW9yeUgAUgxkZW'
'xldGVNZW1vcnkScQoXZGlzYWJsZV9tZW1vcmllc19iYWNrdXAYJyABKAsyNy5jbGllbnRfdG9f'
'c2VydmVyLkFwcGxpY2F0aW9uRGF0YS5EaXNhYmxlTWVtb3JpZXNCYWNrdXBIAFIVZGlzYWJsZU'
'1lbW9yaWVzQmFja3VwGmoKC1RleHRNZXNzYWdlEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBIS'
'CgRib2R5GAMgASgMUgRib2R5EiAKCXB1c2hfZGF0YRgEIAEoDEgAUghwdXNoRGF0YYgBAUIMCg'
'pfcHVzaF9kYXRhGi8KEUdldFVzZXJCeVVzZXJuYW1lEhoKCHVzZXJuYW1lGAEgASgJUgh1c2Vy'
'bmFtZRosCg5DaGFuZ2VVc2VybmFtZRIaCgh1c2VybmFtZRgBIAEoCVIIdXNlcm5hbWUaNQoUVX'
'BkYXRlR29vZ2xlRmNtVG9rZW4SHQoKZ29vZ2xlX2ZjbRgBIAEoCVIJZ29vZ2xlRmNtGiYKC0dl'
'dFVzZXJCeUlkEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBoTChFHZXRBdmFpbGFibGVQbGFucx'
'oVChNHZXRDdXJyZW50UGxhbkluZm9zGi8KFFJlbW92ZUFkZGl0aW9uYWxVc2VyEhcKB3VzZXJf'
'aWQYASABKANSBnVzZXJJZBotChJHZXRQcmVrZXlzQnlVc2VySWQSFwoHdXNlcl9pZBgBIAEoA1'
'IGdXNlcklkGjIKF0dldFNpZ25lZFByZUtleUJ5VXNlcklkEhcKB3VzZXJfaWQYASABKANSBnVz'
'ZXJJZBqbAQoSVXBkYXRlU2lnbmVkUHJlS2V5EigKEHNpZ25lZF9wcmVrZXlfaWQYASABKANSDn'
'NpZ25lZFByZWtleUlkEiMKDXNpZ25lZF9wcmVrZXkYAiABKAxSDHNpZ25lZFByZWtleRI2Chdz'
'aWduZWRfcHJla2V5X3NpZ25hdHVyZRgDIAEoDFIVc2lnbmVkUHJla2V5U2lnbmF0dXJlGjUKDE'
'Rvd25sb2FkRG9uZRIlCg5kb3dubG9hZF90b2tlbhgBIAEoDFINZG93bmxvYWRUb2tlbhpOCgpS'
'ZXBvcnRVc2VyEigKEHJlcG9ydGVkX3VzZXJfaWQYASABKANSDnJlcG9ydGVkVXNlcklkEhYKBn'
'JlYXNvbhgCIAEoCVIGcmVhc29uGnEKC0lQQVB1cmNoYXNlEh0KCnByb2R1Y3RfaWQYASABKAlS'
'CXByb2R1Y3RJZBIWCgZzb3VyY2UYAiABKAlSBnNvdXJjZRIrChF2ZXJpZmljYXRpb25fZGF0YR'
'gDIAEoCVIQdmVyaWZpY2F0aW9uRGF0YRoPCg1JUEFGb3JjZUNoZWNrGg8KDURlbGV0ZUFjY291'
'bnQaLAoRQWRkQWRkaXRpb25hbFVzZXISFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklkGjAKDVNldE'
'xvZ2luVG9rZW4SHwoLbG9naW5fdG9rZW4YASABKAxSCmxvZ2luVG9rZW4ajgEKHFJlZ2lzdGVy'
'UGFzc3dvcmRMZXNzUmVjb3ZlcnkSLgoSZW5jcnlwdGVkU2VydmVyS2V5GAEgASgMUhJlbmNyeX'
'B0ZWRTZXJ2ZXJLZXkSKwoOcGluVW5sb2NrVG9rZW4YAiABKAxIAFIOcGluVW5sb2NrVG9rZW6I'
'AQFCEQoPX3BpblVubG9ja1Rva2VuGnAKGFBhc3N3b3JkbGVzc05vdGlmaWNhdGlvbhInCg9ub3'
'RpZmljYXRpb25faWQYASABKAlSDm5vdGlmaWNhdGlvbklkEisKEWVuY3J5cHRlZF9tZXNzYWdl'
'GAIgASgMUhBlbmNyeXB0ZWRNZXNzYWdlGmsKFVJlcXVlc3RNZW1vcmllc1VwbG9hZBISCgRzaX'
'plGAEgASgDUgRzaXplEiMKDW9yaWdpbmFsX2RhdGUYAiABKANSDG9yaWdpbmFsRGF0ZRIZCght'
'ZWRpYV9pZBgDIAEoCVIHbWVkaWFJZBoyChVDb25maXJtTWVtb3JpZXNVcGxvYWQSGQoIbWVkaW'
'FfaWQYASABKAlSB21lZGlhSWQaSAoPR2V0TWVtb3JpZXNMaXN0Eh8KC29mZnNldF9kYXRlGAEg'
'ASgDUgpvZmZzZXREYXRlEhQKBWxpbWl0GAIgASgDUgVsaW1pdBpJCg5HZXRNZW1vcmllc1VybB'
'IZCghtZWRpYV9pZBgBIAEoCVIHbWVkaWFJZBIcCgl0aHVtYm5haWwYAiABKAhSCXRodW1ibmFp'
'bBoSChBHZXRNZW1vcmllc1VzYWdlGikKDERlbGV0ZU1lbW9yeRIZCghtZWRpYV9pZBgBIAEoCV'
'IHbWVkaWFJZBoXChVEaXNhYmxlTWVtb3JpZXNCYWNrdXBCEQoPQXBwbGljYXRpb25EYXRhSgQI'
'CRAKSgQICxAMSgQIDRASSgQIExAU');
'1lbW9yaWVzQmFja3VwEmIKEnVwbG9hZF9wcWNfcHJla2V5cxgoIAEoCzIyLmNsaWVudF90b19z'
'ZXJ2ZXIuQXBwbGljYXRpb25EYXRhLlVwbG9hZFBxY1ByZUtleXNIAFIQdXBsb2FkUHFjUHJla2'
'V5cxpqCgtUZXh0TWVzc2FnZRIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQSEgoEYm9keRgDIAEo'
'DFIEYm9keRIgCglwdXNoX2RhdGEYBCABKAxIAFIIcHVzaERhdGGIAQFCDAoKX3B1c2hfZGF0YR'
'ovChFHZXRVc2VyQnlVc2VybmFtZRIaCgh1c2VybmFtZRgBIAEoCVIIdXNlcm5hbWUaLAoOQ2hh'
'bmdlVXNlcm5hbWUSGgoIdXNlcm5hbWUYASABKAlSCHVzZXJuYW1lGjUKFFVwZGF0ZUdvb2dsZU'
'ZjbVRva2VuEh0KCmdvb2dsZV9mY20YASABKAlSCWdvb2dsZUZjbRomCgtHZXRVc2VyQnlJZBIX'
'Cgd1c2VyX2lkGAEgASgDUgZ1c2VySWQaEwoRR2V0QXZhaWxhYmxlUGxhbnMaFQoTR2V0Q3Vycm'
'VudFBsYW5JbmZvcxovChRSZW1vdmVBZGRpdGlvbmFsVXNlchIXCgd1c2VyX2lkGAEgASgDUgZ1'
'c2VySWQaLQoSR2V0UHJla2V5c0J5VXNlcklkEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBoyCh'
'dHZXRTaWduZWRQcmVLZXlCeVVzZXJJZBIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQamwEKElVw'
'ZGF0ZVNpZ25lZFByZUtleRIoChBzaWduZWRfcHJla2V5X2lkGAEgASgDUg5zaWduZWRQcmVrZX'
'lJZBIjCg1zaWduZWRfcHJla2V5GAIgASgMUgxzaWduZWRQcmVrZXkSNgoXc2lnbmVkX3ByZWtl'
'eV9zaWduYXR1cmUYAyABKAxSFXNpZ25lZFByZWtleVNpZ25hdHVyZRrUAQoJUHFjUHJlS2V5Ei'
'MKDmVjY19wcmVfa2V5X2lkGAEgASgDUgtlY2NQcmVLZXlJZBIeCgtlY2NfcHJlX2tleRgCIAEo'
'DFIJZWNjUHJlS2V5EicKEGt5YmVyX3ByZV9rZXlfaWQYAyABKANSDWt5YmVyUHJlS2V5SWQSIg'
'oNa3liZXJfcHJlX2tleRgEIAEoDFILa3liZXJQcmVLZXkSNQoXa3liZXJfcHJlX2tleV9zaWdu'
'YXR1cmUYBSABKAxSFGt5YmVyUHJlS2V5U2lnbmF0dXJlGp0DChBVcGxvYWRQcWNQcmVLZXlzEi'
'8KFGVjY19zaWduZWRfcHJla2V5X2lkGAEgASgDUhFlY2NTaWduZWRQcmVrZXlJZBIqChFlY2Nf'
'c2lnbmVkX3ByZWtleRgCIAEoDFIPZWNjU2lnbmVkUHJla2V5Ej0KG2VjY19zaWduZWRfcHJla2'
'V5X3NpZ25hdHVyZRgDIAEoDFIYZWNjU2lnbmVkUHJla2V5U2lnbmF0dXJlEjMKFmt5YmVyX3Np'
'Z25lZF9wcmVrZXlfaWQYBCABKANSE2t5YmVyU2lnbmVkUHJla2V5SWQSLgoTa3liZXJfc2lnbm'
'VkX3ByZWtleRgFIAEoDFIRa3liZXJTaWduZWRQcmVrZXkSQQoda3liZXJfc2lnbmVkX3ByZWtl'
'eV9zaWduYXR1cmUYBiABKAxSGmt5YmVyU2lnbmVkUHJla2V5U2lnbmF0dXJlEkUKB3ByZWtleX'
'MYByADKAsyKy5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5QcWNQcmVLZXlSB3By'
'ZWtleXMaNQoMRG93bmxvYWREb25lEiUKDmRvd25sb2FkX3Rva2VuGAEgASgMUg1kb3dubG9hZF'
'Rva2VuGk4KClJlcG9ydFVzZXISKAoQcmVwb3J0ZWRfdXNlcl9pZBgBIAEoA1IOcmVwb3J0ZWRV'
'c2VySWQSFgoGcmVhc29uGAIgASgJUgZyZWFzb24acQoLSVBBUHVyY2hhc2USHQoKcHJvZHVjdF'
'9pZBgBIAEoCVIJcHJvZHVjdElkEhYKBnNvdXJjZRgCIAEoCVIGc291cmNlEisKEXZlcmlmaWNh'
'dGlvbl9kYXRhGAMgASgJUhB2ZXJpZmljYXRpb25EYXRhGg8KDUlQQUZvcmNlQ2hlY2saDwoNRG'
'VsZXRlQWNjb3VudBosChFBZGRBZGRpdGlvbmFsVXNlchIXCgd1c2VyX2lkGAEgASgDUgZ1c2Vy'
'SWQaMAoNU2V0TG9naW5Ub2tlbhIfCgtsb2dpbl90b2tlbhgBIAEoDFIKbG9naW5Ub2tlbhqOAQ'
'ocUmVnaXN0ZXJQYXNzd29yZExlc3NSZWNvdmVyeRIuChJlbmNyeXB0ZWRTZXJ2ZXJLZXkYASAB'
'KAxSEmVuY3J5cHRlZFNlcnZlcktleRIrCg5waW5VbmxvY2tUb2tlbhgCIAEoDEgAUg5waW5Vbm'
'xvY2tUb2tlbogBAUIRCg9fcGluVW5sb2NrVG9rZW4acAoYUGFzc3dvcmRsZXNzTm90aWZpY2F0'
'aW9uEicKD25vdGlmaWNhdGlvbl9pZBgBIAEoCVIObm90aWZpY2F0aW9uSWQSKwoRZW5jcnlwdG'
'VkX21lc3NhZ2UYAiABKAxSEGVuY3J5cHRlZE1lc3NhZ2UaawoVUmVxdWVzdE1lbW9yaWVzVXBs'
'b2FkEhIKBHNpemUYASABKANSBHNpemUSIwoNb3JpZ2luYWxfZGF0ZRgCIAEoA1IMb3JpZ2luYW'
'xEYXRlEhkKCG1lZGlhX2lkGAMgASgJUgdtZWRpYUlkGjIKFUNvbmZpcm1NZW1vcmllc1VwbG9h'
'ZBIZCghtZWRpYV9pZBgBIAEoCVIHbWVkaWFJZBpICg9HZXRNZW1vcmllc0xpc3QSHwoLb2Zmc2'
'V0X2RhdGUYASABKANSCm9mZnNldERhdGUSFAoFbGltaXQYAiABKANSBWxpbWl0GkkKDkdldE1l'
'bW9yaWVzVXJsEhkKCG1lZGlhX2lkGAEgASgJUgdtZWRpYUlkEhwKCXRodW1ibmFpbBgCIAEoCF'
'IJdGh1bWJuYWlsGhIKEEdldE1lbW9yaWVzVXNhZ2UaKQoMRGVsZXRlTWVtb3J5EhkKCG1lZGlh'
'X2lkGAEgASgJUgdtZWRpYUlkGhcKFURpc2FibGVNZW1vcmllc0JhY2t1cEIRCg9BcHBsaWNhdG'
'lvbkRhdGFKBAgJEApKBAgLEAxKBAgNEBJKBAgTEBQ=');
@$core.Deprecated('Use responseDescriptor instead')
const Response$json = {
@ -1129,7 +1228,12 @@ const Response$json = {
'10': 'error'
},
],
'3': [Response_PreKey$json, Response_Prekeys$json, Response_Ok$json],
'3': [
Response_PreKey$json,
Response_Prekeys$json,
Response_PqcPrekeys$json,
Response_Ok$json
],
'8': [
{'1': 'Response'},
],
@ -1159,6 +1263,21 @@ const Response_Prekeys$json = {
],
};
@$core.Deprecated('Use responseDescriptor instead')
const Response_PqcPrekeys$json = {
'1': 'PqcPrekeys',
'2': [
{
'1': 'prekeys',
'3': 1,
'4': 3,
'5': 11,
'6': '.client_to_server.ApplicationData.PqcPreKey',
'10': 'prekeys'
},
],
};
@$core.Deprecated('Use responseDescriptor instead')
const Response_Ok$json = {
'1': 'Ok',
@ -1173,6 +1292,15 @@ const Response_Ok$json = {
'9': 0,
'10': 'prekeys'
},
{
'1': 'prekeys_pqc',
'3': 3,
'4': 1,
'5': 11,
'6': '.client_to_server.Response.PqcPrekeys',
'9': 0,
'10': 'prekeysPqc'
},
],
'8': [
{'1': 'Ok'},
@ -1185,5 +1313,8 @@ final $typed_data.Uint8List responseDescriptor = $convert.base64Decode(
'ICb2sSKAoFZXJyb3IYAiABKA4yEC5lcnJvci5FcnJvckNvZGVIAFIFZXJyb3IaMAoGUHJlS2V5'
'Eg4KAmlkGAEgASgDUgJpZBIWCgZwcmVrZXkYAiABKAxSBnByZWtleRpGCgdQcmVrZXlzEjsKB3'
'ByZWtleXMYASADKAsyIS5jbGllbnRfdG9fc2VydmVyLlJlc3BvbnNlLlByZUtleVIHcHJla2V5'
'cxpgCgJPaxIUCgROb25lGAEgASgISABSBE5vbmUSPgoHcHJla2V5cxgCIAEoCzIiLmNsaWVudF'
'90b19zZXJ2ZXIuUmVzcG9uc2UuUHJla2V5c0gAUgdwcmVrZXlzQgQKAk9rQgoKCFJlc3BvbnNl');
'cxpTCgpQcWNQcmVrZXlzEkUKB3ByZWtleXMYASADKAsyKy5jbGllbnRfdG9fc2VydmVyLkFwcG'
'xpY2F0aW9uRGF0YS5QcWNQcmVLZXlSB3ByZWtleXMaqgEKAk9rEhQKBE5vbmUYASABKAhIAFIE'
'Tm9uZRI+CgdwcmVrZXlzGAIgASgLMiIuY2xpZW50X3RvX3NlcnZlci5SZXNwb25zZS5QcmVrZX'
'lzSABSB3ByZWtleXMSSAoLcHJla2V5c19wcWMYAyABKAsyJS5jbGllbnRfdG9fc2VydmVyLlJl'
'c3BvbnNlLlBxY1ByZWtleXNIAFIKcHJla2V5c1BxY0IECgJPa0IKCghSZXNwb25zZQ==');

View file

@ -95,6 +95,7 @@ enum V0_Kind {
requestNewPreKeys,
error,
newMessages,
requestNewPqcPreKeys,
notSet
}
@ -106,6 +107,7 @@ class V0 extends $pb.GeneratedMessage {
$core.bool? requestNewPreKeys,
$0.ErrorCode? error,
NewMessages? newMessages,
$core.bool? requestNewPqcPreKeys,
}) {
final result = create();
if (seq != null) result.seq = seq;
@ -114,6 +116,8 @@ class V0 extends $pb.GeneratedMessage {
if (requestNewPreKeys != null) result.requestNewPreKeys = requestNewPreKeys;
if (error != null) result.error = error;
if (newMessages != null) result.newMessages = newMessages;
if (requestNewPqcPreKeys != null)
result.requestNewPqcPreKeys = requestNewPqcPreKeys;
return result;
}
@ -132,6 +136,7 @@ class V0 extends $pb.GeneratedMessage {
4: V0_Kind.requestNewPreKeys,
6: V0_Kind.error,
7: V0_Kind.newMessages,
8: V0_Kind.requestNewPqcPreKeys,
0: V0_Kind.notSet
};
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
@ -139,7 +144,7 @@ class V0 extends $pb.GeneratedMessage {
package:
const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'),
createEmptyInstance: create)
..oo(0, [2, 3, 4, 6, 7])
..oo(0, [2, 3, 4, 6, 7, 8])
..a<$fixnum.Int64>(1, _omitFieldNames ? '' : 'seq', $pb.PbFieldType.OU6,
defaultOrMaker: $fixnum.Int64.ZERO)
..aOM<Response>(2, _omitFieldNames ? '' : 'response',
@ -152,6 +157,8 @@ class V0 extends $pb.GeneratedMessage {
enumValues: $0.ErrorCode.values)
..aOM<NewMessages>(7, _omitFieldNames ? '' : 'newMessages',
protoName: 'newMessages', subBuilder: NewMessages.create)
..aOB(8, _omitFieldNames ? '' : 'RequestNewPqcPreKeys',
protoName: 'RequestNewPqcPreKeys')
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
@ -177,12 +184,14 @@ class V0 extends $pb.GeneratedMessage {
@$pb.TagNumber(4)
@$pb.TagNumber(6)
@$pb.TagNumber(7)
@$pb.TagNumber(8)
V0_Kind whichKind() => _V0_KindByTag[$_whichOneof(0)]!;
@$pb.TagNumber(2)
@$pb.TagNumber(3)
@$pb.TagNumber(4)
@$pb.TagNumber(6)
@$pb.TagNumber(7)
@$pb.TagNumber(8)
void clearKind() => $_clearField($_whichOneof(0));
@$pb.TagNumber(1)
@ -244,6 +253,15 @@ class V0 extends $pb.GeneratedMessage {
void clearNewMessages() => $_clearField(7);
@$pb.TagNumber(7)
NewMessages ensureNewMessages() => $_ensure(5);
@$pb.TagNumber(8)
$core.bool get requestNewPqcPreKeys => $_getBF(6);
@$pb.TagNumber(8)
set requestNewPqcPreKeys($core.bool value) => $_setBool(6, value);
@$pb.TagNumber(8)
$core.bool hasRequestNewPqcPreKeys() => $_has(6);
@$pb.TagNumber(8)
void clearRequestNewPqcPreKeys() => $_clearField(8);
}
class NewMessage extends $pb.GeneratedMessage {
@ -830,45 +848,6 @@ class Response_AdditionalAccount extends $pb.GeneratedMessage {
void clearPlanId() => $_clearField(3);
}
class Response_Deprecated extends $pb.GeneratedMessage {
factory Response_Deprecated() => create();
Response_Deprecated._();
factory Response_Deprecated.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory Response_Deprecated.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'Response.Deprecated',
package:
const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'),
createEmptyInstance: create)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
Response_Deprecated clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
Response_Deprecated copyWith(void Function(Response_Deprecated) updates) =>
super.copyWith((message) => updates(message as Response_Deprecated))
as Response_Deprecated;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static Response_Deprecated create() => Response_Deprecated._();
@$core.override
Response_Deprecated createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static Response_Deprecated getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<Response_Deprecated>(create);
static Response_Deprecated? _defaultInstance;
}
class Response_Transaction extends $pb.GeneratedMessage {
factory Response_Transaction() => create();
@ -1196,6 +1175,252 @@ class Response_SignedPreKey extends $pb.GeneratedMessage {
void clearSignedPrekeySignature() => $_clearField(3);
}
class Response_PqcPreKey extends $pb.GeneratedMessage {
factory Response_PqcPreKey({
$fixnum.Int64? eccPreKeyId,
$core.List<$core.int>? eccPreKey,
$fixnum.Int64? kyberPreKeyId,
$core.List<$core.int>? kyberPreKey,
$core.List<$core.int>? kyberPreKeySignature,
}) {
final result = create();
if (eccPreKeyId != null) result.eccPreKeyId = eccPreKeyId;
if (eccPreKey != null) result.eccPreKey = eccPreKey;
if (kyberPreKeyId != null) result.kyberPreKeyId = kyberPreKeyId;
if (kyberPreKey != null) result.kyberPreKey = kyberPreKey;
if (kyberPreKeySignature != null)
result.kyberPreKeySignature = kyberPreKeySignature;
return result;
}
Response_PqcPreKey._();
factory Response_PqcPreKey.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory Response_PqcPreKey.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'Response.PqcPreKey',
package:
const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'),
createEmptyInstance: create)
..aInt64(1, _omitFieldNames ? '' : 'eccPreKeyId')
..a<$core.List<$core.int>>(
2, _omitFieldNames ? '' : 'eccPreKey', $pb.PbFieldType.OY)
..aInt64(3, _omitFieldNames ? '' : 'kyberPreKeyId')
..a<$core.List<$core.int>>(
4, _omitFieldNames ? '' : 'kyberPreKey', $pb.PbFieldType.OY)
..a<$core.List<$core.int>>(
5, _omitFieldNames ? '' : 'kyberPreKeySignature', $pb.PbFieldType.OY)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
Response_PqcPreKey clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
Response_PqcPreKey copyWith(void Function(Response_PqcPreKey) updates) =>
super.copyWith((message) => updates(message as Response_PqcPreKey))
as Response_PqcPreKey;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static Response_PqcPreKey create() => Response_PqcPreKey._();
@$core.override
Response_PqcPreKey createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static Response_PqcPreKey getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<Response_PqcPreKey>(create);
static Response_PqcPreKey? _defaultInstance;
@$pb.TagNumber(1)
$fixnum.Int64 get eccPreKeyId => $_getI64(0);
@$pb.TagNumber(1)
set eccPreKeyId($fixnum.Int64 value) => $_setInt64(0, value);
@$pb.TagNumber(1)
$core.bool hasEccPreKeyId() => $_has(0);
@$pb.TagNumber(1)
void clearEccPreKeyId() => $_clearField(1);
@$pb.TagNumber(2)
$core.List<$core.int> get eccPreKey => $_getN(1);
@$pb.TagNumber(2)
set eccPreKey($core.List<$core.int> value) => $_setBytes(1, value);
@$pb.TagNumber(2)
$core.bool hasEccPreKey() => $_has(1);
@$pb.TagNumber(2)
void clearEccPreKey() => $_clearField(2);
@$pb.TagNumber(3)
$fixnum.Int64 get kyberPreKeyId => $_getI64(2);
@$pb.TagNumber(3)
set kyberPreKeyId($fixnum.Int64 value) => $_setInt64(2, value);
@$pb.TagNumber(3)
$core.bool hasKyberPreKeyId() => $_has(2);
@$pb.TagNumber(3)
void clearKyberPreKeyId() => $_clearField(3);
@$pb.TagNumber(4)
$core.List<$core.int> get kyberPreKey => $_getN(3);
@$pb.TagNumber(4)
set kyberPreKey($core.List<$core.int> value) => $_setBytes(3, value);
@$pb.TagNumber(4)
$core.bool hasKyberPreKey() => $_has(3);
@$pb.TagNumber(4)
void clearKyberPreKey() => $_clearField(4);
@$pb.TagNumber(5)
$core.List<$core.int> get kyberPreKeySignature => $_getN(4);
@$pb.TagNumber(5)
set kyberPreKeySignature($core.List<$core.int> value) => $_setBytes(4, value);
@$pb.TagNumber(5)
$core.bool hasKyberPreKeySignature() => $_has(4);
@$pb.TagNumber(5)
void clearKyberPreKeySignature() => $_clearField(5);
}
class Response_PqcBundle extends $pb.GeneratedMessage {
factory Response_PqcBundle({
$fixnum.Int64? eccSignedPrekeyId,
$core.List<$core.int>? eccSignedPrekey,
$core.List<$core.int>? eccSignedPrekeySignature,
$fixnum.Int64? kyberSignedPrekeyId,
$core.List<$core.int>? kyberSignedPrekey,
$core.List<$core.int>? kyberSignedPrekeySignature,
Response_PqcPreKey? prekey,
}) {
final result = create();
if (eccSignedPrekeyId != null) result.eccSignedPrekeyId = eccSignedPrekeyId;
if (eccSignedPrekey != null) result.eccSignedPrekey = eccSignedPrekey;
if (eccSignedPrekeySignature != null)
result.eccSignedPrekeySignature = eccSignedPrekeySignature;
if (kyberSignedPrekeyId != null)
result.kyberSignedPrekeyId = kyberSignedPrekeyId;
if (kyberSignedPrekey != null) result.kyberSignedPrekey = kyberSignedPrekey;
if (kyberSignedPrekeySignature != null)
result.kyberSignedPrekeySignature = kyberSignedPrekeySignature;
if (prekey != null) result.prekey = prekey;
return result;
}
Response_PqcBundle._();
factory Response_PqcBundle.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory Response_PqcBundle.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'Response.PqcBundle',
package:
const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'),
createEmptyInstance: create)
..aInt64(1, _omitFieldNames ? '' : 'eccSignedPrekeyId')
..a<$core.List<$core.int>>(
2, _omitFieldNames ? '' : 'eccSignedPrekey', $pb.PbFieldType.OY)
..a<$core.List<$core.int>>(3,
_omitFieldNames ? '' : 'eccSignedPrekeySignature', $pb.PbFieldType.OY)
..aInt64(4, _omitFieldNames ? '' : 'kyberSignedPrekeyId')
..a<$core.List<$core.int>>(
5, _omitFieldNames ? '' : 'kyberSignedPrekey', $pb.PbFieldType.OY)
..a<$core.List<$core.int>>(6,
_omitFieldNames ? '' : 'kyberSignedPrekeySignature', $pb.PbFieldType.OY)
..aOM<Response_PqcPreKey>(7, _omitFieldNames ? '' : 'prekey',
subBuilder: Response_PqcPreKey.create)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
Response_PqcBundle clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
Response_PqcBundle copyWith(void Function(Response_PqcBundle) updates) =>
super.copyWith((message) => updates(message as Response_PqcBundle))
as Response_PqcBundle;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static Response_PqcBundle create() => Response_PqcBundle._();
@$core.override
Response_PqcBundle createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static Response_PqcBundle getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<Response_PqcBundle>(create);
static Response_PqcBundle? _defaultInstance;
@$pb.TagNumber(1)
$fixnum.Int64 get eccSignedPrekeyId => $_getI64(0);
@$pb.TagNumber(1)
set eccSignedPrekeyId($fixnum.Int64 value) => $_setInt64(0, value);
@$pb.TagNumber(1)
$core.bool hasEccSignedPrekeyId() => $_has(0);
@$pb.TagNumber(1)
void clearEccSignedPrekeyId() => $_clearField(1);
@$pb.TagNumber(2)
$core.List<$core.int> get eccSignedPrekey => $_getN(1);
@$pb.TagNumber(2)
set eccSignedPrekey($core.List<$core.int> value) => $_setBytes(1, value);
@$pb.TagNumber(2)
$core.bool hasEccSignedPrekey() => $_has(1);
@$pb.TagNumber(2)
void clearEccSignedPrekey() => $_clearField(2);
@$pb.TagNumber(3)
$core.List<$core.int> get eccSignedPrekeySignature => $_getN(2);
@$pb.TagNumber(3)
set eccSignedPrekeySignature($core.List<$core.int> value) =>
$_setBytes(2, value);
@$pb.TagNumber(3)
$core.bool hasEccSignedPrekeySignature() => $_has(2);
@$pb.TagNumber(3)
void clearEccSignedPrekeySignature() => $_clearField(3);
@$pb.TagNumber(4)
$fixnum.Int64 get kyberSignedPrekeyId => $_getI64(3);
@$pb.TagNumber(4)
set kyberSignedPrekeyId($fixnum.Int64 value) => $_setInt64(3, value);
@$pb.TagNumber(4)
$core.bool hasKyberSignedPrekeyId() => $_has(3);
@$pb.TagNumber(4)
void clearKyberSignedPrekeyId() => $_clearField(4);
@$pb.TagNumber(5)
$core.List<$core.int> get kyberSignedPrekey => $_getN(4);
@$pb.TagNumber(5)
set kyberSignedPrekey($core.List<$core.int> value) => $_setBytes(4, value);
@$pb.TagNumber(5)
$core.bool hasKyberSignedPrekey() => $_has(4);
@$pb.TagNumber(5)
void clearKyberSignedPrekey() => $_clearField(5);
@$pb.TagNumber(6)
$core.List<$core.int> get kyberSignedPrekeySignature => $_getN(5);
@$pb.TagNumber(6)
set kyberSignedPrekeySignature($core.List<$core.int> value) =>
$_setBytes(5, value);
@$pb.TagNumber(6)
$core.bool hasKyberSignedPrekeySignature() => $_has(5);
@$pb.TagNumber(6)
void clearKyberSignedPrekeySignature() => $_clearField(6);
@$pb.TagNumber(7)
Response_PqcPreKey get prekey => $_getN(6);
@$pb.TagNumber(7)
set prekey(Response_PqcPreKey value) => $_setField(7, value);
@$pb.TagNumber(7)
$core.bool hasPrekey() => $_has(6);
@$pb.TagNumber(7)
void clearPrekey() => $_clearField(7);
@$pb.TagNumber(7)
Response_PqcPreKey ensurePrekey() => $_ensure(6);
}
class Response_UserData extends $pb.GeneratedMessage {
factory Response_UserData({
$fixnum.Int64? userId,
@ -1206,6 +1431,7 @@ class Response_UserData extends $pb.GeneratedMessage {
$fixnum.Int64? signedPrekeyId,
$core.List<$core.int>? username,
$fixnum.Int64? registrationId,
Response_PqcBundle? pqcBundle,
}) {
final result = create();
if (userId != null) result.userId = userId;
@ -1217,6 +1443,7 @@ class Response_UserData extends $pb.GeneratedMessage {
if (signedPrekeyId != null) result.signedPrekeyId = signedPrekeyId;
if (username != null) result.username = username;
if (registrationId != null) result.registrationId = registrationId;
if (pqcBundle != null) result.pqcBundle = pqcBundle;
return result;
}
@ -1247,6 +1474,8 @@ class Response_UserData extends $pb.GeneratedMessage {
..a<$core.List<$core.int>>(
7, _omitFieldNames ? '' : 'username', $pb.PbFieldType.OY)
..aInt64(8, _omitFieldNames ? '' : 'registrationId')
..aOM<Response_PqcBundle>(9, _omitFieldNames ? '' : 'pqcBundle',
subBuilder: Response_PqcBundle.create)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
@ -1334,6 +1563,17 @@ class Response_UserData extends $pb.GeneratedMessage {
$core.bool hasRegistrationId() => $_has(7);
@$pb.TagNumber(8)
void clearRegistrationId() => $_clearField(8);
@$pb.TagNumber(9)
Response_PqcBundle get pqcBundle => $_getN(8);
@$pb.TagNumber(9)
set pqcBundle(Response_PqcBundle value) => $_setField(9, value);
@$pb.TagNumber(9)
$core.bool hasPqcBundle() => $_has(8);
@$pb.TagNumber(9)
void clearPqcBundle() => $_clearField(9);
@$pb.TagNumber(9)
Response_PqcBundle ensurePqcBundle() => $_ensure(8);
}
class Response_UploadToken extends $pb.GeneratedMessage {
@ -2074,11 +2314,9 @@ enum Response_Ok_Ok {
uploadtoken,
userdata,
authtoken,
deprecated7,
authenticated,
plans,
planballance,
deprecated11,
addaccountsinvites,
downloadtokens,
signedprekey,
@ -2100,11 +2338,9 @@ class Response_Ok extends $pb.GeneratedMessage {
Response_UploadToken? uploadtoken,
Response_UserData? userdata,
$core.List<$core.int>? authtoken,
Response_Deprecated? deprecated7,
Response_Authenticated? authenticated,
Response_Plans? plans,
Response_PlanBallance? planballance,
Response_Deprecated? deprecated11,
Response_AddAccountsInvites? addaccountsinvites,
Response_DownloadTokens? downloadtokens,
Response_SignedPreKey? signedprekey,
@ -2123,11 +2359,9 @@ class Response_Ok extends $pb.GeneratedMessage {
if (uploadtoken != null) result.uploadtoken = uploadtoken;
if (userdata != null) result.userdata = userdata;
if (authtoken != null) result.authtoken = authtoken;
if (deprecated7 != null) result.deprecated7 = deprecated7;
if (authenticated != null) result.authenticated = authenticated;
if (plans != null) result.plans = plans;
if (planballance != null) result.planballance = planballance;
if (deprecated11 != null) result.deprecated11 = deprecated11;
if (addaccountsinvites != null)
result.addaccountsinvites = addaccountsinvites;
if (downloadtokens != null) result.downloadtokens = downloadtokens;
@ -2162,11 +2396,9 @@ class Response_Ok extends $pb.GeneratedMessage {
4: Response_Ok_Ok.uploadtoken,
5: Response_Ok_Ok.userdata,
6: Response_Ok_Ok.authtoken,
7: Response_Ok_Ok.deprecated7,
8: Response_Ok_Ok.authenticated,
9: Response_Ok_Ok.plans,
10: Response_Ok_Ok.planballance,
11: Response_Ok_Ok.deprecated11,
12: Response_Ok_Ok.addaccountsinvites,
13: Response_Ok_Ok.downloadtokens,
14: Response_Ok_Ok.signedprekey,
@ -2184,29 +2416,8 @@ class Response_Ok extends $pb.GeneratedMessage {
package:
const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'),
createEmptyInstance: create)
..oo(0, [
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21
])
..oo(
0, [1, 2, 3, 4, 5, 6, 8, 9, 10, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21])
..aOB(1, _omitFieldNames ? '' : 'None', protoName: 'None')
..aInt64(2, _omitFieldNames ? '' : 'userid')
..a<$core.List<$core.int>>(
@ -2217,16 +2428,12 @@ class Response_Ok extends $pb.GeneratedMessage {
subBuilder: Response_UserData.create)
..a<$core.List<$core.int>>(
6, _omitFieldNames ? '' : 'authtoken', $pb.PbFieldType.OY)
..aOM<Response_Deprecated>(7, _omitFieldNames ? '' : 'deprecated7',
protoName: 'deprecated_7', subBuilder: Response_Deprecated.create)
..aOM<Response_Authenticated>(8, _omitFieldNames ? '' : 'authenticated',
subBuilder: Response_Authenticated.create)
..aOM<Response_Plans>(9, _omitFieldNames ? '' : 'plans',
subBuilder: Response_Plans.create)
..aOM<Response_PlanBallance>(10, _omitFieldNames ? '' : 'planballance',
subBuilder: Response_PlanBallance.create)
..aOM<Response_Deprecated>(11, _omitFieldNames ? '' : 'deprecated11',
protoName: 'deprecated_11', subBuilder: Response_Deprecated.create)
..aOM<Response_AddAccountsInvites>(
12, _omitFieldNames ? '' : 'addaccountsinvites',
subBuilder: Response_AddAccountsInvites.create)
@ -2279,11 +2486,9 @@ class Response_Ok extends $pb.GeneratedMessage {
@$pb.TagNumber(4)
@$pb.TagNumber(5)
@$pb.TagNumber(6)
@$pb.TagNumber(7)
@$pb.TagNumber(8)
@$pb.TagNumber(9)
@$pb.TagNumber(10)
@$pb.TagNumber(11)
@$pb.TagNumber(12)
@$pb.TagNumber(13)
@$pb.TagNumber(14)
@ -2301,11 +2506,9 @@ class Response_Ok extends $pb.GeneratedMessage {
@$pb.TagNumber(4)
@$pb.TagNumber(5)
@$pb.TagNumber(6)
@$pb.TagNumber(7)
@$pb.TagNumber(8)
@$pb.TagNumber(9)
@$pb.TagNumber(10)
@$pb.TagNumber(11)
@$pb.TagNumber(12)
@$pb.TagNumber(13)
@$pb.TagNumber(14)
@ -2376,175 +2579,153 @@ class Response_Ok extends $pb.GeneratedMessage {
@$pb.TagNumber(6)
void clearAuthtoken() => $_clearField(6);
@$pb.TagNumber(7)
Response_Deprecated get deprecated7 => $_getN(6);
@$pb.TagNumber(7)
set deprecated7(Response_Deprecated value) => $_setField(7, value);
@$pb.TagNumber(7)
$core.bool hasDeprecated7() => $_has(6);
@$pb.TagNumber(7)
void clearDeprecated7() => $_clearField(7);
@$pb.TagNumber(7)
Response_Deprecated ensureDeprecated7() => $_ensure(6);
@$pb.TagNumber(8)
Response_Authenticated get authenticated => $_getN(7);
Response_Authenticated get authenticated => $_getN(6);
@$pb.TagNumber(8)
set authenticated(Response_Authenticated value) => $_setField(8, value);
@$pb.TagNumber(8)
$core.bool hasAuthenticated() => $_has(7);
$core.bool hasAuthenticated() => $_has(6);
@$pb.TagNumber(8)
void clearAuthenticated() => $_clearField(8);
@$pb.TagNumber(8)
Response_Authenticated ensureAuthenticated() => $_ensure(7);
Response_Authenticated ensureAuthenticated() => $_ensure(6);
@$pb.TagNumber(9)
Response_Plans get plans => $_getN(8);
Response_Plans get plans => $_getN(7);
@$pb.TagNumber(9)
set plans(Response_Plans value) => $_setField(9, value);
@$pb.TagNumber(9)
$core.bool hasPlans() => $_has(8);
$core.bool hasPlans() => $_has(7);
@$pb.TagNumber(9)
void clearPlans() => $_clearField(9);
@$pb.TagNumber(9)
Response_Plans ensurePlans() => $_ensure(8);
Response_Plans ensurePlans() => $_ensure(7);
@$pb.TagNumber(10)
Response_PlanBallance get planballance => $_getN(9);
Response_PlanBallance get planballance => $_getN(8);
@$pb.TagNumber(10)
set planballance(Response_PlanBallance value) => $_setField(10, value);
@$pb.TagNumber(10)
$core.bool hasPlanballance() => $_has(9);
$core.bool hasPlanballance() => $_has(8);
@$pb.TagNumber(10)
void clearPlanballance() => $_clearField(10);
@$pb.TagNumber(10)
Response_PlanBallance ensurePlanballance() => $_ensure(9);
@$pb.TagNumber(11)
Response_Deprecated get deprecated11 => $_getN(10);
@$pb.TagNumber(11)
set deprecated11(Response_Deprecated value) => $_setField(11, value);
@$pb.TagNumber(11)
$core.bool hasDeprecated11() => $_has(10);
@$pb.TagNumber(11)
void clearDeprecated11() => $_clearField(11);
@$pb.TagNumber(11)
Response_Deprecated ensureDeprecated11() => $_ensure(10);
Response_PlanBallance ensurePlanballance() => $_ensure(8);
@$pb.TagNumber(12)
Response_AddAccountsInvites get addaccountsinvites => $_getN(11);
Response_AddAccountsInvites get addaccountsinvites => $_getN(9);
@$pb.TagNumber(12)
set addaccountsinvites(Response_AddAccountsInvites value) =>
$_setField(12, value);
@$pb.TagNumber(12)
$core.bool hasAddaccountsinvites() => $_has(11);
$core.bool hasAddaccountsinvites() => $_has(9);
@$pb.TagNumber(12)
void clearAddaccountsinvites() => $_clearField(12);
@$pb.TagNumber(12)
Response_AddAccountsInvites ensureAddaccountsinvites() => $_ensure(11);
Response_AddAccountsInvites ensureAddaccountsinvites() => $_ensure(9);
@$pb.TagNumber(13)
Response_DownloadTokens get downloadtokens => $_getN(12);
Response_DownloadTokens get downloadtokens => $_getN(10);
@$pb.TagNumber(13)
set downloadtokens(Response_DownloadTokens value) => $_setField(13, value);
@$pb.TagNumber(13)
$core.bool hasDownloadtokens() => $_has(12);
$core.bool hasDownloadtokens() => $_has(10);
@$pb.TagNumber(13)
void clearDownloadtokens() => $_clearField(13);
@$pb.TagNumber(13)
Response_DownloadTokens ensureDownloadtokens() => $_ensure(12);
Response_DownloadTokens ensureDownloadtokens() => $_ensure(10);
@$pb.TagNumber(14)
Response_SignedPreKey get signedprekey => $_getN(13);
Response_SignedPreKey get signedprekey => $_getN(11);
@$pb.TagNumber(14)
set signedprekey(Response_SignedPreKey value) => $_setField(14, value);
@$pb.TagNumber(14)
$core.bool hasSignedprekey() => $_has(13);
$core.bool hasSignedprekey() => $_has(11);
@$pb.TagNumber(14)
void clearSignedprekey() => $_clearField(14);
@$pb.TagNumber(14)
Response_SignedPreKey ensureSignedprekey() => $_ensure(13);
Response_SignedPreKey ensureSignedprekey() => $_ensure(11);
@$pb.TagNumber(15)
Response_ProofOfWork get proofOfWork => $_getN(14);
Response_ProofOfWork get proofOfWork => $_getN(12);
@$pb.TagNumber(15)
set proofOfWork(Response_ProofOfWork value) => $_setField(15, value);
@$pb.TagNumber(15)
$core.bool hasProofOfWork() => $_has(14);
$core.bool hasProofOfWork() => $_has(12);
@$pb.TagNumber(15)
void clearProofOfWork() => $_clearField(15);
@$pb.TagNumber(15)
Response_ProofOfWork ensureProofOfWork() => $_ensure(14);
Response_ProofOfWork ensureProofOfWork() => $_ensure(12);
@$pb.TagNumber(16)
$core.List<$core.int> get passwordlessRecoveryServerKey => $_getN(15);
$core.List<$core.int> get passwordlessRecoveryServerKey => $_getN(13);
@$pb.TagNumber(16)
set passwordlessRecoveryServerKey($core.List<$core.int> value) =>
$_setBytes(15, value);
$_setBytes(13, value);
@$pb.TagNumber(16)
$core.bool hasPasswordlessRecoveryServerKey() => $_has(15);
$core.bool hasPasswordlessRecoveryServerKey() => $_has(13);
@$pb.TagNumber(16)
void clearPasswordlessRecoveryServerKey() => $_clearField(16);
@$pb.TagNumber(17)
Response_PasswordlessNotificationMessages
get passwordlessNotificationMessages => $_getN(16);
get passwordlessNotificationMessages => $_getN(14);
@$pb.TagNumber(17)
set passwordlessNotificationMessages(
Response_PasswordlessNotificationMessages value) =>
$_setField(17, value);
@$pb.TagNumber(17)
$core.bool hasPasswordlessNotificationMessages() => $_has(16);
$core.bool hasPasswordlessNotificationMessages() => $_has(14);
@$pb.TagNumber(17)
void clearPasswordlessNotificationMessages() => $_clearField(17);
@$pb.TagNumber(17)
Response_PasswordlessNotificationMessages
ensurePasswordlessNotificationMessages() => $_ensure(16);
ensurePasswordlessNotificationMessages() => $_ensure(14);
@$pb.TagNumber(18)
Response_MemoriesUploadUrls get memoriesUploadUrls => $_getN(17);
Response_MemoriesUploadUrls get memoriesUploadUrls => $_getN(15);
@$pb.TagNumber(18)
set memoriesUploadUrls(Response_MemoriesUploadUrls value) =>
$_setField(18, value);
@$pb.TagNumber(18)
$core.bool hasMemoriesUploadUrls() => $_has(17);
$core.bool hasMemoriesUploadUrls() => $_has(15);
@$pb.TagNumber(18)
void clearMemoriesUploadUrls() => $_clearField(18);
@$pb.TagNumber(18)
Response_MemoriesUploadUrls ensureMemoriesUploadUrls() => $_ensure(17);
Response_MemoriesUploadUrls ensureMemoriesUploadUrls() => $_ensure(15);
@$pb.TagNumber(19)
Response_MemoriesList get memoriesList => $_getN(18);
Response_MemoriesList get memoriesList => $_getN(16);
@$pb.TagNumber(19)
set memoriesList(Response_MemoriesList value) => $_setField(19, value);
@$pb.TagNumber(19)
$core.bool hasMemoriesList() => $_has(18);
$core.bool hasMemoriesList() => $_has(16);
@$pb.TagNumber(19)
void clearMemoriesList() => $_clearField(19);
@$pb.TagNumber(19)
Response_MemoriesList ensureMemoriesList() => $_ensure(18);
Response_MemoriesList ensureMemoriesList() => $_ensure(16);
@$pb.TagNumber(20)
Response_MemoriesUrl get memoriesUrl => $_getN(19);
Response_MemoriesUrl get memoriesUrl => $_getN(17);
@$pb.TagNumber(20)
set memoriesUrl(Response_MemoriesUrl value) => $_setField(20, value);
@$pb.TagNumber(20)
$core.bool hasMemoriesUrl() => $_has(19);
$core.bool hasMemoriesUrl() => $_has(17);
@$pb.TagNumber(20)
void clearMemoriesUrl() => $_clearField(20);
@$pb.TagNumber(20)
Response_MemoriesUrl ensureMemoriesUrl() => $_ensure(19);
Response_MemoriesUrl ensureMemoriesUrl() => $_ensure(17);
@$pb.TagNumber(21)
Response_MemoriesUsage get memoriesUsage => $_getN(20);
Response_MemoriesUsage get memoriesUsage => $_getN(18);
@$pb.TagNumber(21)
set memoriesUsage(Response_MemoriesUsage value) => $_setField(21, value);
@$pb.TagNumber(21)
$core.bool hasMemoriesUsage() => $_has(20);
$core.bool hasMemoriesUsage() => $_has(18);
@$pb.TagNumber(21)
void clearMemoriesUsage() => $_clearField(21);
@$pb.TagNumber(21)
Response_MemoriesUsage ensureMemoriesUsage() => $_ensure(20);
Response_MemoriesUsage ensureMemoriesUsage() => $_ensure(18);
}
enum Response_Response { ok, error, notSet }

View file

@ -79,6 +79,14 @@ const V0$json = {
'9': 0,
'10': 'RequestNewPreKeys'
},
{
'1': 'RequestNewPqcPreKeys',
'3': 8,
'4': 1,
'5': 8,
'9': 0,
'10': 'RequestNewPqcPreKeys'
},
{
'1': 'error',
'3': 6,
@ -100,8 +108,9 @@ final $typed_data.Uint8List v0Descriptor = $convert.base64Decode(
'llbnQuUmVzcG9uc2VIAFIIcmVzcG9uc2USPgoKbmV3TWVzc2FnZRgDIAEoCzIcLnNlcnZlcl90'
'b19jbGllbnQuTmV3TWVzc2FnZUgAUgpuZXdNZXNzYWdlEkEKC25ld01lc3NhZ2VzGAcgASgLMh'
'0uc2VydmVyX3RvX2NsaWVudC5OZXdNZXNzYWdlc0gAUgtuZXdNZXNzYWdlcxIuChFSZXF1ZXN0'
'TmV3UHJlS2V5cxgEIAEoCEgAUhFSZXF1ZXN0TmV3UHJlS2V5cxIoCgVlcnJvchgGIAEoDjIQLm'
'Vycm9yLkVycm9yQ29kZUgAUgVlcnJvckIGCgRLaW5k');
'TmV3UHJlS2V5cxgEIAEoCEgAUhFSZXF1ZXN0TmV3UHJlS2V5cxI0ChRSZXF1ZXN0TmV3UHFjUH'
'JlS2V5cxgIIAEoCEgAUhRSZXF1ZXN0TmV3UHFjUHJlS2V5cxIoCgVlcnJvchgGIAEoDjIQLmVy'
'cm9yLkVycm9yQ29kZUgAUgVlcnJvckIGCgRLaW5k');
@$core.Deprecated('Use newMessageDescriptor instead')
const NewMessage$json = {
@ -167,11 +176,12 @@ const Response$json = {
Response_AddAccountsInvite$json,
Response_AddAccountsInvites$json,
Response_AdditionalAccount$json,
Response_Deprecated$json,
Response_Transaction$json,
Response_PlanBallance$json,
Response_PreKey$json,
Response_SignedPreKey$json,
Response_PqcPreKey$json,
Response_PqcBundle$json,
Response_UserData$json,
Response_UploadToken$json,
Response_DownloadTokens$json,
@ -306,11 +316,6 @@ const Response_AdditionalAccount$json = {
],
};
@$core.Deprecated('Use responseDescriptor instead')
const Response_Deprecated$json = {
'1': 'Deprecated',
};
@$core.Deprecated('Use responseDescriptor instead')
const Response_Transaction$json = {
'1': 'Transaction',
@ -420,6 +425,86 @@ const Response_SignedPreKey$json = {
],
};
@$core.Deprecated('Use responseDescriptor instead')
const Response_PqcPreKey$json = {
'1': 'PqcPreKey',
'2': [
{'1': 'ecc_pre_key_id', '3': 1, '4': 1, '5': 3, '10': 'eccPreKeyId'},
{'1': 'ecc_pre_key', '3': 2, '4': 1, '5': 12, '10': 'eccPreKey'},
{'1': 'kyber_pre_key_id', '3': 3, '4': 1, '5': 3, '10': 'kyberPreKeyId'},
{'1': 'kyber_pre_key', '3': 4, '4': 1, '5': 12, '10': 'kyberPreKey'},
{
'1': 'kyber_pre_key_signature',
'3': 5,
'4': 1,
'5': 12,
'10': 'kyberPreKeySignature'
},
],
};
@$core.Deprecated('Use responseDescriptor instead')
const Response_PqcBundle$json = {
'1': 'PqcBundle',
'2': [
{
'1': 'ecc_signed_prekey_id',
'3': 1,
'4': 1,
'5': 3,
'10': 'eccSignedPrekeyId'
},
{
'1': 'ecc_signed_prekey',
'3': 2,
'4': 1,
'5': 12,
'10': 'eccSignedPrekey'
},
{
'1': 'ecc_signed_prekey_signature',
'3': 3,
'4': 1,
'5': 12,
'10': 'eccSignedPrekeySignature'
},
{
'1': 'kyber_signed_prekey_id',
'3': 4,
'4': 1,
'5': 3,
'10': 'kyberSignedPrekeyId'
},
{
'1': 'kyber_signed_prekey',
'3': 5,
'4': 1,
'5': 12,
'10': 'kyberSignedPrekey'
},
{
'1': 'kyber_signed_prekey_signature',
'3': 6,
'4': 1,
'5': 12,
'10': 'kyberSignedPrekeySignature'
},
{
'1': 'prekey',
'3': 7,
'4': 1,
'5': 11,
'6': '.server_to_client.Response.PqcPreKey',
'9': 0,
'10': 'prekey',
'17': true
},
],
'8': [
{'1': '_prekey'},
],
};
@$core.Deprecated('Use responseDescriptor instead')
const Response_UserData$json = {
'1': 'UserData',
@ -487,6 +572,16 @@ const Response_UserData$json = {
'10': 'registrationId',
'17': true
},
{
'1': 'pqc_bundle',
'3': 9,
'4': 1,
'5': 11,
'6': '.server_to_client.Response.PqcBundle',
'9': 6,
'10': 'pqcBundle',
'17': true
},
],
'8': [
{'1': '_username'},
@ -495,6 +590,7 @@ const Response_UserData$json = {
{'1': '_signed_prekey_signature'},
{'1': '_signed_prekey_id'},
{'1': '_registration_id'},
{'1': '_pqc_bundle'},
],
};
@ -687,15 +783,6 @@ const Response_Ok$json = {
'10': 'userdata'
},
{'1': 'authtoken', '3': 6, '4': 1, '5': 12, '9': 0, '10': 'authtoken'},
{
'1': 'deprecated_7',
'3': 7,
'4': 1,
'5': 11,
'6': '.server_to_client.Response.Deprecated',
'9': 0,
'10': 'deprecated7'
},
{
'1': 'authenticated',
'3': 8,
@ -723,15 +810,6 @@ const Response_Ok$json = {
'9': 0,
'10': 'planballance'
},
{
'1': 'deprecated_11',
'3': 11,
'4': 1,
'5': 11,
'6': '.server_to_client.Response.Deprecated',
'9': 0,
'10': 'deprecated11'
},
{
'1': 'addaccountsinvites',
'3': 12,
@ -825,6 +903,10 @@ const Response_Ok$json = {
'8': [
{'1': 'Ok'},
],
'9': [
{'1': 7, '2': 8},
{'1': 11, '2': 12},
],
};
/// Descriptor for `Response`. Decode as a `google.protobuf.DescriptorProto`.
@ -846,79 +928,89 @@ final $typed_data.Uint8List responseDescriptor = $convert.base64Decode(
'cGxhbklkEh8KC2ludml0ZV9jb2RlGAIgASgJUgppbnZpdGVDb2RlGlwKEkFkZEFjY291bnRzSW'
'52aXRlcxJGCgdpbnZpdGVzGAEgAygLMiwuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5BZGRB'
'Y2NvdW50c0ludml0ZVIHaW52aXRlcxpFChFBZGRpdGlvbmFsQWNjb3VudBIXCgd1c2VyX2lkGA'
'EgASgDUgZ1c2VySWQSFwoHcGxhbl9pZBgDIAEoCVIGcGxhbklkGgwKCkRlcHJlY2F0ZWQaDQoL'
'VHJhbnNhY3Rpb24alwUKDFBsYW5CYWxsYW5jZRJACh11c2VkX2RhaWx5X21lZGlhX3VwbG9hZF'
'9saW1pdBgBIAEoA1IZdXNlZERhaWx5TWVkaWFVcGxvYWRMaW1pdBI+Chx1c2VkX3VwbG9hZF9t'
'ZWRpYV9zaXplX2xpbWl0GAIgASgDUhh1c2VkVXBsb2FkTWVkaWFTaXplTGltaXQSMwoTcGF5bW'
'VudF9wZXJpb2RfZGF5cxgDIAEoA0gAUhFwYXltZW50UGVyaW9kRGF5c4gBARJLCiBsYXN0X3Bh'
'eW1lbnRfZG9uZV91bml4X3RpbWVzdGFtcBgEIAEoA0gBUhxsYXN0UGF5bWVudERvbmVVbml4VG'
'ltZXN0YW1wiAEBEkoKDHRyYW5zYWN0aW9ucxgFIAMoCzImLnNlcnZlcl90b19jbGllbnQuUmVz'
'cG9uc2UuVHJhbnNhY3Rpb25SDHRyYW5zYWN0aW9ucxJdChNhZGRpdGlvbmFsX2FjY291bnRzGA'
'YgAygLMiwuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5BZGRpdGlvbmFsQWNjb3VudFISYWRk'
'aXRpb25hbEFjY291bnRzEiYKDGF1dG9fcmVuZXdhbBgHIAEoCEgCUgthdXRvUmVuZXdhbIgBAR'
'JCChthZGRpdGlvbmFsX2FjY291bnRfb3duZXJfaWQYCCABKANIA1IYYWRkaXRpb25hbEFjY291'
'bnRPd25lcklkiAEBQhYKFF9wYXltZW50X3BlcmlvZF9kYXlzQiMKIV9sYXN0X3BheW1lbnRfZG'
'9uZV91bml4X3RpbWVzdGFtcEIPCg1fYXV0b19yZW5ld2FsQh4KHF9hZGRpdGlvbmFsX2FjY291'
'bnRfb3duZXJfaWQaMAoGUHJlS2V5Eg4KAmlkGAEgASgDUgJpZBIWCgZwcmVrZXkYAiABKAxSBn'
'ByZWtleRqVAQoMU2lnbmVkUHJlS2V5EigKEHNpZ25lZF9wcmVrZXlfaWQYASABKANSDnNpZ25l'
'ZFByZWtleUlkEiMKDXNpZ25lZF9wcmVrZXkYAiABKAxSDHNpZ25lZFByZWtleRI2ChdzaWduZW'
'RfcHJla2V5X3NpZ25hdHVyZRgDIAEoDFIVc2lnbmVkUHJla2V5U2lnbmF0dXJlGvYDCghVc2Vy'
'RGF0YRIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQSOwoHcHJla2V5cxgCIAMoCzIhLnNlcnZlcl'
'90b19jbGllbnQuUmVzcG9uc2UuUHJlS2V5UgdwcmVrZXlzEh8KCHVzZXJuYW1lGAcgASgMSABS'
'CHVzZXJuYW1liAEBEjMKE3B1YmxpY19pZGVudGl0eV9rZXkYAyABKAxIAVIRcHVibGljSWRlbn'
'RpdHlLZXmIAQESKAoNc2lnbmVkX3ByZWtleRgEIAEoDEgCUgxzaWduZWRQcmVrZXmIAQESOwoX'
'c2lnbmVkX3ByZWtleV9zaWduYXR1cmUYBSABKAxIA1IVc2lnbmVkUHJla2V5U2lnbmF0dXJliA'
'EBEi0KEHNpZ25lZF9wcmVrZXlfaWQYBiABKANIBFIOc2lnbmVkUHJla2V5SWSIAQESLAoPcmVn'
'aXN0cmF0aW9uX2lkGAggASgDSAVSDnJlZ2lzdHJhdGlvbklkiAEBQgsKCV91c2VybmFtZUIWCh'
'RfcHVibGljX2lkZW50aXR5X2tleUIQCg5fc2lnbmVkX3ByZWtleUIaChhfc2lnbmVkX3ByZWtl'
'eV9zaWduYXR1cmVCEwoRX3NpZ25lZF9wcmVrZXlfaWRCEgoQX3JlZ2lzdHJhdGlvbl9pZBpZCg'
'tVcGxvYWRUb2tlbhIhCgx1cGxvYWRfdG9rZW4YASABKAxSC3VwbG9hZFRva2VuEicKD2Rvd25s'
'b2FkX3Rva2VucxgCIAMoDFIOZG93bmxvYWRUb2tlbnMaOQoORG93bmxvYWRUb2tlbnMSJwoPZG'
'93bmxvYWRfdG9rZW5zGAEgAygMUg5kb3dubG9hZFRva2VucxpFCgtQcm9vZk9mV29yaxIWCgZw'
'cmVmaXgYASABKAlSBnByZWZpeBIeCgpkaWZmaWN1bHR5GAIgASgDUgpkaWZmaWN1bHR5Gl4KH1'
'Bhc3N3b3JkbGVzc05vdGlmaWNhdGlvbk1lc3NhZ2USDgoCaWQYASABKANSAmlkEisKEWVuY3J5'
'cHRlZF9tZXNzYWdlGAIgASgMUhBlbmNyeXB0ZWRNZXNzYWdlGnoKIFBhc3N3b3JkbGVzc05vdG'
'lmaWNhdGlvbk1lc3NhZ2VzElYKCG1lc3NhZ2VzGAEgAygLMjouc2VydmVyX3RvX2NsaWVudC5S'
'ZXNwb25zZS5QYXNzd29yZGxlc3NOb3RpZmljYXRpb25NZXNzYWdlUghtZXNzYWdlcxqqAQoNUH'
'Jlc2lnbmVkUG9zdBIQCgN1cmwYASABKAlSA3VybBJMCgZmaWVsZHMYAiADKAsyNC5zZXJ2ZXJf'
'dG9fY2xpZW50LlJlc3BvbnNlLlByZXNpZ25lZFBvc3QuRmllbGRzRW50cnlSBmZpZWxkcxo5Cg'
'tGaWVsZHNFbnRyeRIQCgNrZXkYASABKAlSA2tleRIUCgV2YWx1ZRgCIAEoCVIFdmFsdWU6AjgB'
'Gs8BChJNZW1vcmllc1VwbG9hZFVybHMSGQoIbWVkaWFfaWQYASABKAlSB21lZGlhSWQSUwoQdG'
'h1bWJuYWlsX3VwbG9hZBgCIAEoCzIoLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuUHJlc2ln'
'bmVkUG9zdFIPdGh1bWJuYWlsVXBsb2FkEkkKC2Z1bGxfdXBsb2FkGAMgASgLMiguc2VydmVyX3'
'RvX2NsaWVudC5SZXNwb25zZS5QcmVzaWduZWRQb3N0UgpmdWxsVXBsb2FkGoEBCglNZWRpYUl0'
'ZW0SGQoIbWVkaWFfaWQYASABKAlSB21lZGlhSWQSIwoNb3JpZ2luYWxfZGF0ZRgCIAEoA1IMb3'
'JpZ2luYWxEYXRlEjQKFnRodW1ibmFpbF9kb3dubG9hZF91cmwYAyABKAlSFHRodW1ibmFpbERv'
'd25sb2FkVXJsGkoKDE1lbW9yaWVzTGlzdBI6CgVpdGVtcxgBIAMoCzIkLnNlcnZlcl90b19jbG'
'llbnQuUmVzcG9uc2UuTWVkaWFJdGVtUgVpdGVtcxo5CgtNZW1vcmllc1VybBIqChFmdWxsX2Rv'
'd25sb2FkX3VybBgBIAEoCVIPZnVsbERvd25sb2FkVXJsGmcKDU1lbW9yaWVzVXNhZ2USGwoJbW'
'F4X2J5dGVzGAEgASgDUghtYXhCeXRlcxIjCg1jdXJyZW50X2J5dGVzGAIgASgDUgxjdXJyZW50'
'Qnl0ZXMSFAoFY291bnQYAyABKANSBWNvdW50GoMMCgJPaxIUCgROb25lGAEgASgISABSBE5vbm'
'USGAoGdXNlcmlkGAIgASgDSABSBnVzZXJpZBImCg1hdXRoY2hhbGxlbmdlGAMgASgMSABSDWF1'
'dGhjaGFsbGVuZ2USSgoLdXBsb2FkdG9rZW4YBCABKAsyJi5zZXJ2ZXJfdG9fY2xpZW50LlJlc3'
'BvbnNlLlVwbG9hZFRva2VuSABSC3VwbG9hZHRva2VuEkEKCHVzZXJkYXRhGAUgASgLMiMuc2Vy'
'dmVyX3RvX2NsaWVudC5SZXNwb25zZS5Vc2VyRGF0YUgAUgh1c2VyZGF0YRIeCglhdXRodG9rZW'
'4YBiABKAxIAFIJYXV0aHRva2VuEkoKDGRlcHJlY2F0ZWRfNxgHIAEoCzIlLnNlcnZlcl90b19j'
'bGllbnQuUmVzcG9uc2UuRGVwcmVjYXRlZEgAUgtkZXByZWNhdGVkNxJQCg1hdXRoZW50aWNhdG'
'VkGAggASgLMiguc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5BdXRoZW50aWNhdGVkSABSDWF1'
'dGhlbnRpY2F0ZWQSOAoFcGxhbnMYCSABKAsyIC5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLl'
'BsYW5zSABSBXBsYW5zEk0KDHBsYW5iYWxsYW5jZRgKIAEoCzInLnNlcnZlcl90b19jbGllbnQu'
'UmVzcG9uc2UuUGxhbkJhbGxhbmNlSABSDHBsYW5iYWxsYW5jZRJMCg1kZXByZWNhdGVkXzExGA'
'sgASgLMiUuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5EZXByZWNhdGVkSABSDGRlcHJlY2F0'
'ZWQxMRJfChJhZGRhY2NvdW50c2ludml0ZXMYDCABKAsyLS5zZXJ2ZXJfdG9fY2xpZW50LlJlc3'
'BvbnNlLkFkZEFjY291bnRzSW52aXRlc0gAUhJhZGRhY2NvdW50c2ludml0ZXMSUwoOZG93bmxv'
'YWR0b2tlbnMYDSABKAsyKS5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLkRvd25sb2FkVG9rZW'
'5zSABSDmRvd25sb2FkdG9rZW5zEk0KDHNpZ25lZHByZWtleRgOIAEoCzInLnNlcnZlcl90b19j'
'bGllbnQuUmVzcG9uc2UuU2lnbmVkUHJlS2V5SABSDHNpZ25lZHByZWtleRJKCgtwcm9vZk9mV2'
'9yaxgPIAEoCzImLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuUHJvb2ZPZldvcmtIAFILcHJv'
'b2ZPZldvcmsSSQogcGFzc3dvcmRsZXNzX3JlY292ZXJ5X3NlcnZlcl9rZXkYECABKAxIAFIdcG'
'Fzc3dvcmRsZXNzUmVjb3ZlcnlTZXJ2ZXJLZXkSiwEKInBhc3N3b3JkbGVzc19ub3RpZmljYXRp'
'b25fbWVzc2FnZXMYESABKAsyOy5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlBhc3N3b3JkbG'
'Vzc05vdGlmaWNhdGlvbk1lc3NhZ2VzSABSIHBhc3N3b3JkbGVzc05vdGlmaWNhdGlvbk1lc3Nh'
'Z2VzEmEKFG1lbW9yaWVzX3VwbG9hZF91cmxzGBIgASgLMi0uc2VydmVyX3RvX2NsaWVudC5SZX'
'Nwb25zZS5NZW1vcmllc1VwbG9hZFVybHNIAFISbWVtb3JpZXNVcGxvYWRVcmxzEk4KDW1lbW9y'
'aWVzX2xpc3QYEyABKAsyJy5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLk1lbW9yaWVzTGlzdE'
'gAUgxtZW1vcmllc0xpc3QSSwoMbWVtb3JpZXNfdXJsGBQgASgLMiYuc2VydmVyX3RvX2NsaWVu'
'dC5SZXNwb25zZS5NZW1vcmllc1VybEgAUgttZW1vcmllc1VybBJRCg5tZW1vcmllc191c2FnZR'
'gVIAEoCzIoLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuTWVtb3JpZXNVc2FnZUgAUg1tZW1v'
'cmllc1VzYWdlQgQKAk9rQgoKCFJlc3BvbnNl');
'EgASgDUgZ1c2VySWQSFwoHcGxhbl9pZBgDIAEoCVIGcGxhbklkGg0KC1RyYW5zYWN0aW9uGpcF'
'CgxQbGFuQmFsbGFuY2USQAoddXNlZF9kYWlseV9tZWRpYV91cGxvYWRfbGltaXQYASABKANSGX'
'VzZWREYWlseU1lZGlhVXBsb2FkTGltaXQSPgocdXNlZF91cGxvYWRfbWVkaWFfc2l6ZV9saW1p'
'dBgCIAEoA1IYdXNlZFVwbG9hZE1lZGlhU2l6ZUxpbWl0EjMKE3BheW1lbnRfcGVyaW9kX2RheX'
'MYAyABKANIAFIRcGF5bWVudFBlcmlvZERheXOIAQESSwogbGFzdF9wYXltZW50X2RvbmVfdW5p'
'eF90aW1lc3RhbXAYBCABKANIAVIcbGFzdFBheW1lbnREb25lVW5peFRpbWVzdGFtcIgBARJKCg'
'x0cmFuc2FjdGlvbnMYBSADKAsyJi5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlRyYW5zYWN0'
'aW9uUgx0cmFuc2FjdGlvbnMSXQoTYWRkaXRpb25hbF9hY2NvdW50cxgGIAMoCzIsLnNlcnZlcl'
'90b19jbGllbnQuUmVzcG9uc2UuQWRkaXRpb25hbEFjY291bnRSEmFkZGl0aW9uYWxBY2NvdW50'
'cxImCgxhdXRvX3JlbmV3YWwYByABKAhIAlILYXV0b1JlbmV3YWyIAQESQgobYWRkaXRpb25hbF'
'9hY2NvdW50X293bmVyX2lkGAggASgDSANSGGFkZGl0aW9uYWxBY2NvdW50T3duZXJJZIgBAUIW'
'ChRfcGF5bWVudF9wZXJpb2RfZGF5c0IjCiFfbGFzdF9wYXltZW50X2RvbmVfdW5peF90aW1lc3'
'RhbXBCDwoNX2F1dG9fcmVuZXdhbEIeChxfYWRkaXRpb25hbF9hY2NvdW50X293bmVyX2lkGjAK'
'BlByZUtleRIOCgJpZBgBIAEoA1ICaWQSFgoGcHJla2V5GAIgASgMUgZwcmVrZXkalQEKDFNpZ2'
'5lZFByZUtleRIoChBzaWduZWRfcHJla2V5X2lkGAEgASgDUg5zaWduZWRQcmVrZXlJZBIjCg1z'
'aWduZWRfcHJla2V5GAIgASgMUgxzaWduZWRQcmVrZXkSNgoXc2lnbmVkX3ByZWtleV9zaWduYX'
'R1cmUYAyABKAxSFXNpZ25lZFByZWtleVNpZ25hdHVyZRrUAQoJUHFjUHJlS2V5EiMKDmVjY19w'
'cmVfa2V5X2lkGAEgASgDUgtlY2NQcmVLZXlJZBIeCgtlY2NfcHJlX2tleRgCIAEoDFIJZWNjUH'
'JlS2V5EicKEGt5YmVyX3ByZV9rZXlfaWQYAyABKANSDWt5YmVyUHJlS2V5SWQSIgoNa3liZXJf'
'cHJlX2tleRgEIAEoDFILa3liZXJQcmVLZXkSNQoXa3liZXJfcHJlX2tleV9zaWduYXR1cmUYBS'
'ABKAxSFGt5YmVyUHJlS2V5U2lnbmF0dXJlGp0DCglQcWNCdW5kbGUSLwoUZWNjX3NpZ25lZF9w'
'cmVrZXlfaWQYASABKANSEWVjY1NpZ25lZFByZWtleUlkEioKEWVjY19zaWduZWRfcHJla2V5GA'
'IgASgMUg9lY2NTaWduZWRQcmVrZXkSPQobZWNjX3NpZ25lZF9wcmVrZXlfc2lnbmF0dXJlGAMg'
'ASgMUhhlY2NTaWduZWRQcmVrZXlTaWduYXR1cmUSMwoWa3liZXJfc2lnbmVkX3ByZWtleV9pZB'
'gEIAEoA1ITa3liZXJTaWduZWRQcmVrZXlJZBIuChNreWJlcl9zaWduZWRfcHJla2V5GAUgASgM'
'UhFreWJlclNpZ25lZFByZWtleRJBCh1reWJlcl9zaWduZWRfcHJla2V5X3NpZ25hdHVyZRgGIA'
'EoDFIaa3liZXJTaWduZWRQcmVrZXlTaWduYXR1cmUSQQoGcHJla2V5GAcgASgLMiQuc2VydmVy'
'X3RvX2NsaWVudC5SZXNwb25zZS5QcWNQcmVLZXlIAFIGcHJla2V5iAEBQgkKB19wcmVrZXkazw'
'QKCFVzZXJEYXRhEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBI7CgdwcmVrZXlzGAIgAygLMiEu'
'c2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5QcmVLZXlSB3ByZWtleXMSHwoIdXNlcm5hbWUYBy'
'ABKAxIAFIIdXNlcm5hbWWIAQESMwoTcHVibGljX2lkZW50aXR5X2tleRgDIAEoDEgBUhFwdWJs'
'aWNJZGVudGl0eUtleYgBARIoCg1zaWduZWRfcHJla2V5GAQgASgMSAJSDHNpZ25lZFByZWtleY'
'gBARI7ChdzaWduZWRfcHJla2V5X3NpZ25hdHVyZRgFIAEoDEgDUhVzaWduZWRQcmVrZXlTaWdu'
'YXR1cmWIAQESLQoQc2lnbmVkX3ByZWtleV9pZBgGIAEoA0gEUg5zaWduZWRQcmVrZXlJZIgBAR'
'IsCg9yZWdpc3RyYXRpb25faWQYCCABKANIBVIOcmVnaXN0cmF0aW9uSWSIAQESSAoKcHFjX2J1'
'bmRsZRgJIAEoCzIkLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuUHFjQnVuZGxlSAZSCXBxY0'
'J1bmRsZYgBAUILCglfdXNlcm5hbWVCFgoUX3B1YmxpY19pZGVudGl0eV9rZXlCEAoOX3NpZ25l'
'ZF9wcmVrZXlCGgoYX3NpZ25lZF9wcmVrZXlfc2lnbmF0dXJlQhMKEV9zaWduZWRfcHJla2V5X2'
'lkQhIKEF9yZWdpc3RyYXRpb25faWRCDQoLX3BxY19idW5kbGUaWQoLVXBsb2FkVG9rZW4SIQoM'
'dXBsb2FkX3Rva2VuGAEgASgMUgt1cGxvYWRUb2tlbhInCg9kb3dubG9hZF90b2tlbnMYAiADKA'
'xSDmRvd25sb2FkVG9rZW5zGjkKDkRvd25sb2FkVG9rZW5zEicKD2Rvd25sb2FkX3Rva2VucxgB'
'IAMoDFIOZG93bmxvYWRUb2tlbnMaRQoLUHJvb2ZPZldvcmsSFgoGcHJlZml4GAEgASgJUgZwcm'
'VmaXgSHgoKZGlmZmljdWx0eRgCIAEoA1IKZGlmZmljdWx0eRpeCh9QYXNzd29yZGxlc3NOb3Rp'
'ZmljYXRpb25NZXNzYWdlEg4KAmlkGAEgASgDUgJpZBIrChFlbmNyeXB0ZWRfbWVzc2FnZRgCIA'
'EoDFIQZW5jcnlwdGVkTWVzc2FnZRp6CiBQYXNzd29yZGxlc3NOb3RpZmljYXRpb25NZXNzYWdl'
'cxJWCghtZXNzYWdlcxgBIAMoCzI6LnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuUGFzc3dvcm'
'RsZXNzTm90aWZpY2F0aW9uTWVzc2FnZVIIbWVzc2FnZXMaqgEKDVByZXNpZ25lZFBvc3QSEAoD'
'dXJsGAEgASgJUgN1cmwSTAoGZmllbGRzGAIgAygLMjQuc2VydmVyX3RvX2NsaWVudC5SZXNwb2'
'5zZS5QcmVzaWduZWRQb3N0LkZpZWxkc0VudHJ5UgZmaWVsZHMaOQoLRmllbGRzRW50cnkSEAoD'
'a2V5GAEgASgJUgNrZXkSFAoFdmFsdWUYAiABKAlSBXZhbHVlOgI4ARrPAQoSTWVtb3JpZXNVcG'
'xvYWRVcmxzEhkKCG1lZGlhX2lkGAEgASgJUgdtZWRpYUlkElMKEHRodW1ibmFpbF91cGxvYWQY'
'AiABKAsyKC5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlByZXNpZ25lZFBvc3RSD3RodW1ibm'
'FpbFVwbG9hZBJJCgtmdWxsX3VwbG9hZBgDIAEoCzIoLnNlcnZlcl90b19jbGllbnQuUmVzcG9u'
'c2UuUHJlc2lnbmVkUG9zdFIKZnVsbFVwbG9hZBqBAQoJTWVkaWFJdGVtEhkKCG1lZGlhX2lkGA'
'EgASgJUgdtZWRpYUlkEiMKDW9yaWdpbmFsX2RhdGUYAiABKANSDG9yaWdpbmFsRGF0ZRI0ChZ0'
'aHVtYm5haWxfZG93bmxvYWRfdXJsGAMgASgJUhR0aHVtYm5haWxEb3dubG9hZFVybBpKCgxNZW'
'1vcmllc0xpc3QSOgoFaXRlbXMYASADKAsyJC5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLk1l'
'ZGlhSXRlbVIFaXRlbXMaOQoLTWVtb3JpZXNVcmwSKgoRZnVsbF9kb3dubG9hZF91cmwYASABKA'
'lSD2Z1bGxEb3dubG9hZFVybBpnCg1NZW1vcmllc1VzYWdlEhsKCW1heF9ieXRlcxgBIAEoA1II'
'bWF4Qnl0ZXMSIwoNY3VycmVudF9ieXRlcxgCIAEoA1IMY3VycmVudEJ5dGVzEhQKBWNvdW50GA'
'MgASgDUgVjb3VudBr1CgoCT2sSFAoETm9uZRgBIAEoCEgAUgROb25lEhgKBnVzZXJpZBgCIAEo'
'A0gAUgZ1c2VyaWQSJgoNYXV0aGNoYWxsZW5nZRgDIAEoDEgAUg1hdXRoY2hhbGxlbmdlEkoKC3'
'VwbG9hZHRva2VuGAQgASgLMiYuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5VcGxvYWRUb2tl'
'bkgAUgt1cGxvYWR0b2tlbhJBCgh1c2VyZGF0YRgFIAEoCzIjLnNlcnZlcl90b19jbGllbnQuUm'
'VzcG9uc2UuVXNlckRhdGFIAFIIdXNlcmRhdGESHgoJYXV0aHRva2VuGAYgASgMSABSCWF1dGh0'
'b2tlbhJQCg1hdXRoZW50aWNhdGVkGAggASgLMiguc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS'
'5BdXRoZW50aWNhdGVkSABSDWF1dGhlbnRpY2F0ZWQSOAoFcGxhbnMYCSABKAsyIC5zZXJ2ZXJf'
'dG9fY2xpZW50LlJlc3BvbnNlLlBsYW5zSABSBXBsYW5zEk0KDHBsYW5iYWxsYW5jZRgKIAEoCz'
'InLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuUGxhbkJhbGxhbmNlSABSDHBsYW5iYWxsYW5j'
'ZRJfChJhZGRhY2NvdW50c2ludml0ZXMYDCABKAsyLS5zZXJ2ZXJfdG9fY2xpZW50LlJlc3Bvbn'
'NlLkFkZEFjY291bnRzSW52aXRlc0gAUhJhZGRhY2NvdW50c2ludml0ZXMSUwoOZG93bmxvYWR0'
'b2tlbnMYDSABKAsyKS5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLkRvd25sb2FkVG9rZW5zSA'
'BSDmRvd25sb2FkdG9rZW5zEk0KDHNpZ25lZHByZWtleRgOIAEoCzInLnNlcnZlcl90b19jbGll'
'bnQuUmVzcG9uc2UuU2lnbmVkUHJlS2V5SABSDHNpZ25lZHByZWtleRJKCgtwcm9vZk9mV29yax'
'gPIAEoCzImLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuUHJvb2ZPZldvcmtIAFILcHJvb2ZP'
'ZldvcmsSSQogcGFzc3dvcmRsZXNzX3JlY292ZXJ5X3NlcnZlcl9rZXkYECABKAxIAFIdcGFzc3'
'dvcmRsZXNzUmVjb3ZlcnlTZXJ2ZXJLZXkSiwEKInBhc3N3b3JkbGVzc19ub3RpZmljYXRpb25f'
'bWVzc2FnZXMYESABKAsyOy5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlBhc3N3b3JkbGVzc0'
'5vdGlmaWNhdGlvbk1lc3NhZ2VzSABSIHBhc3N3b3JkbGVzc05vdGlmaWNhdGlvbk1lc3NhZ2Vz'
'EmEKFG1lbW9yaWVzX3VwbG9hZF91cmxzGBIgASgLMi0uc2VydmVyX3RvX2NsaWVudC5SZXNwb2'
'5zZS5NZW1vcmllc1VwbG9hZFVybHNIAFISbWVtb3JpZXNVcGxvYWRVcmxzEk4KDW1lbW9yaWVz'
'X2xpc3QYEyABKAsyJy5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLk1lbW9yaWVzTGlzdEgAUg'
'xtZW1vcmllc0xpc3QSSwoMbWVtb3JpZXNfdXJsGBQgASgLMiYuc2VydmVyX3RvX2NsaWVudC5S'
'ZXNwb25zZS5NZW1vcmllc1VybEgAUgttZW1vcmllc1VybBJRCg5tZW1vcmllc191c2FnZRgVIA'
'EoCzIoLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuTWVtb3JpZXNVc2FnZUgAUg1tZW1vcmll'
'c1VzYWdlQgQKAk9rSgQIBxAISgQICxAMQgoKCFJlc3BvbnNl');

View file

@ -25,6 +25,8 @@ class Message_Type extends $pb.ProtobufEnum {
Message_Type._(3, _omitEnumNames ? '' : 'PREKEY_BUNDLE');
static const Message_Type TEST_NOTIFICATION =
Message_Type._(4, _omitEnumNames ? '' : 'TEST_NOTIFICATION');
static const Message_Type CIPHERTEXT_V2 =
Message_Type._(5, _omitEnumNames ? '' : 'CIPHERTEXT_V2');
static const $core.List<Message_Type> values = <Message_Type>[
SENDER_DELIVERY_RECEIPT,
@ -32,10 +34,11 @@ class Message_Type extends $pb.ProtobufEnum {
CIPHERTEXT,
PREKEY_BUNDLE,
TEST_NOTIFICATION,
CIPHERTEXT_V2,
];
static final $core.List<Message_Type?> _byValue =
$pb.ProtobufEnum.$_initByValueList(values, 4);
$pb.ProtobufEnum.$_initByValueList(values, 5);
static Message_Type? valueOf($core.int value) =>
value < 0 || value >= _byValue.length ? null : _byValue[value];

View file

@ -57,6 +57,7 @@ const Message_Type$json = {
{'1': 'CIPHERTEXT', '2': 2},
{'1': 'PREKEY_BUNDLE', '2': 3},
{'1': 'TEST_NOTIFICATION', '2': 4},
{'1': 'CIPHERTEXT_V2', '2': 5},
],
};
@ -65,10 +66,10 @@ final $typed_data.Uint8List messageDescriptor = $convert.base64Decode(
'CgdNZXNzYWdlEiEKBHR5cGUYASABKA4yDS5NZXNzYWdlLlR5cGVSBHR5cGUSHQoKcmVjZWlwdF'
'9pZBgCIAEoCVIJcmVjZWlwdElkEjAKEWVuY3J5cHRlZF9jb250ZW50GAMgASgMSABSEGVuY3J5'
'cHRlZENvbnRlbnSIAQESQwoRcGxhaW50ZXh0X2NvbnRlbnQYBCABKAsyES5QbGFpbnRleHRDb2'
'50ZW50SAFSEHBsYWludGV4dENvbnRlbnSIAQEidAoEVHlwZRIbChdTRU5ERVJfREVMSVZFUllf'
'UkVDRUlQVBAAEhUKEVBMQUlOVEVYVF9DT05URU5UEAESDgoKQ0lQSEVSVEVYVBACEhEKDVBSRU'
'tFWV9CVU5ETEUQAxIVChFURVNUX05PVElGSUNBVElPThAEQhQKEl9lbmNyeXB0ZWRfY29udGVu'
'dEIUChJfcGxhaW50ZXh0X2NvbnRlbnQ=');
'50ZW50SAFSEHBsYWludGV4dENvbnRlbnSIAQEihwEKBFR5cGUSGwoXU0VOREVSX0RFTElWRVJZ'
'X1JFQ0VJUFQQABIVChFQTEFJTlRFWFRfQ09OVEVOVBABEg4KCkNJUEhFUlRFWFQQAhIRCg1QUk'
'VLRVlfQlVORExFEAMSFQoRVEVTVF9OT1RJRklDQVRJT04QBBIRCg1DSVBIRVJURVhUX1YyEAVC'
'FAoSX2VuY3J5cHRlZF9jb250ZW50QhQKEl9wbGFpbnRleHRfY29udGVudA==');
@$core.Deprecated('Use plaintextContentDescriptor instead')
const PlaintextContent$json = {

View file

@ -7,6 +7,7 @@ message Message {
CIPHERTEXT = 2;
PREKEY_BUNDLE = 3;
TEST_NOTIFICATION = 4;
CIPHERTEXT_V2 = 5;
}
Type type = 1;
string receipt_id = 2;

View file

@ -2,18 +2,23 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/visual/themes/light.dart';
class SettingsChangeProvider with ChangeNotifier, DiagnosticableTreeMixin {
late ThemeMode _themeMode;
late Color _primaryColor;
ThemeMode get themeMode => _themeMode;
Color get primaryColor => _primaryColor;
void loadSettings() {
if (userService.isUserCreated) {
_themeMode = userService.currentUser.themeMode;
_primaryColor = userService.currentUser.primaryColor;
notifyListeners();
} else {
_themeMode = ThemeMode.system;
_primaryColor = defaultPrimaryColor;
}
}
@ -28,4 +33,16 @@ class SettingsChangeProvider with ChangeNotifier, DiagnosticableTreeMixin {
await UserService.update((u) => u.themeMode = newThemeMode);
}
Future<void> updatePrimaryColor(Color newColor) async {
if (newColor.toARGB32() == _primaryColor.toARGB32()) return;
_primaryColor = newColor;
notifyListeners();
await UserService.update(
(u) => u.primaryColorValue = newColor.toARGB32(),
);
}
}

View file

@ -19,6 +19,8 @@ import 'package:twonly/core/bridge/wrapper/key_manager.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart'
as client;
import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pbserver.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'
@ -625,7 +627,13 @@ class ApiService {
final signalStore = await getSignalStoreFromIdentity(signalIdentity);
final signedPreKey = (await signalStore.loadSignedPreKeys())[0];
final signedPreKeysList = await signalStore.loadSignedPreKeys();
if (signedPreKeysList.isEmpty) {
throw Exception(
'Signal Signed PreKeys list is empty. Database insertion likely failed due to lack of storage space or a corrupted database.',
);
}
final signedPreKey = signedPreKeysList[0];
final loginToken = await RustKeyManager.getLoginToken();
@ -992,6 +1000,28 @@ class ApiService {
return sendRequestSync(req);
}
Future<Result> uploadPqcPreKeys(
int eccSignedPreKeyId,
Uint8List eccSignedPreKey,
Uint8List eccSignedPreKeySignature,
int kyberSignedPreKeyId,
Uint8List kyberSignedPreKey,
Uint8List kyberSignedPreKeySignature,
List<client.ApplicationData_PqcPreKey> prekeys,
) async {
final get = ApplicationData_UploadPqcPreKeys()
..eccSignedPrekeyId = Int64(eccSignedPreKeyId)
..eccSignedPrekey = eccSignedPreKey
..eccSignedPrekeySignature = eccSignedPreKeySignature
..kyberSignedPrekeyId = Int64(kyberSignedPreKeyId)
..kyberSignedPrekey = kyberSignedPreKey
..kyberSignedPrekeySignature = kyberSignedPreKeySignature
..prekeys.addAll(prekeys);
final appData = ApplicationData()..uploadPqcPrekeys = get;
final req = createClientToServerFromApplicationData(appData);
return sendRequestSync(req);
}
Future<Response_PlanBallance?> loadPlanBalance({bool useCache = true}) async {
final ballance = await getPlanBallance();
if (ballance != null) {

View file

@ -1,4 +1,5 @@
import 'package:fixnum/fixnum.dart';
import 'package:twonly/core/bridge/wrapper/signal.dart';
import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart'
as client;
import 'package:twonly/src/services/signal/identity.signal.dart';
@ -18,3 +19,25 @@ Future<client.Response> handleRequestNewPreKey() async {
final ok = client.Response_Ok()..prekeys = prekeys;
return client.Response()..ok = ok;
}
Future<client.Response?> handleRequestNewPqcPreKey() async {
final pqcKeys = await RustSignal.generatePqcPrekeys();
if (pqcKeys.isEmpty) return null;
final prekeysList = <client.ApplicationData_PqcPreKey>[];
for (final pqcKey in pqcKeys) {
prekeysList.add(
client.ApplicationData_PqcPreKey(
eccPreKeyId: Int64(pqcKey.eccPreKeyId),
eccPreKey: pqcKey.eccPreKey,
kyberPreKeyId: Int64(pqcKey.kyberPreKeyId),
kyberPreKey: pqcKey.kyberPreKey,
kyberPreKeySignature: pqcKey.kyberPreKeySignature,
),
);
}
final prekeys = client.Response_PqcPrekeys(prekeys: prekeysList);
final ok = client.Response_Ok()..prekeysPqc = prekeys;
return client.Response()..ok = ok;
}

View file

@ -392,6 +392,7 @@ Future<void> insertMediaFileInMessagesTable(
List<String> groupIds, {
AdditionalMessageData? additionalData,
}) async {
await twonlyDB.transaction(() async {
await twonlyDB.mediaFilesDao.updateAllMediaFiles(
const MediaFilesCompanion(
isDraftMedia: Value(false),
@ -418,7 +419,10 @@ Future<void> insertMediaFileInMessagesTable(
),
),
);
await twonlyDB.groupsDao.increaseLastMessageExchange(groupId, clock.now());
await twonlyDB.groupsDao.increaseLastMessageExchange(
groupId,
clock.now(),
);
if (message != null) {
Log.info(
'Created message ${message.messageId} for media ${message.mediaId}',
@ -435,6 +439,7 @@ Future<void> insertMediaFileInMessagesTable(
Log.error('Error inserting media upload message in database.');
}
}
});
unawaited(startBackgroundMediaUpload(mediaService));
}

View file

@ -5,10 +5,10 @@ import 'dart:io';
import 'package:clock/clock.dart';
import 'package:drift/drift.dart';
import 'package:fixnum/fixnum.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.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/tables/contacts.table.dart';
import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
@ -176,11 +176,11 @@ Future<(Uint8List, Uint8List?)?> _tryToSendCompleteMessageInternal({
}
if (message.type == pb.Message_Type.CIPHERTEXT) {
final cipherText = await signalEncryptMessage(
final encryptResult = await signalEncryptMessage(
receipt.contactId,
Uint8List.fromList(message.encryptedContent),
);
if (cipherText == null) {
if (encryptResult == null) {
Log.error(
'[${receipt.receiptId}] Could not encrypt the message for user ${receipt.contactId}. Aborting and trying again.',
);
@ -194,16 +194,31 @@ Future<(Uint8List, Uint8List?)?> _tryToSendCompleteMessageInternal({
await twonlyDB.receiptsDao.deleteReceipt(receipt.receiptId);
return null;
}
message.encryptedContent = cipherText.serialize();
switch (cipherText.getType()) {
case CiphertextMessage.prekeyType:
message.type = pb.Message_Type.PREKEY_BUNDLE;
case CiphertextMessage.whisperType:
message.type = pb.Message_Type.CIPHERTEXT;
default:
Log.error('Invalid ciphertext type: ${cipherText.getType()}.');
message
..encryptedContent = encryptResult.ciphertext
..type = encryptResult.type;
} else if (message.type == pb.Message_Type.CIPHERTEXT_V2) {
final encryptResult = await signalEncryptMessageV2(
receipt.contactId,
Uint8List.fromList(message.encryptedContent),
);
if (encryptResult == null) {
Log.error(
'[${receipt.receiptId}] Could not encrypt the message (V2) for user ${receipt.contactId}. Aborting and trying again.',
);
if (receipt.messageId != null) {
await twonlyDB.messagesDao.handleMessageAckByServer(
receipt.contactId,
receipt.messageId!,
clock.now(),
);
}
await twonlyDB.receiptsDao.deleteReceipt(receipt.receiptId);
return null;
}
message
..encryptedContent = encryptResult.ciphertext
..type = encryptResult.type;
}
if (onlyReturnEncryptedData) {
@ -482,8 +497,11 @@ Future<(Uint8List, Uint8List?)?> sendCipherText(
}
}
final contact = await twonlyDB.contactsDao.getContactById(contactId);
final isV2 = contact?.signalVersion == SignalVersion.v2;
final response = pb.Message()
..type = pb.Message_Type.CIPHERTEXT
..type = isV2 ? pb.Message_Type.CIPHERTEXT_V2 : pb.Message_Type.CIPHERTEXT
..encryptedContent = encryptedContent.writeToBuffer();
var retryCounter = 0;

View file

@ -49,6 +49,8 @@ Future<void> handleServerMessage(server.ServerToClient msg) async {
try {
if (msg.v0.hasRequestNewPreKeys()) {
response = await handleRequestNewPreKey();
} else if (msg.v0.hasRequestNewPqcPreKeys()) {
response = (await handleRequestNewPqcPreKey()) ?? response;
} else if (msg.v0.hasNewMessage()) {
Log.info('Got 1 message from the server.');
await handleClient2ClientMessage(msg.v0.newMessage);
@ -190,6 +192,7 @@ Future<void> _handleClient2ClientMessage(
}
case Message_Type.CIPHERTEXT:
case Message_Type.CIPHERTEXT_V2:
case Message_Type.PREKEY_BUNDLE:
if (message.hasEncryptedContent()) {
Value<String>? receiptIdDB;
@ -297,12 +300,23 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessageRaw(
Set<int>? brokenSessionsInCurrentBatch,
}) async {
Log.info('[$receiptId] calling signalDecryptMessage');
var (encryptedContent, decryptionErrorType) = await signalDecryptMessage(
EncryptedContent? encryptedContent;
PlaintextContent_DecryptionErrorMessage_Type? decryptionErrorType;
if (messageType == Message_Type.CIPHERTEXT_V2) {
(encryptedContent, decryptionErrorType) = await signalDecryptMessageV2(
fromUserId,
encryptedContentRaw,
brokenSessionsInCurrentBatch: brokenSessionsInCurrentBatch,
);
} else {
(encryptedContent, decryptionErrorType) = await signalDecryptMessageV1(
fromUserId,
encryptedContentRaw,
messageType.value,
brokenSessionsInCurrentBatch: brokenSessionsInCurrentBatch,
);
}
if (encryptedContent == null) {
return (

View file

@ -33,7 +33,12 @@ class Result<T, E> {
}
DateTime fromTimestamp(Int64 timeStamp) {
return DateTime.fromMillisecondsSinceEpoch(timeStamp.toInt());
final date = DateTime.fromMillisecondsSinceEpoch(timeStamp.toInt());
final now = DateTime.now();
if (date.isAfter(now)) {
return now;
}
return date;
}
// ignore: strict_raw_type

View file

@ -89,7 +89,10 @@ Future<bool> initBackgroundExecution() async {
final Mutex _keyValueMutex = Mutex();
// ignore: unreachable_from_main
Future<void> handlePeriodicTask({int lastExecutionInSecondsLimit = 120}) async {
Future<bool> backgroundFetch({
int? lastExecutionInSecondsLimit = 120,
}) async {
if (lastExecutionInSecondsLimit != null) {
final shouldBeExecuted = await exclusiveAccess(
lockName: 'periodic_task',
mutex: _keyValueMutex,
@ -116,9 +119,10 @@ Future<void> handlePeriodicTask({int lastExecutionInSecondsLimit = 120}) async {
},
);
if (!shouldBeExecuted) return;
if (!shouldBeExecuted) return false;
}
Log.info('eu.twonly.periodic_task was called.');
Log.info('Periodic task was called.');
AppState.gotMessageFromServer = false;
final stopwatch = Stopwatch()..start();
@ -130,14 +134,16 @@ Future<void> handlePeriodicTask({int lastExecutionInSecondsLimit = 120}) async {
if (!await apiService.connect()) {
Log.info('Could not connect to the api. Returning early.');
return;
return false;
}
if (!apiService.isAuthenticated) {
Log.info('Api is not authenticated. Returning early.');
return;
return false;
}
var receiveMessage = false;
try {
while (!AppState.gotMessageFromServer) {
if (stopwatch.elapsed.inSeconds >= 15) {
@ -148,19 +154,22 @@ Future<void> handlePeriodicTask({int lastExecutionInSecondsLimit = 120}) async {
}
if (AppState.gotMessageFromServer) {
receiveMessage = true;
Log.info('Received a server message from the server.');
}
await finishStartedPreprocessing();
if (lastExecutionInSecondsLimit != null) {
await Future.delayed(const Duration(milliseconds: 2000));
}
} finally {
await apiService.close(() {});
stopwatch.stop();
}
Log.info('eu.twonly.periodic_task finished after ${stopwatch.elapsed}.');
return;
Log.info('Periodic task finished after ${stopwatch.elapsed}.');
return receiveMessage;
}
Future<void> handleProcessingTask() async {

View file

@ -323,10 +323,12 @@ class MediaFileService {
name: mediaFile.mediaId,
);
} else {
await saveImageToGallery(
storedPath.readAsBytesSync(),
unawaited(
saveImageToGallery(
await storedPath.readAsBytes(),
createdAt: mediaFile.createdAt,
name: mediaFile.mediaId,
),
);
}
}

View file

@ -1,8 +1,10 @@
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart';
import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart';
@ -100,6 +102,7 @@ Future<void> showLocalPushNotification(
PushUser pushUser,
PushNotification pushNotification, {
String? groupId,
String? titleSuffix,
}) async {
String? title;
String? body;
@ -125,7 +128,9 @@ Future<void> showLocalPushNotification(
if (targetGroupId != null) {
try {
final currentUri = routerProvider.routerDelegate.currentConfiguration.uri;
if (currentUri.path.contains(targetGroupId)) {
final isResumed =
WidgetsBinding.instance.lifecycleState == AppLifecycleState.resumed;
if (isResumed && currentUri.path.contains(targetGroupId)) {
Log.info(
'Suppressing local push notification because chat with group $targetGroupId is currently open.',
);
@ -137,6 +142,10 @@ Future<void> showLocalPushNotification(
}
title = pushUser.displayName;
if (titleSuffix != null && titleSuffix.isNotEmpty) {
title = '$title $titleSuffix';
}
body = getPushNotificationText(pushNotification);
if (body == '') {
Log.error('No push notification type defined!');

View file

@ -20,7 +20,7 @@ Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
if (Platform.isAndroid) {
if (isInitialized) {
await handlePeriodicTask(lastExecutionInSecondsLimit: 10);
await backgroundFetch(lastExecutionInSecondsLimit: 3);
}
} else {
// make sure every thing run...

View file

@ -8,6 +8,7 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/secure_storage.keys.dart';
import 'package:twonly/src/services/background/callback_dispatcher.background.dart';
import 'package:twonly/src/services/notifications/background.notifications.dart';
import 'package:twonly/src/services/notifications/fcm.background.dart';
import 'package:twonly/src/services/user.service.dart';
@ -144,6 +145,7 @@ class FcmNotificationService {
}
static Future<void> handleRemoteMessage(RemoteMessage message) async {
Log.info('handleRemoteMessage received message: ${message.messageId}');
await _updateLastFcmMessageTimestamp();
if (!Platform.isAndroid) {
Log.error('Got message in Dart while on iOS');
@ -155,6 +157,24 @@ class FcmNotificationService {
return;
}
// In scenarios like Android Doze Mode or aggressive background restrictions, the OS may kill
// or heavily restrict network access, preventing the WebSocket from connecting in time.
// By parsing the FCM data payload offline, we can instantly display the notification, which also
// prevents FCM from penalizing/downgrading the app's data message priority for failing to show a notification.
// This is just a workarround until the new Rust decryption is enrolled fully.
final pushDataString = message.data['push_data'] as String?;
if (pushDataString != null) {
if (apiService.isConnected) {
Log.info('Got FCM message, but API is connected...');
} else {
Log.info('Trying to connect to the API in the background.');
if (await backgroundFetch()) {
return;
}
}
}
if (message.notification != null || message.data['title'] != null) {
final title =
message.notification?.title ?? message.data['title'] as String? ?? '';

View file

@ -3,23 +3,48 @@ import 'dart:typed_data';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
// ignore: implementation_imports
import 'package:libsignal_protocol_dart/src/invalid_message_exception.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/core/bridge/wrapper/signal.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
as pb;
import 'package:twonly/src/services/api/messages.api.dart';
import 'package:twonly/src/services/signal/protocol_state.signal.dart';
import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/services/signal/utils.signal.dart';
import 'package:twonly/src/utils/log.dart';
Future<CiphertextMessage?> signalEncryptMessage(
class SignalEncryptResult {
SignalEncryptResult(this.ciphertext, this.type);
final Uint8List ciphertext;
final pb.Message_Type type;
}
Future<SignalEncryptResult?> signalEncryptMessage(
int target,
Uint8List plaintextContent,
) async {
return lockingSignalProtocol.protect<CiphertextMessage?>(() async {
return _signalEncryptMessage(target, plaintextContent);
return lockingSignalProtocol.protect<SignalEncryptResult?>(() async {
return _signalEncryptMessageV1(target, plaintextContent);
});
}
Future<CiphertextMessage?> _signalEncryptMessage(
Future<SignalEncryptResult?> signalEncryptMessageV2(
int target,
Uint8List plaintextContent,
) async {
try {
final res = await RustSignal.encrypt(
name: target.toString(),
deviceId: 1,
plaintext: plaintextContent,
);
return SignalEncryptResult(res, pb.Message_Type.CIPHERTEXT_V2);
} catch (e) {
Log.error('Could not encrypt message (V2) for target $target: $e');
return null;
}
}
Future<SignalEncryptResult?> _signalEncryptMessageV1(
int target,
Uint8List plaintextContent,
) async {
@ -27,28 +52,93 @@ Future<CiphertextMessage?> _signalEncryptMessage(
final signalStore = (await getSignalStore())!;
final address = getSignalAddress(target);
final session = SessionCipher.fromStore(signalStore, address);
return await session.encrypt(plaintextContent);
final cipherText = await session.encrypt(plaintextContent);
pb.Message_Type type;
switch (cipherText.getType()) {
case CiphertextMessage.prekeyType:
type = pb.Message_Type.PREKEY_BUNDLE;
case CiphertextMessage.whisperType:
type = pb.Message_Type.CIPHERTEXT;
default:
Log.error('Invalid ciphertext type: ${cipherText.getType()}.');
return null;
}
return SignalEncryptResult(cipherText.serialize(), type);
} catch (e) {
Log.error('Could not encrypt message for target $target: $e');
Log.error('Could not encrypt message (V1) for target $target: $e');
return null;
}
}
Future<(EncryptedContent?, PlaintextContent_DecryptionErrorMessage_Type?)>
signalDecryptMessage(
Future<(pb.EncryptedContent?, pb.PlaintextContent_DecryptionErrorMessage_Type?)>
signalDecryptMessageV2(
int fromUserId,
Uint8List encryptedContentRaw,
int type, {
Uint8List encryptedContentRaw, {
Set<int>? brokenSessionsInCurrentBatch,
}) async {
// Hold the lock only for the cryptographic operation, not for network I/O
Log.info('Acquiring lockingSignalProtocol for $fromUserId');
Log.info('Acquiring lockingSignalProtocol for $fromUserId (V2)');
final (
decryptedContent,
errorType,
needsResync,
) = await lockingSignalProtocol.protect(() async {
Log.info('Lock acquired for $fromUserId');
Log.info('Lock acquired for $fromUserId (V2)');
try {
final plaintext = await RustSignal.decrypt(
name: fromUserId.toString(),
deviceId: 1,
ciphertext: encryptedContentRaw,
);
recordResyncAttempt(fromUserId, success: true);
return (pb.EncryptedContent.fromBuffer(plaintext), null, false);
} catch (e) {
Log.error('Could not decrypt message (V2) from $fromUserId: $e');
return (
null,
pb.PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN,
true, // Needs resync on failure
);
}
});
Log.info('Released lockingSignalProtocol for $fromUserId (V2)');
if (needsResync) {
brokenSessionsInCurrentBatch?.add(fromUserId);
if (shouldAttemptResync(fromUserId)) {
if (await handleSessionResync(fromUserId)) {
recordResyncAttempt(fromUserId, success: false);
await sendCipherText(
fromUserId,
pb.EncryptedContent(
errorMessages: pb.EncryptedContent_ErrorMessages(
type: pb.EncryptedContent_ErrorMessages_Type.SESSION_OUT_OF_SYNC,
),
),
);
}
}
}
return (decryptedContent, errorType);
}
Future<(pb.EncryptedContent?, pb.PlaintextContent_DecryptionErrorMessage_Type?)>
signalDecryptMessageV1(
int fromUserId,
Uint8List encryptedContentRaw,
int type, {
Set<int>? brokenSessionsInCurrentBatch,
}) async {
Log.info('Acquiring lockingSignalProtocol for $fromUserId (V1)');
final (
decryptedContent,
errorType,
needsResync,
) = await lockingSignalProtocol.protect(() async {
Log.info('Lock acquired for $fromUserId (V1)');
try {
final session = SessionCipher.fromStore(
(await getSignalStore())!,
@ -70,18 +160,18 @@ signalDecryptMessage(
Log.error('Unknown Message Decryption Type: $type');
return (
null,
PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN,
pb.PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN,
false,
);
}
recordResyncAttempt(fromUserId, success: true);
return (EncryptedContent.fromBuffer(plaintext), null, false);
return (pb.EncryptedContent.fromBuffer(plaintext), null, false);
} on InvalidKeyIdException catch (e) {
Log.warn(e);
return (
null,
PlaintextContent_DecryptionErrorMessage_Type.PREKEY_UNKNOWN,
pb.PlaintextContent_DecryptionErrorMessage_Type.PREKEY_UNKNOWN,
false,
);
} on DuplicateMessageException catch (e) {
@ -91,27 +181,27 @@ signalDecryptMessage(
Log.info(e);
return (
null,
PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN,
pb.PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN,
false,
);
} on InvalidMessageException catch (e) {
Log.warn(e);
return (
null,
PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN,
pb.PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN,
true,
);
} catch (e) {
Log.error(e);
return (
null,
PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN,
pb.PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN,
false,
);
}
});
Log.info('Released lockingSignalProtocol for $fromUserId');
Log.info('Released lockingSignalProtocol for $fromUserId (V1)');
// Handle session resync OUTSIDE the lock to avoid holding it during
// network round-trips (which can block for up to 60 seconds)
@ -127,9 +217,9 @@ signalDecryptMessage(
// session
await sendCipherText(
fromUserId,
EncryptedContent(
errorMessages: EncryptedContent_ErrorMessages(
type: EncryptedContent_ErrorMessages_Type.SESSION_OUT_OF_SYNC,
pb.EncryptedContent(
errorMessages: pb.EncryptedContent_ErrorMessages(
type: pb.EncryptedContent_ErrorMessages_Type.SESSION_OUT_OF_SYNC,
),
),
);

View file

@ -1,7 +1,9 @@
import 'dart:typed_data';
import 'package:clock/clock.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/core/bridge/wrapper/key_manager.dart';
import 'package:twonly/core/bridge/wrapper/signal.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/signal/signal_signed_pre_key_store.dart';
import 'package:twonly/src/model/json/signal_identity.model.dart';
@ -13,26 +15,20 @@ import 'package:twonly/src/utils/log.dart';
class SignalIdentityService {
static Future<void> onAuthenticated() async {
if (userService.currentUser.signalLastSignedPreKeyUpdated != null) {
final fortyEightHoursAgo = clock.now().subtract(
const Duration(hours: 48),
);
final isYoungerThan48Hours =
(userService.currentUser.signalLastSignedPreKeyUpdated!).isAfter(
final now = clock.now();
final fortyEightHoursAgo = now.subtract(const Duration(hours: 48));
final oneWeekAgo = now.subtract(const Duration(days: 7));
if (userService.currentUser.signalLastSignedPreKeyUpdated == null ||
!userService.currentUser.signalLastSignedPreKeyUpdated!.isAfter(
fortyEightHoursAgo,
);
if (isYoungerThan48Hours) {
// The key does live for 48 hours then it expires and a new key is generated.
return;
}
}
)) {
final signedPreKey = await _getNewSignalSignedPreKey();
if (signedPreKey == null) {
Log.error('could not generate a new signed pre key!');
return;
}
} else {
await UserService.update((user) {
user.signalLastSignedPreKeyUpdated = clock.now();
user.signalLastSignedPreKeyUpdated = now;
});
final res = await apiService.updateSignedPreKey(
signedPreKey.id,
@ -50,6 +46,34 @@ class SignalIdentityService {
}
}
if (userService.currentUser.signalLastPqcPreKeysUploaded == null ||
!userService.currentUser.signalLastPqcPreKeysUploaded!.isAfter(
oneWeekAgo,
)) {
final bundle = await RustSignal.generateBundle();
final pqcRes = await apiService.uploadPqcPreKeys(
bundle.signedPreKeyId,
bundle.signedPreKeyPublic,
bundle.signedPreKeySignature,
bundle.kyberPreKeyId,
bundle.kyberPreKeyPublic,
bundle.kyberPreKeySignature,
[],
);
if (pqcRes.isError) {
Log.warn('could not update the pqc signed pre key: ${pqcRes.error}');
} else {
Log.info('updated pqc signed pre key');
await UserService.update((user) {
user.signalLastPqcPreKeysUploaded = now;
});
}
}
}
}
Future<List<PreKeyRecord>> signalGetPreKeys() async {
return lockingSignalProtocol.protect(() async {
final start = userService.currentUser.currentPreKeyIndexStart;

View file

@ -1,7 +1,12 @@
import 'dart:typed_data';
import 'package:drift/drift.dart' as drift;
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/core/bridge/wrapper/signal.dart';
import 'package:twonly/core/signal/engine.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart';
import 'package:twonly/src/services/signal/consts.signal.dart';
import 'package:twonly/src/services/signal/protocol_state.signal.dart';
@ -15,6 +20,106 @@ Future<bool> processSignalUserData(Response_UserData userData) async {
}
Future<bool> _processSignalUserData(Response_UserData userData) async {
if (userData.hasPqcBundle()) {
return _processSignalUserDataV2(userData);
}
return _processSignalUserDataV1(userData);
}
Future<bool> _processSignalUserDataV2(Response_UserData userData) async {
try {
final tempIdentityKey = IdentityKey(
Curve.decodePoint(
DjbECPublicKey(
Uint8List.fromList(userData.publicIdentityKey),
).serialize(),
1,
),
);
final signalStore = await getSignalStore();
if (signalStore != null) {
final existingIdentity = await signalStore.getIdentity(
SignalProtocolAddress(userData.userId.toString(), defaultDeviceId),
);
if (existingIdentity != null && existingIdentity != tempIdentityKey) {
Log.error(
'Identity key mismatch for contact ${userData.userId}! Existing V1 key does not match the incoming V2 identity key.',
);
return false;
}
}
int? tempEccPreKeyId;
Uint8List? tempEccPreKeyPublic;
if (userData.pqcBundle.hasPrekey()) {
tempEccPreKeyId = userData.pqcBundle.prekey.eccPreKeyId.toInt();
tempEccPreKeyPublic = Uint8List.fromList(
userData.pqcBundle.prekey.eccPreKey,
);
} else if (userData.prekeys.isNotEmpty) {
tempEccPreKeyId = userData.prekeys.first.id.toInt();
tempEccPreKeyPublic = Curve.decodePoint(
DjbECPublicKey(
Uint8List.fromList(userData.prekeys.first.prekey),
).serialize(),
1,
).serialize();
}
final tempKyberPreKeyId = userData.pqcBundle.hasPrekey()
? userData.pqcBundle.prekey.kyberPreKeyId.toInt()
: userData.pqcBundle.kyberSignedPrekeyId.toInt();
final tempKyberPreKeyPublic = userData.pqcBundle.hasPrekey()
? Uint8List.fromList(userData.pqcBundle.prekey.kyberPreKey)
: Uint8List.fromList(userData.pqcBundle.kyberSignedPrekey);
final tempKyberPreKeySignature = userData.pqcBundle.hasPrekey()
? Uint8List.fromList(userData.pqcBundle.prekey.kyberPreKeySignature)
: Uint8List.fromList(userData.pqcBundle.kyberSignedPrekeySignature);
final rustBundle = FrbPreKeyBundle(
registrationId: userData.registrationId.toInt(),
deviceId: 1,
preKeyId: tempEccPreKeyId,
preKeyPublic: tempEccPreKeyPublic,
signedPreKeyId: userData.pqcBundle.eccSignedPrekeyId.toInt(),
signedPreKeyPublic: Uint8List.fromList(
userData.pqcBundle.eccSignedPrekey,
),
signedPreKeySignature: Uint8List.fromList(
userData.pqcBundle.eccSignedPrekeySignature,
),
identityKey: tempIdentityKey.publicKey.serialize(),
kyberPreKeyId: tempKyberPreKeyId,
kyberPreKeyPublic: tempKyberPreKeyPublic,
kyberPreKeySignature: tempKyberPreKeySignature,
);
await RustSignal.processPrekeyBundle(
name: userData.userId.toString(),
deviceId: 1,
bundle: rustBundle,
);
final contact = await twonlyDB.contactsDao.getContactById(
userData.userId.toInt(),
);
if (contact != null && contact.signalVersion != SignalVersion.v2) {
await twonlyDB.contactsDao.updateContact(
contact.userId,
const ContactsCompanion(signalVersion: drift.Value(SignalVersion.v2)),
);
}
return true;
} catch (e) {
Log.error('could not process pqc bundle: $e');
return false;
}
}
Future<bool> _processSignalUserDataV1(Response_UserData userData) async {
final SignalProtocolStore? signalStore = await getSignalStore();
if (signalStore == null) {

View file

@ -210,6 +210,10 @@ Future<bool> authenticateUser(
return true;
}
}
} catch (e) {
// Catch unexpected platform exceptions (e.g. PlatformException) that
// would otherwise propagate and leave callers in a broken state.
Log.error('Unexpected authentication error: $e');
}
return false;
}
@ -260,6 +264,28 @@ String formatDateTime(BuildContext context, DateTime? dateTime) {
}
}
String formatRelativeDateTime(BuildContext context, DateTime? dateTime) {
if (dateTime == null) {
return '-';
}
final now = clock.now();
final today = DateTime(now.year, now.month, now.day);
final yesterday = today.subtract(const Duration(days: 1));
final dateDay = DateTime(dateTime.year, dateTime.month, dateTime.day);
final time = DateFormat.Hm(
Localizations.localeOf(context).toLanguageTag(),
).format(dateTime);
if (dateDay == today) {
return context.lang.todayAt(time);
} else if (dateDay == yesterday) {
return context.lang.yesterdayAt(time);
} else {
return formatDateTime(context, dateTime);
}
}
String formatBytes(int bytes) {
if (bytes <= 0) return '0 Bytes';
const units = <String>['Bytes', 'KB', 'MB', 'GB', 'TB'];

View file

@ -1,11 +1,27 @@
import 'dart:async';
import 'dart:ui';
import 'package:path_provider/path_provider.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/secure_storage.dart';
Future<bool> deleteLocalUserData() async {
await twonlyDB.close();
// Wait for the background drift isolate to potentially shut down
await Future.delayed(const Duration(milliseconds: 200));
// Remove port mappings to prevent connecting to an old, detached isolate that holds a deleted DB file.
IsolateNameServer.removePortNameMapping('drift-db/twonly');
IsolateNameServer.removePortNameMapping('drift-db/twonly/control');
final appDir = await getApplicationSupportDirectory();
if (appDir.existsSync()) {
appDir.deleteSync(recursive: true);
}
await SecureStorage.instance.deleteAll();
locator
..unregister<TwonlyDB>()
..registerLazySingleton<TwonlyDB>(TwonlyDB.new);
return true;
}

View file

@ -0,0 +1,144 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/misc.dart';
class ContactLabels extends StatefulWidget {
const ContactLabels({
required this.contactId,
this.fontSize = 8,
this.padding = const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
this.emptyText,
this.showEmptyText = false,
super.key,
});
final int contactId;
final double fontSize;
final EdgeInsetsGeometry padding;
final String? emptyText;
final bool showEmptyText;
@override
State<ContactLabels> createState() => _ContactLabelsState();
}
class _ContactLabelsState extends State<ContactLabels> {
List<Label> _labels = [];
late StreamSubscription<List<Label>> _sub;
@override
void initState() {
super.initState();
_sub = twonlyDB.labelsDao.watchContactLabels(widget.contactId).listen((
labels,
) {
if (mounted) {
setState(() {
_labels = labels;
});
}
});
}
@override
void dispose() {
_sub.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (_labels.isEmpty) {
if (!widget.showEmptyText && widget.emptyText == null) {
return const SizedBox.shrink();
}
return Text(
widget.emptyText ?? context.lang.contactLabelsSubtitleEmpty,
style: TextStyle(
fontSize: widget.fontSize,
color: Theme.of(context).disabledColor,
),
);
}
return Wrap(
spacing: 4,
runSpacing: 2,
children: _labels.map((label) {
final bgColor = Color(label.backgroundColor);
final textColor = Color(label.textColor);
return Container(
padding: widget.padding,
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(8),
),
child: Text(
label.name,
style: TextStyle(
fontSize: widget.fontSize,
color: textColor,
fontWeight: FontWeight.w600,
),
),
);
}).toList(),
);
}
}
Widget? buildContactLabelsSubtitle({
required int contactId,
required List<Label> labels,
Widget? additionalSubtitle,
}) {
if (additionalSubtitle != null) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
additionalSubtitle,
if (labels.isNotEmpty)
ContactLabels(contactId: contactId),
],
);
}
if (labels.isEmpty) {
return null;
}
return ContactLabels(contactId: contactId);
}
class ContactLabelsSubtitleBuilder extends StatelessWidget {
const ContactLabelsSubtitleBuilder({
required this.contactId,
required this.builder,
this.additionalSubtitle,
super.key,
});
final int contactId;
final Widget? additionalSubtitle;
final Widget Function(BuildContext context, Widget? subtitleWidget) builder;
@override
Widget build(BuildContext context) {
return StreamBuilder<List<Label>>(
stream: twonlyDB.labelsDao.watchContactLabels(contactId),
builder: (context, snapshot) {
final labels = snapshot.data ?? [];
final subtitle = buildContactLabelsSubtitle(
contactId: contactId,
labels: labels,
additionalSubtitle: additionalSubtitle,
);
return builder(context, subtitle);
},
);
}
}

View file

@ -5,7 +5,6 @@ import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/notification_badge.comp.dart';
import 'package:twonly/src/visual/themes/light.dart';
class ContactRequestBadgeComp extends StatelessWidget {
const ContactRequestBadgeComp({super.key});
@ -26,8 +25,8 @@ class ContactRequestBadgeComp extends StatelessWidget {
child: Container(
width: 40,
height: 40,
decoration: const BoxDecoration(
color: primaryColor,
decoration: BoxDecoration(
color: context.color.primary,
shape: BoxShape.circle,
),
),

View file

@ -0,0 +1,114 @@
import 'package:flutter/material.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/elements/my_button.element.dart';
class CustomColorPickerDialog extends StatefulWidget {
const CustomColorPickerDialog({
required this.initialColor,
super.key,
});
final Color initialColor;
@override
State<CustomColorPickerDialog> createState() => _CustomColorPickerDialogState();
}
class _CustomColorPickerDialogState extends State<CustomColorPickerDialog> {
late HSVColor _hsvColor;
@override
void initState() {
super.initState();
_hsvColor = HSVColor.fromColor(widget.initialColor);
}
@override
Widget build(BuildContext context) {
final currentColor = _hsvColor.toColor();
return AlertDialog(
title: Text(context.lang.customColor),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Color preview box
Center(
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: currentColor,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.15),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
),
),
const SizedBox(height: 20),
// Hue Slider
Text(context.lang.hue, style: const TextStyle(fontWeight: FontWeight.w600)),
Slider(
value: _hsvColor.hue,
max: 360,
activeColor: HSVColor.fromAHSV(1, _hsvColor.hue, 1, 1).toColor(),
onChanged: (val) {
setState(() {
_hsvColor = _hsvColor.withHue(val);
});
},
),
// Saturation Slider
Text(context.lang.saturation, style: const TextStyle(fontWeight: FontWeight.w600)),
Slider(
value: _hsvColor.saturation,
onChanged: (val) {
setState(() {
_hsvColor = _hsvColor.withSaturation(val);
});
},
),
// Brightness / Value Slider
Text(context.lang.brightness, style: const TextStyle(fontWeight: FontWeight.w600)),
Slider(
value: _hsvColor.value,
onChanged: (val) {
setState(() {
_hsvColor = _hsvColor.withValue(val);
});
},
),
],
),
),
actions: [
Row(
children: [
Expanded(
child: MyButton(
variant: MyButtonVariant.text,
onPressed: () => Navigator.of(context).pop(),
child: Text(context.lang.cancel),
),
),
const SizedBox(width: 8),
Expanded(
child: MyButton(
variant: MyButtonVariant.primaryMiddle,
onPressed: () => Navigator.of(context).pop(currentColor.toARGB32()),
child: Text(context.lang.ok),
),
),
],
),
],
);
}
}

View file

@ -0,0 +1,328 @@
import 'package:flutter/material.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/custom_color_picker_dialog.comp.dart';
import 'package:twonly/src/visual/elements/my_button.element.dart';
class LabelEditorBottomSheet extends StatefulWidget {
const LabelEditorBottomSheet({
this.label,
super.key,
});
final Label? label;
@override
State<LabelEditorBottomSheet> createState() => _LabelEditorBottomSheetState();
}
class _LabelEditorBottomSheetState extends State<LabelEditorBottomSheet> {
late TextEditingController _nameController;
late int _selectedBgColor;
late int _selectedTextColor;
static const List<int> defaultBgColors = [
0xFFE57373, // Red
0xFFF06292, // Pink
0xFFBA68C8, // Purple
0xFF7986CB, // Indigo
0xFF64B5F6, // Blue
0xFF4DD0E1, // Cyan
0xFF4DB6AC, // Teal
0xFF81C784, // Green
0xFFFFB74D, // Amber
0xFFFF8A65, // Deep Orange
0xFF90A4AE, // Blue Grey
0xFF424242, // Dark Grey
];
static const List<int> defaultTextColors = [
0xFFFFFFFF, // White
0xFF121212, // Dark/Black
0xFF1B263B, // Deep Navy
0xFF8B0000, // Dark Red
0xFF004D40, // Dark Teal
0xFF4A148C, // Dark Purple
];
@override
void initState() {
super.initState();
_nameController = TextEditingController(text: widget.label?.name ?? '');
_selectedBgColor = widget.label?.backgroundColor ?? defaultBgColors[4];
_selectedTextColor = widget.label?.textColor ?? defaultTextColors[0];
}
@override
void dispose() {
_nameController.dispose();
super.dispose();
}
Future<void> _pickCustomColor({required bool isBgColor}) async {
final initial = isBgColor ? Color(_selectedBgColor) : Color(_selectedTextColor);
final pickedColorInt = await showDialog<int>(
context: context,
builder: (context) => CustomColorPickerDialog(initialColor: initial),
);
if (pickedColorInt != null && mounted) {
setState(() {
if (isBgColor) {
_selectedBgColor = pickedColorInt;
} else {
_selectedTextColor = pickedColorInt;
}
});
}
}
@override
Widget build(BuildContext context) {
final isEditing = widget.label != null;
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
return Padding(
padding: EdgeInsets.only(bottom: bottomInset),
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Drag indicator handle
Center(
child: Container(
width: 40,
height: 4,
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.grey.shade400,
borderRadius: BorderRadius.circular(2),
),
),
),
Text(
isEditing ? context.lang.editLabel : context.lang.createLabel,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 20),
// Interactive Inline Label Badge
Center(
child: IntrinsicWidth(
child: ConstrainedBox(
constraints: const BoxConstraints(minWidth: 100, maxWidth: 200),
child: Container(
decoration: BoxDecoration(
color: Color(_selectedBgColor),
borderRadius: BorderRadius.circular(16),
),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 2),
child: TextField(
controller: _nameController,
autofocus: true,
maxLength: 8,
textAlign: TextAlign.center,
textCapitalization: TextCapitalization.words,
style: TextStyle(
color: Color(_selectedTextColor),
fontSize: 13,
fontWeight: FontWeight.w600,
),
decoration: InputDecoration(
hintText: context.lang.labelNameHint,
hintStyle: TextStyle(
color: Color(_selectedTextColor).withValues(alpha: 0.6),
fontSize: 13,
fontWeight: FontWeight.w600,
),
border: InputBorder.none,
isDense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 4, vertical: 6),
counterText: '',
),
onChanged: (_) => setState(() {}),
),
),
),
),
),
const SizedBox(height: 24),
// Background Color Section
Text(
context.lang.labelBackgroundColor,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
Wrap(
alignment: WrapAlignment.center,
spacing: 10,
runSpacing: 10,
children: [
...defaultBgColors.map((colorValue) {
final selected = _selectedBgColor == colorValue;
return GestureDetector(
onTap: () {
setState(() {
_selectedBgColor = colorValue;
});
},
child: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: Color(colorValue),
shape: BoxShape.circle,
),
child: selected
? Icon(
Icons.check,
size: 18,
color: Color(colorValue).computeLuminance() > 0.5
? Colors.black
: Colors.white,
)
: null,
),
);
}),
// Custom Color Picker Button
GestureDetector(
onTap: () => _pickCustomColor(isBgColor: true),
child: Container(
width: 36,
height: 36,
decoration: const BoxDecoration(
shape: BoxShape.circle,
gradient: SweepGradient(
colors: [
Colors.red,
Colors.yellow,
Colors.green,
Colors.cyan,
Colors.blue,
Colors.purple,
Colors.red,
],
),
),
child: const Icon(
Icons.colorize,
size: 18,
color: Colors.white,
),
),
),
],
),
const SizedBox(height: 20),
// Text Color Section
Text(
context.lang.labelTextColor,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
Wrap(
alignment: WrapAlignment.center,
spacing: 10,
runSpacing: 10,
children: [
...defaultTextColors.map((colorValue) {
final selected = _selectedTextColor == colorValue;
return GestureDetector(
onTap: () {
setState(() {
_selectedTextColor = colorValue;
});
},
child: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: Color(colorValue),
shape: BoxShape.circle,
),
child: selected
? Icon(
Icons.check,
size: 18,
color: Color(colorValue).computeLuminance() > 0.5
? Colors.black
: Colors.white,
)
: null,
),
);
}),
// Custom Color Picker Button for Text
GestureDetector(
onTap: () => _pickCustomColor(isBgColor: false),
child: Container(
width: 36,
height: 36,
decoration: const BoxDecoration(
shape: BoxShape.circle,
gradient: SweepGradient(
colors: [
Colors.red,
Colors.yellow,
Colors.green,
Colors.cyan,
Colors.blue,
Colors.purple,
Colors.red,
],
),
),
child: const Icon(
Icons.colorize,
size: 18,
color: Colors.white,
),
),
),
],
),
const SizedBox(height: 24),
Row(
children: [
Expanded(
child: MyButton(
variant: MyButtonVariant.text,
onPressed: () => Navigator.of(context).pop(),
child: Text(context.lang.cancel),
),
),
const SizedBox(width: 12),
Expanded(
child: MyButton(
variant: MyButtonVariant.primaryMiddle,
onPressed: () {
final name = _nameController.text.trim();
if (name.isNotEmpty) {
Navigator.of(context).pop({
'name': name,
'backgroundColor': _selectedBgColor,
'textColor': _selectedTextColor,
});
}
},
child: Text(context.lang.ok),
),
),
],
),
],
),
),
),
);
}
}

View file

@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:twonly/src/visual/themes/light.dart';
import 'package:twonly/src/utils/misc.dart';
class SelectableThumbnailComp extends StatelessWidget {
const SelectableThumbnailComp({
@ -19,7 +19,7 @@ class SelectableThumbnailComp extends StatelessWidget {
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
decoration: BoxDecoration(
color: isSelected ? primaryColor : Colors.transparent,
color: isSelected ? context.color.primary : Colors.transparent,
boxShadow: const [
BoxShadow(
color: Colors.black12,
@ -49,7 +49,7 @@ class SelectableThumbnailComp extends StatelessWidget {
child: Container(
padding: const EdgeInsets.all(2),
decoration: BoxDecoration(
color: isSelected ? primaryColor : Colors.black38,
color: isSelected ? context.color.primary : Colors.black38,
shape: BoxShape.circle,
border: Border.all(
color: Theme.of(context).brightness == Brightness.dark

View file

@ -9,7 +9,6 @@ import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/profile_qr_code.comp.dart';
import 'package:twonly/src/visual/elements/my_button.element.dart';
import 'package:twonly/src/visual/elements/svg_icon.element.dart';
import 'package:twonly/src/visual/themes/light.dart';
const colorVerificationBadgeYellow = Color.fromARGB(255, 0, 182, 238);
@ -90,7 +89,7 @@ class VerificationBadgeInfo extends StatelessWidget {
context,
icon: const SvgIcon(assetPath: SvgIcons.verifiedGreen, size: 40),
description: context.lang.verificationBadgeGreenDesc,
boldTextColor: primaryColor,
boldTextColor: context.color.primary,
onTap: () => context.push(
Routes.cameraQRScanner,
extra: {

View file

@ -86,7 +86,7 @@ class VerificationSuccessAnimationState
'<svg viewBox="0 0 640 640"><path d="$_path2" fill="white"/></svg>';
static const _grey = Color(0xFF8E9AAF);
static const Color _green = primaryColor;
static const Color _green = defaultPrimaryColor;
@override
void initState() {

View file

@ -13,7 +13,7 @@ class BetterText extends StatelessWidget {
Widget build(BuildContext context) {
// Regular expression to find URLs and domains
final urlRegExp = RegExp(
r'(?:(?:https?://|www\.)[^\s]+|(?:[a-zA-Z0-9-]+\.[a-zA-Z]{2,}))',
r'''(?:(?:https?://|www\.)(?:[^\s()<>]+|\([^\s()<>]+\))+(?:\([^\s()<>]+\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))|(?:(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,})''',
caseSensitive: false,
);

View file

@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/elements/reactive_tap_feedback.element.dart';
import 'package:twonly/src/visual/themes/light.dart';
enum MyButtonVariant {
primary,
@ -49,7 +48,7 @@ class _MyButtonState extends State<MyButton> {
switch (widget.variant) {
case MyButtonVariant.primary:
buttonStyle = FilledButton.styleFrom(
backgroundColor: primaryColor,
backgroundColor: context.color.primary,
foregroundColor: Colors.black87,
disabledBackgroundColor: disabledBgColor,
disabledForegroundColor: disabledFgColor,
@ -96,7 +95,7 @@ class _MyButtonState extends State<MyButton> {
);
case MyButtonVariant.primaryMiddle:
buttonStyle = FilledButton.styleFrom(
backgroundColor: primaryColor,
backgroundColor: context.color.primary,
foregroundColor: Colors.black87,
disabledBackgroundColor: disabledBgColor,
disabledForegroundColor: disabledFgColor,
@ -115,7 +114,7 @@ class _MyButtonState extends State<MyButton> {
);
case MyButtonVariant.primaryDense:
buttonStyle = FilledButton.styleFrom(
backgroundColor: primaryColor,
backgroundColor: context.color.primary,
foregroundColor: Colors.black87,
disabledBackgroundColor: disabledBgColor,
disabledForegroundColor: disabledFgColor,

View file

@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/elements/reactive_tap_feedback.element.dart';
import 'package:twonly/src/visual/themes/light.dart';
enum MyIconButtonVariant {
primary,
@ -43,7 +42,7 @@ class _MyIconButtonState extends State<MyIconButton> {
late final Color fgColor;
if (widget.variant == MyIconButtonVariant.primary) {
bgColor = primaryColor;
bgColor = context.color.primary;
fgColor = Colors.black87;
} else {
bgColor = isDark ? Colors.grey[800]! : Colors.grey[200]!;

View file

@ -1,11 +1,13 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:twonly/src/visual/themes/light.dart';
final ThemeData darkTheme = () {
ThemeData getDarkTheme([Color primary = defaultPrimaryColor]) {
final base = ThemeData.dark().copyWith(
colorScheme: ColorScheme.fromSeed(
brightness: Brightness.dark,
seedColor: const Color(0xFF57CC99),
seedColor: primary,
primary: primary,
surface: const Color.fromARGB(255, 20, 18, 23),
surfaceContainer: const Color.fromARGB(255, 45, 41, 54),
surfaceContainerLow: const Color.fromARGB(255, 38, 34, 45),
@ -21,4 +23,6 @@ final ThemeData darkTheme = () {
fontFamilyFallback: Platform.isAndroid ? const ['NotoColorEmoji'] : null,
),
);
}();
}
final ThemeData darkTheme = getDarkTheme();

View file

@ -2,12 +2,13 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:twonly/src/utils/misc.dart';
const primaryColor = Color(0xFF57CC99);
const defaultPrimaryColor = Color(0xFF57CC99);
final ThemeData lightTheme = () {
ThemeData getLightTheme([Color primary = defaultPrimaryColor]) {
final base = ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: primaryColor,
seedColor: primary,
primary: primary,
),
inputDecorationTheme: const InputDecorationTheme(
border: OutlineInputBorder(),
@ -19,10 +20,12 @@ final ThemeData lightTheme = () {
fontFamilyFallback: Platform.isAndroid ? const ['NotoColorEmoji'] : null,
),
);
}();
}
final ThemeData lightTheme = getLightTheme();
final ButtonStyle primaryColorButtonStyle = FilledButton.styleFrom(
backgroundColor: primaryColor,
backgroundColor: defaultPrimaryColor,
foregroundColor: Colors.black87,
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
shape: RoundedRectangleBorder(

View file

@ -23,7 +23,8 @@ class CameraScannedOverlay extends StatelessWidget {
width: 150,
child: ListView(
children: [
if (mainController.scannedUrl != null)
if (mainController.scannedUrl != null &&
mainController.scannedUrl!.isNotEmpty)
_buildScannedUrlTile(context, mainController.scannedUrl!),
],
),

View file

@ -168,6 +168,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
super.initState();
initVolumeControl();
initAsync();
_checkAndInitCamera();
}
@override
@ -176,12 +177,26 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
if (oldWidget.isVisible != widget.isVisible) {
if (widget.isVisible) {
initVolumeControl();
_checkAndInitCamera();
} else {
_deInitVolumeControl();
}
}
}
void _checkAndInitCamera() {
if (widget.isVisible &&
mc.cameraController == null &&
!mc.initCameraStarted) {
unawaited(
mc.selectCamera(
mc.selectedCameraDetails.cameraId,
false,
),
);
}
}
@override
void dispose() {
_videoRecordingTimer?.cancel();
@ -686,6 +701,13 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
Widget build(BuildContext context) {
if (mc.selectedCameraDetails.cameraId >= AppEnvironment.cameras.length ||
mc.cameraController == null) {
if (widget.isVisible &&
!mc.initCameraStarted &&
!mc.isSharePreviewIsShown) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_checkAndInitCamera();
});
}
return Container();
}
return StreamBuilder(

View file

@ -81,7 +81,7 @@ class MainCameraController {
}
void onImageSend() {
scannedUrl = '';
scannedUrl = null;
setState?.call();
}
@ -155,6 +155,7 @@ class MainCameraController {
} catch (e) {
Log.error('Error querying available cameras: $e');
initCameraStarted = false;
setState?.call();
return;
}
@ -164,6 +165,7 @@ class MainCameraController {
'Trying to select a non existing camera $cameraId >= ${AppEnvironment.cameras.length}',
);
initCameraStarted = false;
setState?.call();
return;
}
@ -192,7 +194,7 @@ class MainCameraController {
final hasMic = await micPermissionFuture;
if (sessionId != _cameraSessionId) return;
final controller = CameraController(
var controller = CameraController(
AppEnvironment.cameras[cameraId],
ResolutionPreset.high,
enableAudio: hasMic,
@ -204,7 +206,23 @@ class MainCameraController {
var assignedToGlobal = false;
try {
_initializeFuture = controller.initialize();
try {
await _initializeFuture;
} on CameraException catch (e) {
// If specific image format is unsupported on this hardware, fallback to default format
Log.warn(
'Initial camera format initialization failed ($e), trying fallback format...',
);
await controller.dispose();
controller = CameraController(
AppEnvironment.cameras[cameraId],
ResolutionPreset.high,
enableAudio: hasMic,
);
_initializeFuture = controller.initialize();
await _initializeFuture;
}
if (sessionId != _cameraSessionId) {
unawaited(controller.dispose());
return;
@ -256,6 +274,7 @@ class MainCameraController {
unawaited(controller.dispose());
}
initCameraStarted = false;
setState?.call();
return;
}
} else {

View file

@ -1,6 +1,8 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/key_verification.service.dart';
@ -8,6 +10,7 @@ import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/views/camera/camera_preview_components/camera_preview.dart';
import 'package:twonly/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart';
import 'package:twonly/src/visual/views/camera/camera_preview_components/main_camera_controller.dart';
import 'package:twonly/src/visual/views/camera/share_image_editor_components/action_button.dart';
class QrCodeScannerView extends StatefulWidget {
const QrCodeScannerView({
@ -41,6 +44,7 @@ class QrCodeScannerViewState extends State<QrCodeScannerView> {
}
Permission.camera.isGranted.then((hasPermission) {
if (hasPermission && mounted) {
AppState.hasCameraPermissions = true;
unawaited(_mainCameraController.selectCamera(0, true));
}
});
@ -100,6 +104,17 @@ class QrCodeScannerViewState extends State<QrCodeScannerView> {
),
),
),
// Always-visible back button so the user can exit regardless of
// camera state (e.g. during init or after init failure).
Positioned(
left: 5,
top: MediaQuery.paddingOf(context).top + 10,
child: ActionButton(
FontAwesomeIcons.xmark,
tooltipText: context.lang.close,
onPressed: () => Navigator.pop(context),
),
),
],
),
);

View file

@ -1,10 +1,14 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/views/camera/camera_preview_components/camera_preview.dart';
import 'package:twonly/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart';
import 'package:twonly/src/visual/views/camera/camera_preview_components/main_camera_controller.dart';
import 'package:twonly/src/visual/views/camera/share_image_editor_components/action_button.dart';
class CameraSendToView extends StatefulWidget {
const CameraSendToView(this.sendToGroup, {super.key});
@ -24,6 +28,7 @@ class CameraSendToViewState extends State<CameraSendToView> {
};
Permission.camera.isGranted.then((hasPermission) {
if (hasPermission && mounted) {
AppState.hasCameraPermissions = true;
unawaited(_mainCameraController.selectCamera(0, true));
}
});
@ -58,6 +63,17 @@ class CameraSendToViewState extends State<CameraSendToView> {
isVisible: true,
),
),
// Always-visible back button so the user can exit regardless of
// camera state (e.g. during init or after init failure).
Positioned(
left: 5,
top: MediaQuery.paddingOf(context).top + 10,
child: ActionButton(
FontAwesomeIcons.xmark,
tooltipText: context.lang.close,
onPressed: () => Navigator.pop(context),
),
),
],
),
);

View file

@ -310,10 +310,12 @@ class _ShareImageView extends State<ShareImageView> {
});
// in case mediaStoreFutureReady is ready, the image is stored in the originalPath
await insertMediaFileInMessagesTable(
unawaited(
insertMediaFileInMessagesTable(
widget.mediaFileService,
widget.selectedGroupIds.toList(),
additionalData: widget.additionalData,
),
);
if (context.mounted) {

View file

@ -359,11 +359,12 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
context.lang.dialogAskDeleteMediaFilePopTitle,
),
actions: [
FilledButton(
child: Text(context.lang.dialogAskDeleteMediaFilePopDelete),
MyButton(
variant: MyButtonVariant.primaryMiddle,
onPressed: () {
Navigator.pop(context, true);
},
child: Text(context.lang.dialogAskDeleteMediaFilePopDelete),
),
TextButton(
child: Text(context.lang.cancel),

View file

@ -47,6 +47,7 @@ class _ArchivedChatsViewState extends State<ArchivedChatsView> {
body: ListView(
children: _groupsArchived.map((group) {
return GroupListItemComp(
key: ValueKey(group.groupId),
group: group,
);
}).toList(),

View file

@ -15,7 +15,6 @@ import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
import 'package:twonly/src/visual/components/connection_status.comp.dart';
import 'package:twonly/src/visual/components/notification_badge.comp.dart';
import 'package:twonly/src/visual/themes/light.dart';
import 'package:twonly/src/visual/views/chats/chat_list_components/empty_chat_list.comp.dart';
import 'package:twonly/src/visual/views/chats/chat_list_components/group_list_item.comp.dart';
import 'package:twonly/src/visual/views/chats/chat_list_components/news_btn.comp.dart';
@ -196,8 +195,8 @@ class _ChatListViewState extends State<ChatListView> with AutomaticKeepAliveClie
child: Container(
width: 40,
height: 40,
decoration: const BoxDecoration(
color: primaryColor,
decoration: BoxDecoration(
color: context.color.primary,
shape: BoxShape.circle,
),
),
@ -329,7 +328,7 @@ class _ChatListViewState extends State<ChatListView> with AutomaticKeepAliveClie
FloatingActionButton(
heroTag: 'new_chat_fab',
elevation: 2,
backgroundColor: primaryColor,
backgroundColor: context.color.primary,
foregroundColor: Colors.black87,
onPressed: () => context.push(Routes.chatsStartNewChat),
child: const FaIcon(

View file

@ -6,13 +6,13 @@ import 'package:go_router/go_router.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/api/mediafiles/download.api.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
import 'package:twonly/src/visual/components/contact_labels.comp.dart';
import 'package:twonly/src/visual/components/flame_counter.comp.dart';
import 'package:twonly/src/visual/components/verification_badge.comp.dart';
import 'package:twonly/src/visual/context_menu/group.context_menu.dart';
@ -42,6 +42,8 @@ class _UserListItem extends State<GroupListItemComp> {
StreamSubscription<Message?>? _lastMessageStream;
StreamSubscription<Reaction?>? _lastReactionStream;
StreamSubscription<List<MediaFile>>? _lastMediaFilesStream;
Contact? _directContact;
StreamSubscription<List<Contact>>? _directContactStream;
List<Message> _previewMessages = [];
final List<MediaFile> _previewMediaFiles = [];
@ -60,6 +62,7 @@ class _UserListItem extends State<GroupListItemComp> {
_lastReactionStream?.cancel();
_lastMessageStream?.cancel();
_lastMediaFilesStream?.cancel();
_directContactStream?.cancel();
super.dispose();
}
@ -102,6 +105,19 @@ class _UserListItem extends State<GroupListItemComp> {
setState(() {});
});
if (widget.group.isDirectChat) {
_directContactStream = twonlyDB.groupsDao
.watchGroupContact(widget.group.groupId)
.listen((contacts) {
if (!mounted) return;
if (contacts.isNotEmpty) {
setState(() {
_directContact = contacts.first;
_receiverDeletedAccount = _directContact!.accountDeleted;
});
}
});
} else {
final groupContacts = await twonlyDB.groupsDao.getGroupContact(
widget.group.groupId,
);
@ -110,6 +126,7 @@ class _UserListItem extends State<GroupListItemComp> {
_receiverDeletedAccount = groupContacts.first.accountDeleted;
}
}
}
void _updateState(
Message? newLastMessage,
@ -239,8 +256,12 @@ class _UserListItem extends State<GroupListItemComp> {
child: ListTile(
title: Row(
children: [
Text(
substringBy(widget.group.groupName, 30),
Flexible(
child: Text(
widget.group.groupName,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
const SizedBox(width: 3),
VerificationBadgeComp(
@ -249,6 +270,18 @@ class _UserListItem extends State<GroupListItemComp> {
clickable: false,
size: 12,
),
if (widget.group.isDirectChat && _directContact != null) ...[
const SizedBox(width: 6),
Flexible(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
physics: const BouncingScrollPhysics(),
child: ContactLabels(
contactId: _directContact!.userId,
),
),
),
],
],
),
subtitle: _receiverDeletedAccount

View file

@ -2,7 +2,6 @@ import 'dart:async';
import 'package:clock/clock.dart';
import 'package:flutter/material.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/misc.dart';
@ -42,12 +41,12 @@ class _LastMessageTimeCompState extends State<LastMessageTimeComp> {
_actionSubscription = null;
if (widget.message != null) {
_actionSubscription = twonlyDB.messagesDao
.watchLastMessageAction(widget.message!.messageId)
.listen((lastAction) {
targetTime = lastAction?.actionAt ?? widget.message!.createdAt;
if (widget.message!.senderId == null) {
targetTime = widget.message!.openedAt ?? widget.message!.createdAt;
} else {
targetTime = widget.message!.createdAt;
}
_updateSeconds();
});
} else if (widget.dateTime != null) {
targetTime = widget.dateTime;
_updateSeconds();

View file

@ -5,7 +5,6 @@ import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/notification_badge.comp.dart';
import 'package:twonly/src/visual/themes/light.dart';
class NewsIconButtonComp extends StatelessWidget {
const NewsIconButtonComp({super.key});
@ -23,7 +22,7 @@ class NewsIconButtonComp extends StatelessWidget {
builder: (context, count, child) {
return NotificationBadgeComp(
count: count.toString(),
backgroundColor: primaryColor,
backgroundColor: context.color.primary,
textColor: Colors.black87,
child: IconButton(
onPressed: () => context.push(Routes.settingsHelpNews),

View file

@ -16,6 +16,7 @@ import 'package:twonly/src/services/api/messages.api.dart';
import 'package:twonly/src/services/notifications/background.notifications.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
import 'package:twonly/src/visual/components/contact_labels.comp.dart';
import 'package:twonly/src/visual/components/flame_counter.comp.dart';
import 'package:twonly/src/visual/components/verification_badge.comp.dart';
import 'package:twonly/src/visual/themes/colors.dart';
@ -234,13 +235,20 @@ class _ChatMessagesViewState extends State<ChatMessagesView>
messages = chatItems.reversed.toList();
});
if (wasSentByMe && itemScrollController.isAttached) {
if (wasSentByMe) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted || !itemScrollController.isAttached) return;
try {
unawaited(
itemScrollController.scrollTo(
index: 0,
duration: const Duration(milliseconds: 150),
),
);
} catch (_) {
// Ignore if the inner scroll controller is still not attached
}
});
}
final items = await MemoryItem.convertFromMessages(storedMediaFiles);
@ -303,7 +311,11 @@ class _ChatMessagesViewState extends State<ChatMessagesView>
Expanded(
child: ColoredBox(
color: Colors.transparent,
child: Row(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
children: [
Text(
substringBy(group.groupName, 20),
@ -317,6 +329,23 @@ class _ChatMessagesViewState extends State<ChatMessagesView>
FlameCounterWidget(groupId: group.groupId),
],
),
if (group.isDirectChat)
StreamBuilder<List<Contact>>(
stream: twonlyDB.groupsDao.watchGroupContact(
group.groupId,
),
builder: (context, snapshot) {
final contacts = snapshot.data ?? [];
if (contacts.isEmpty) {
return const SizedBox.shrink();
}
return ContactLabels(
contactId: contacts.first.userId,
);
},
),
],
),
),
),
],

View file

@ -15,11 +15,12 @@ import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dar
import 'package:twonly/src/services/api/messages.api.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/alert.dialog.dart';
import 'package:twonly/src/visual/components/emoji_picker.bottom.dart';
import 'package:twonly/src/visual/context_menu/context_menu.helper.dart';
import 'package:twonly/src/visual/elements/my_button.element.dart';
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layer_data.dart';
import 'package:twonly/src/visual/views/chats/message_info.view.dart';
import 'package:twonly/src/visual/views/chats/chat_messages_components/chat_list_entry.dart';
import 'package:twonly/src/visual/views/memories/synchronized_viewer.view.dart';
class MessageContextMenu extends StatelessWidget {
@ -83,8 +84,9 @@ class MessageContextMenu extends StatelessWidget {
return SynchronizedImageViewerScreen(
galleryItems: galleryItems,
initialIndex: 0,
activeMediaIdNotifier:
ValueNotifier(mediaFileService!.mediaFile.mediaId),
activeMediaIdNotifier: ValueNotifier(
mediaFileService!.mediaFile.mediaId,
),
);
},
transitionsBuilder: (context, animation, secondaryAnimation, child) {
@ -172,17 +174,13 @@ class MessageContextMenu extends StatelessWidget {
ContextMenuItem(
title: context.lang.delete,
onTap: () async {
final delete = await showAlertDialog(
final action = await showDeleteMessageOptions(
navigator.context,
navigator.context.lang.deleteTitle,
null,
customOk:
(message.senderId == null && !message.isDeletedFromSender)
? navigator.context.lang.deleteOkBtnForAll
: navigator.context.lang.deleteOkBtnForMe,
message,
group,
galleryItems,
);
if (delete) {
if (message.senderId == null && !message.isDeletedFromSender) {
if (action == 'delete_for_all') {
await twonlyDB.messagesDao.handleMessageDeletion(
null,
message.messageId,
@ -197,12 +195,11 @@ class MessageContextMenu extends StatelessWidget {
),
),
);
} else {
} else if (action == 'delete_for_me') {
await twonlyDB.messagesDao.deleteMessagesById(
message.messageId,
);
}
}
},
icon: FontAwesomeIcons.trash,
),
@ -230,22 +227,138 @@ class MessageContextMenu extends StatelessWidget {
}
}
Future<String?> showDeleteMessageOptions(
BuildContext context,
Message message,
Group group,
List<MemoryItem> galleryItems,
) async {
return showModalBottomSheet<String>(
context: context,
backgroundColor: Colors.transparent,
builder: (context) {
final isForAll = message.senderId == null && !message.isDeletedFromSender;
return Container(
width: double.infinity,
padding: const EdgeInsets.only(
left: 20,
right: 20,
top: 20,
bottom: 40,
),
decoration: BoxDecoration(
color: context.color.surfaceContainer,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(24),
topRight: Radius.circular(24),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Stack(
children: [
ChatListEntry(
group: group,
message: message,
galleryItems: galleryItems,
),
Positioned.fill(
child: GestureDetector(
onTap: () {
// Prevent clicks
},
child: Container(
color: Colors.transparent,
),
),
),
],
),
const SizedBox(height: 24),
const Divider(),
const SizedBox(height: 24),
if (isForAll) ...[
Center(
child: MyButton(
variant: MyButtonVariant.errorMiddle,
onPressed: () {
Navigator.pop(context, 'delete_for_all');
},
child: Text(
context.lang.deleteOkBtnForAll,
textAlign: TextAlign.center,
),
),
),
const SizedBox(height: 12),
],
Center(
child: MyButton(
variant: isForAll
? MyButtonVariant.secondaryDense
: MyButtonVariant.errorMiddle,
onPressed: () {
Navigator.pop(context, 'delete_for_me');
},
child: Text(
isForAll
? context.lang.deleteOnlyForMe
: context.lang.deleteOkBtnForMe,
textAlign: TextAlign.center,
),
),
),
const SizedBox(height: 12),
Center(
child: MyButton(
variant: MyButtonVariant.text,
onPressed: () {
Navigator.pop(context, 'cancel');
},
child: Text(context.lang.cancel, textAlign: TextAlign.center),
),
),
],
),
);
},
);
}
Future<void> editTextMessage(BuildContext context, Message message) async {
var newText = message.content;
final controller = TextEditingController(text: message.content);
await showDialog(
await showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) {
return AlertDialog(
content: StatefulBuilder(
return StatefulBuilder(
builder: (context, setState) {
return SingleChildScrollView(
return Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: Container(
padding: const EdgeInsets.only(
left: 20,
right: 20,
top: 20,
bottom: 40,
),
decoration: BoxDecoration(
color: context.color.surfaceContainer,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(24),
topRight: Radius.circular(24),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.only(top: 8),
child: TextField(
TextField(
controller: controller,
autofocus: true,
textCapitalization: TextCapitalization.sentences,
@ -259,20 +372,25 @@ Future<void> editTextMessage(BuildContext context, Message message) async {
border: OutlineInputBorder(),
),
),
),
],
),
);
},
),
actions: [
TextButton(
const SizedBox(height: 24),
Row(
children: [
Expanded(
child: MyButton(
variant: MyButtonVariant.secondaryMiddle,
onPressed: () {
Navigator.of(context).pop();
},
child: Text(context.lang.cancel),
child: Text(
context.lang.cancel,
textAlign: TextAlign.center,
),
TextButton(
),
),
const SizedBox(width: 12),
Expanded(
child: MyButton(
variant: MyButtonVariant.primaryMiddle,
onPressed: () async {
if (newText != null &&
newText != message.content &&
@ -288,8 +406,11 @@ Future<void> editTextMessage(BuildContext context, Message message) async {
await sendCipherTextToGroup(
message.groupId,
pb.EncryptedContent(
messageUpdate: pb.EncryptedContent_MessageUpdate(
type: pb.EncryptedContent_MessageUpdate_Type.EDIT_TEXT,
messageUpdate:
pb.EncryptedContent_MessageUpdate(
type: pb
.EncryptedContent_MessageUpdate_Type
.EDIT_TEXT,
senderMessageId: message.messageId,
text: newText,
timestamp: Int64(
@ -302,9 +423,19 @@ Future<void> editTextMessage(BuildContext context, Message message) async {
if (!context.mounted) return;
Navigator.of(context).pop();
},
child: Text(context.lang.ok),
child: Text(
context.lang.ok,
textAlign: TextAlign.center,
),
),
),
],
),
],
),
),
);
},
);
},
);

View file

@ -58,6 +58,8 @@ class _MessageInputState extends State<MessageInput> {
Timer? _nextTypingIndicator;
DateTime? _lastTextChangeTime;
int? _contactId;
Timer? _recordingTimer;
DateTime? _recordingStartTime;
Future<void> _sendMessage() async {
if (_textFieldController.text == '') return;
@ -104,6 +106,7 @@ class _MessageInputState extends State<MessageInput> {
_textFieldController.removeListener(_handleTextChange);
widget.textFieldFocus.removeListener(_handleTextFocusChange);
widget.textFieldFocus.dispose();
_recordingTimer?.cancel();
recorderController.dispose();
_nextTypingIndicator?.cancel();
@ -124,12 +127,6 @@ class _MessageInputState extends State<MessageInput> {
void _initializeControllers() {
recorderController = RecorderController();
recorderController.onCurrentDuration.listen((duration) {
if (!mounted) return;
setState(() {
_currentDuration = duration.inMilliseconds;
});
});
}
void _handleTextChange() {
@ -161,6 +158,21 @@ class _MessageInputState extends State<MessageInput> {
_recordingState = RecordingState.recording;
_currentDuration = 0;
});
_recordingStartTime = clock.now();
_recordingTimer?.cancel();
_recordingTimer = Timer.periodic(const Duration(milliseconds: 100), (
timer,
) {
if (!mounted) return;
setState(() {
if (_recordingStartTime != null) {
_currentDuration = clock
.now()
.difference(_recordingStartTime!)
.inMilliseconds;
}
});
});
await HapticFeedback.heavyImpact();
final audioTmpPath = '${AppEnvironment.cacheDir}/recording.m4a';
unawaited(
@ -171,6 +183,8 @@ class _MessageInputState extends State<MessageInput> {
}
Future<void> _stopAudioRecording() async {
_recordingTimer?.cancel();
_recordingTimer = null;
await HapticFeedback.heavyImpact();
setState(() {
_audioRecordingLock = false;
@ -200,6 +214,8 @@ class _MessageInputState extends State<MessageInput> {
}
Future<void> _cancelAudioRecording() async {
_recordingTimer?.cancel();
_recordingTimer = null;
setState(() {
_audioRecordingLock = false;
_cancelSlideOffset = 0;
@ -330,8 +346,7 @@ class _MessageInputState extends State<MessageInput> {
TextField(
controller: _textFieldController,
focusNode: widget.textFieldFocus,
textCapitalization:
TextCapitalization.sentences,
textCapitalization: TextCapitalization.sentences,
keyboardType: TextInputType.multiline,
showCursor:
_recordingState != RecordingState.recording,
@ -385,13 +400,10 @@ class _MessageInputState extends State<MessageInput> {
),
if (!_audioRecordingLock) ...[
SizedBox(
width:
(100 - _cancelSlideOffset) % 101,
width: (100 - _cancelSlideOffset) % 101,
),
Text(
context
.lang
.voiceMessageSlideToCancel,
context.lang.voiceMessageSlideToCancel,
),
] else ...[
Expanded(
@ -476,8 +488,7 @@ class _MessageInputState extends State<MessageInput> {
child: Stack(
clipBehavior: Clip.none,
children: [
if (_recordingState ==
RecordingState.recording &&
if (_recordingState == RecordingState.recording &&
!_audioRecordingLock)
Positioned.fill(
top: -120,
@ -513,8 +524,7 @@ class _MessageInputState extends State<MessageInput> {
),
),
),
if (_recordingState ==
RecordingState.recording &&
if (_recordingState == RecordingState.recording &&
!_audioRecordingLock)
Positioned.fill(
top: -20,

View file

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
@ -206,7 +207,15 @@ class _MessageInfoViewState extends State<MessageInfoView> {
'${context.lang.received}: ${friendlyDateTime(context, widget.message.ackByServer!)}',
),
if (userService.currentUser.isDeveloper)
Text('ID: ${widget.message.messageId}'),
GestureDetector(
onTap: () async {
await Clipboard.setData(
ClipboardData(text: widget.message.messageId),
);
await HapticFeedback.heavyImpact();
},
child: Text('ID: ${widget.message.messageId}'),
),
if (messageHistory.isNotEmpty) ...[
const SizedBox(height: 10),
const Divider(),

View file

@ -12,6 +12,7 @@ import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
import 'package:twonly/src/visual/components/contact_labels.comp.dart';
import 'package:twonly/src/visual/components/flame_counter.comp.dart';
import 'package:twonly/src/visual/components/verification_badge.comp.dart';
import 'package:twonly/src/visual/context_menu/group.context_menu.dart';
@ -216,32 +217,39 @@ class _StartNewChatView extends State<StartNewChatView> {
}
if (i < filteredContacts.length) {
final contact = filteredContacts[i];
return UserContextMenu(
key: ValueKey(filteredContacts[i].userId),
contact: filteredContacts[i],
child: ListTile(
key: ValueKey(contact.userId),
contact: contact,
child: ContactLabelsSubtitleBuilder(
contactId: contact.userId,
builder: (context, subtitleWidget) {
return ListTile(
title: Row(
children: [
Text(getContactDisplayName(filteredContacts[i])),
Text(getContactDisplayName(contact)),
Padding(
padding: const EdgeInsets.only(
right: 8,
left: 1,
),
child: VerificationBadgeComp(
contact: filteredContacts[i],
contact: contact,
),
),
FlameCounterWidget(
contactId: filteredContacts[i].userId,
contactId: contact.userId,
),
],
),
subtitle: subtitleWidget,
leading: AvatarIcon(
contactId: filteredContacts[i].userId,
contactId: contact.userId,
fontSize: 13,
),
onTap: () => _onTapUser(filteredContacts[i]),
onTap: () => _onTapUser(contact),
);
},
),
);
}

View file

@ -7,10 +7,13 @@ import 'package:go_router/go_router.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/alert.dialog.dart';
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
import 'package:twonly/src/visual/components/contact_labels.comp.dart';
import 'package:twonly/src/visual/components/flame_counter.comp.dart';
import 'package:twonly/src/visual/components/select_chat_deletion_time.comp.dart';
import 'package:twonly/src/visual/components/snackbar.dart';
@ -20,6 +23,7 @@ import 'package:twonly/src/visual/views/contact/contact_components/mutual_groups
import 'package:twonly/src/visual/views/contact/contact_components/restore_flame.comp.dart';
import 'package:twonly/src/visual/views/contact/contact_components/user_discovery_contact_settings.comp.dart';
import 'package:twonly/src/visual/views/contact/contact_components/verification_expansion_tile.comp.dart';
import 'package:twonly/src/visual/views/contact/select_contact_labels.view.dart';
import 'package:twonly/src/visual/views/groups/group.view.dart';
class ContactView extends StatefulWidget {
@ -225,6 +229,21 @@ class _ContactViewState extends State<ContactView> {
userService.currentUser.userId,
),
),
ContactLabelsSubtitleBuilder(
contactId: contact.userId,
builder: (context, subtitleWidget) {
return BetterListTile(
icon: FontAwesomeIcons.tag,
text: context.lang.contactLabelsTitle,
subtitle: subtitleWidget,
onTap: () {
context.navPush(
SelectContactLabelsView(contactId: contact.userId),
);
},
);
},
),
const Divider(),
RestoreFlameComp(
contactId: widget.userId,
@ -256,6 +275,48 @@ class _ContactViewState extends State<ContactView> {
text: context.lang.contactRemove,
onTap: () => handleUserRemoveRequest(contact),
),
if (userService.currentUser.isDeveloper) ...[
if (contact.signalVersion != SignalVersion.v2)
BetterListTile(
icon: FontAwesomeIcons.arrowsRotate,
text: 'Update Connection to V2 (PQXDH)',
onTap: () async {
final userData = await apiService.getUserById(contact.userId);
if (userData != null) {
await processSignalUserData(userData);
final updatedContact = await twonlyDB.contactsDao
.getContactById(contact.userId);
final isV2 =
updatedContact?.signalVersion == SignalVersion.v2;
if (context.mounted) {
showSnackbar(
context,
isV2
? 'Connection updated to V2 successfully'
: 'Failed to update connection to V2',
level: isV2
? SnackbarLevel.success
: SnackbarLevel.error,
);
}
}
},
),
const Divider(),
Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Center(
child: Text(
'Encryption: ${switch (contact.signalVersion) {
SignalVersion.v1 => 'Signal Protocol (v1)',
SignalVersion.v2 => 'PQXDH (v2)',
}}',
style: const TextStyle(color: Colors.grey),
),
),
),
],
],
),
);

View file

@ -107,6 +107,9 @@ class _RestoreFlameCompState extends State<RestoreFlameComp> {
@override
Widget build(BuildContext context) {
if (!userService.currentUser.showRestoreFlame) {
return const SizedBox.shrink();
}
if (_group == null || !isItPossibleToRestoreFlames(_group!)) {
return Container();
}
@ -115,7 +118,7 @@ class _RestoreFlameCompState extends State<RestoreFlameComp> {
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
onTap: _restoreFlames,
title: Text(
'Restore your ${_group!.maxFlameCounter} lost flames',
context.lang.restoreLostFlames(_group!.maxFlameCounter),
style: const TextStyle(fontWeight: FontWeight.w500),
),
trailing: const SizedBox(
@ -134,7 +137,7 @@ class _RestoreFlameCompState extends State<RestoreFlameComp> {
emoji: '🔥',
),
),
text: 'Restore your ${_group!.maxFlameCounter} lost flames',
text: context.lang.restoreLostFlames(_group!.maxFlameCounter),
);
}
}

View file

@ -0,0 +1,313 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.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/utils/misc.dart';
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
import 'package:twonly/src/visual/components/contact_labels.comp.dart';
import 'package:twonly/src/visual/components/label_editor_bottom_sheet.comp.dart';
import 'package:twonly/src/visual/components/snackbar.dart';
import 'package:twonly/src/visual/elements/my_button.element.dart';
class SelectContactLabelsView extends StatefulWidget {
const SelectContactLabelsView({
required this.contactId,
super.key,
});
final int contactId;
@override
State<SelectContactLabelsView> createState() => _SelectContactLabelsViewState();
}
class _SelectContactLabelsViewState extends State<SelectContactLabelsView> {
Contact? _contact;
List<Label> _allLabels = [];
Set<int> _selectedLabelIds = {};
late StreamSubscription<Contact?> _contactSub;
late StreamSubscription<List<Label>> _allLabelsSub;
late StreamSubscription<List<Label>> _contactLabelsSub;
@override
void initState() {
super.initState();
_contactSub = twonlyDB.contactsDao.watchContact(widget.contactId).listen((contact) {
if (mounted) {
setState(() {
_contact = contact;
});
}
});
_allLabelsSub = twonlyDB.labelsDao.watchAllLabels().listen((labels) {
if (mounted) {
setState(() {
_allLabels = labels;
});
}
});
_contactLabelsSub = twonlyDB.labelsDao.watchContactLabels(widget.contactId).listen((labels) {
if (mounted) {
setState(() {
_selectedLabelIds = labels.map((l) => l.id).toSet();
});
}
});
}
@override
void dispose() {
_contactSub.cancel();
_allLabelsSub.cancel();
_contactLabelsSub.cancel();
super.dispose();
}
Future<void> _toggleLabel(int labelId) async {
final newSet = Set<int>.from(_selectedLabelIds);
if (newSet.contains(labelId)) {
newSet.remove(labelId);
} else {
if (newSet.length >= 3) {
showSnackbar(
context,
context.lang.contactLabelsMaxLimit,
level: SnackbarLevel.warning,
);
return;
}
newSet.add(labelId);
}
setState(() {
_selectedLabelIds = newSet;
});
await twonlyDB.labelsDao.setContactLabels(widget.contactId, _selectedLabelIds.toList());
}
Future<void> _createLabel() async {
final result = await showModalBottomSheet<Map<String, dynamic>>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => const LabelEditorBottomSheet(),
);
if (result != null && mounted) {
final name = result['name'] as String;
final bgColor = result['backgroundColor'] as int;
final textColor = result['textColor'] as int;
await twonlyDB.labelsDao.createLabel(name, textColor, bgColor);
}
}
Future<void> _editLabel(Label label) async {
final result = await showModalBottomSheet<Map<String, dynamic>>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => LabelEditorBottomSheet(label: label),
);
if (result != null && mounted) {
final name = result['name'] as String;
final bgColor = result['backgroundColor'] as int;
final textColor = result['textColor'] as int;
await twonlyDB.labelsDao.updateLabel(label.id, name, textColor, bgColor);
}
}
Future<void> _deleteLabel(Label label) async {
final confirm = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(context.lang.deleteLabel),
content: Text(context.lang.deleteLabelConfirmation),
actions: [
Row(
children: [
Expanded(
child: MyButton(
variant: MyButtonVariant.text,
onPressed: () => Navigator.of(context).pop(false),
child: Text(context.lang.cancel),
),
),
const SizedBox(width: 8),
Expanded(
child: MyButton(
variant: MyButtonVariant.errorMiddle,
onPressed: () => Navigator.of(context).pop(true),
child: Text(context.lang.deleteLabel),
),
),
],
),
],
),
);
if ((confirm ?? false) && mounted) {
await twonlyDB.labelsDao.deleteLabel(label.id);
}
}
Widget _buildContactHeader() {
if (_contact == null) return const SizedBox.shrink();
final contact = _contact!;
return Container(
margin: const EdgeInsets.all(12),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(16),
),
child: Row(
children: [
AvatarIcon(contactId: contact.userId, fontSize: 24),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
getContactDisplayName(contact, maxLength: 25),
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
if (getContactDisplayName(contact) != contact.username)
Text(
'@${contact.username}',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).disabledColor,
),
),
const SizedBox(height: 6),
ContactLabels(contactId: contact.userId),
],
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(context.lang.contactLabelsTitle),
actions: [
IconButton(
icon: const Icon(Icons.add),
tooltip: context.lang.createLabel,
onPressed: _createLabel,
),
],
),
body: Column(
children: [
_buildContactHeader(),
Expanded(
child: _allLabels.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
context.lang.contactLabelsSubtitleEmpty,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).disabledColor,
),
),
const SizedBox(height: 16),
MyButton(
variant: MyButtonVariant.primaryMiddle,
onPressed: _createLabel,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.add, size: 20),
const SizedBox(width: 6),
Text(context.lang.createLabel),
],
),
),
],
),
)
: ListView.builder(
itemCount: _allLabels.length,
itemBuilder: (context, index) {
final label = _allLabels[index];
final isSelected = _selectedLabelIds.contains(label.id);
return CheckboxListTile(
value: isSelected,
onChanged: (_) => _toggleLabel(label.id),
title: Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: Color(label.backgroundColor),
borderRadius: BorderRadius.circular(12),
),
child: Text(
label.name,
style: TextStyle(
color: Color(label.textColor),
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
),
],
),
secondary: Row(
mainAxisSize: MainAxisSize.min,
children: [
Tooltip(
message: context.lang.editLabel,
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () => _editLabel(label),
child: const Padding(
padding: EdgeInsets.all(6),
child: FaIcon(FontAwesomeIcons.penToSquare, size: 16),
),
),
),
const SizedBox(width: 2),
Tooltip(
message: context.lang.deleteLabel,
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () => _deleteLabel(label),
child: const Padding(
padding: EdgeInsets.all(6),
child: FaIcon(FontAwesomeIcons.trashCan, size: 16, color: Colors.red),
),
),
),
],
),
);
},
),
),
],
),
);
}
}

View file

@ -11,6 +11,7 @@ import 'package:twonly/src/services/group.service.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/alert.dialog.dart';
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
import 'package:twonly/src/visual/components/contact_labels.comp.dart';
import 'package:twonly/src/visual/components/flame_counter.comp.dart';
import 'package:twonly/src/visual/components/select_chat_deletion_time.comp.dart';
import 'package:twonly/src/visual/components/snackbar.dart';
@ -248,7 +249,10 @@ class _GroupViewState extends State<GroupView> {
group: _group!,
contact: member.$1,
member: member.$2,
child: BetterListTile(
child: ContactLabelsSubtitleBuilder(
contactId: member.$1.userId,
builder: (context, subtitleWidget) {
return BetterListTile(
padding: const EdgeInsets.only(left: 13),
leading: AvatarIcon(
contactId: member.$1.userId,
@ -266,6 +270,7 @@ class _GroupViewState extends State<GroupView> {
),
],
),
subtitle: subtitleWidget,
trailing: (member.$2.memberState == MemberState.admin)
? Text(context.lang.admin)
: null,
@ -277,6 +282,8 @@ class _GroupViewState extends State<GroupView> {
),
);
},
);
},
),
);
}),

View file

@ -5,9 +5,12 @@ import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/group.service.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
import 'package:twonly/src/visual/components/contact_labels.comp.dart';
import 'package:twonly/src/visual/components/flame_counter.comp.dart';
import 'package:twonly/src/visual/components/verification_badge.comp.dart';
import 'package:twonly/src/visual/context_menu/user.context_menu.dart';
import 'package:twonly/src/visual/decorations/input_text.decoration.dart';
import 'package:twonly/src/visual/elements/my_button.element.dart';
class GroupCreateSelectGroupNameView extends StatefulWidget {
const GroupCreateSelectGroupNameView({
@ -59,20 +62,28 @@ class _GroupCreateSelectGroupNameViewState
title: Text(context.lang.selectGroupName),
),
floatingActionButtonAnimator: FloatingActionButtonAnimator.noAnimation,
floatingActionButton: FilledButton.icon(
floatingActionButton: MyButton(
variant: MyButtonVariant.primaryMiddle,
onPressed: (textFieldGroupName.text.isEmpty || _isLoading)
? null
: _createNewGroup,
label: Text(context.lang.createGroup),
icon: _isLoading
? const SizedBox(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (_isLoading)
const SizedBox(
width: 15,
height: 15,
child: CircularProgressIndicator.adaptive(
strokeWidth: 1,
),
)
: const FaIcon(FontAwesomeIcons.penToSquare),
else
const FaIcon(FontAwesomeIcons.penToSquare, size: 16),
const SizedBox(width: 8),
Text(context.lang.createGroup),
],
),
),
body: SafeArea(
child: Padding(
@ -111,20 +122,35 @@ class _GroupCreateSelectGroupNameViewState
return UserContextMenu(
key: ValueKey(user.userId),
contact: user,
child: ListTile(
child: ContactLabelsSubtitleBuilder(
contactId: user.userId,
builder: (context, subtitleWidget) {
return ListTile(
title: Row(
children: [
Text(getContactDisplayName(user)),
Padding(
padding: const EdgeInsets.only(
right: 8,
left: 1,
),
child: VerificationBadgeComp(
contact: user,
),
),
FlameCounterWidget(
contactId: user.userId,
prefix: true,
),
],
),
subtitle: subtitleWidget,
leading: AvatarIcon(
contactId: user.userId,
fontSize: 13,
),
);
},
),
);
},

View file

@ -10,11 +10,14 @@ import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
import 'package:twonly/src/visual/components/contact_labels.comp.dart';
import 'package:twonly/src/visual/components/flame_counter.comp.dart';
import 'package:twonly/src/visual/components/snackbar.dart';
import 'package:twonly/src/visual/components/verification_badge.comp.dart';
import 'package:twonly/src/visual/context_menu/user.context_menu.dart';
import 'package:twonly/src/visual/decorations/input_text.decoration.dart';
import 'package:twonly/src/visual/elements/contact_chip.element.dart';
import 'package:twonly/src/visual/elements/my_button.element.dart';
import 'package:twonly/src/visual/views/groups/group_create_select_group_name.view.dart';
class GroupCreateSelectMembersView extends StatefulWidget {
@ -131,14 +134,21 @@ class _StartNewChatView extends State<GroupCreateSelectMembersView> {
),
),
floatingActionButtonAnimator: FloatingActionButtonAnimator.noAnimation,
floatingActionButton: FilledButton.icon(
floatingActionButton: MyButton(
variant: MyButtonVariant.primaryMiddle,
onPressed: selectedUsers.isEmpty ? null : submitChanges,
label: Text(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const FaIcon(FontAwesomeIcons.penToSquare, size: 16),
const SizedBox(width: 8),
Text(
widget.groupId == null
? context.lang.next
: context.lang.updateGroup,
),
icon: const FaIcon(FontAwesomeIcons.penToSquare),
],
),
),
body: SafeArea(
child: Padding(
@ -209,19 +219,32 @@ class _StartNewChatView extends State<GroupCreateSelectMembersView> {
return UserContextMenu(
key: ValueKey(user.userId),
contact: user,
child: ListTile(
child: ContactLabelsSubtitleBuilder(
contactId: user.userId,
additionalSubtitle: alreadyInGroup.contains(user.userId)
? Text(context.lang.alreadyInGroup)
: null,
builder: (context, subtitleWidget) {
return ListTile(
title: Row(
children: [
Text(getContactDisplayName(user)),
Padding(
padding: const EdgeInsets.only(
right: 8,
left: 1,
),
child: VerificationBadgeComp(
contact: user,
),
),
FlameCounterWidget(
contactId: user.userId,
prefix: true,
),
],
),
subtitle: (alreadyInGroup.contains(user.userId))
? Text(context.lang.alreadyInGroup)
: null,
subtitle: subtitleWidget,
leading: AvatarIcon(
contactId: user.userId,
fontSize: 13,
@ -247,6 +270,8 @@ class _StartNewChatView extends State<GroupCreateSelectMembersView> {
onTap: () {
toggleSelectedUser(user.userId);
},
);
},
),
);
},

View file

@ -48,6 +48,7 @@ class HomeViewState extends State<HomeView> with WidgetsBindingObserver {
StreamSubscription<int>? _homeViewPageIndexSub;
StreamSubscription<NotificationResponse>? _selectNotificationSub;
static Uri? pendingSharedLink;
static final streamHomeViewPageIndex = StreamController<int>.broadcast();
static final streamSharedLink = StreamController<Uri>.broadcast();
@ -56,7 +57,10 @@ class HomeViewState extends State<HomeView> with WidgetsBindingObserver {
super.initState();
WidgetsBinding.instance.addObserver(this);
var initialPage = widget.initialPage;
if (initialPage == 1 && !userService.currentUser.startWithCameraOpen) {
if (HomeViewState.pendingSharedLink != null) {
initialPage = 1;
} else if (initialPage == 1 &&
!userService.currentUser.startWithCameraOpen) {
initialPage = 0;
}
_activePageIdx = initialPage;
@ -100,6 +104,22 @@ class HomeViewState extends State<HomeView> with WidgetsBindingObserver {
streamHomeViewPageIndex.add(0);
});
_sharedLinkSub = streamSharedLink.stream.listen((uri) {
HomeViewState.pendingSharedLink = null;
_mainCameraController.setSharedLinkForPreview(uri);
Permission.camera.isGranted.then((hasPermission) {
if (hasPermission && mounted) {
unawaited(_mainCameraController.selectCamera(0, true));
}
});
});
if (HomeViewState.pendingSharedLink != null) {
final link = HomeViewState.pendingSharedLink!;
HomeViewState.pendingSharedLink = null;
_mainCameraController.setSharedLinkForPreview(link);
}
if (initialPage == 1) {
Permission.camera.isGranted.then((hasPermission) {
if (hasPermission && mounted) {
@ -110,14 +130,11 @@ class HomeViewState extends State<HomeView> with WidgetsBindingObserver {
unawaited(_initAsync());
_sharedLinkSub = streamSharedLink.stream.listen(
_mainCameraController.setSharedLinkForPreview,
);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (widget.initialPage == 1 &&
!userService.currentUser.startWithCameraOpen ||
widget.initialPage == 0) {
if (_mainCameraController.sharedLinkForPreview == null &&
((widget.initialPage == 1 &&
!userService.currentUser.startWithCameraOpen) ||
widget.initialPage == 0)) {
streamHomeViewPageIndex.add(0);
}
Future.delayed(const Duration(seconds: 1), () {

View file

@ -113,6 +113,8 @@ class _MemoriesThumbnailCompState extends State<MemoriesThumbnailComp> {
}
if (_imageProvider != null) {
_imageStream?.removeListener(_listener);
_imageStream = null;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
final config = createLocalImageConfiguration(context);
@ -189,11 +191,22 @@ class _MemoriesThumbnailCompState extends State<MemoriesThumbnailComp> {
gaplessPlayback: true,
errorBuilder: (context, error, stackTrace) {
if (error.toString().contains('Invalid image data')) {
if (_selectedImageFile != null) {
_selectedImageFile?.deleteSync();
final fileToDelete = _selectedImageFile;
_selectedImageFile = null;
if (fileToDelete != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
try {
if (fileToDelete.existsSync()) {
fileToDelete.deleteSync();
}
} catch (_) {}
if (_retries < 3) {
_retries++;
_resolveImage();
}
});
}
}
Log.warn(error);
return ColoredBox(

View file

@ -170,6 +170,7 @@ class _RegisterViewState extends State<RegisterView> {
widget.callbackOnSuccess();
} catch (e, stack) {
Log.error('Error creating new user', error: e, stackTrace: stack);
await deleteLocalUserData();
if (mounted) {
setState(() {
_isTryingToRegister = false;

View file

@ -75,7 +75,7 @@ class _BackupSetupPageState extends State<BackupSetupPage> {
mainAxisSize: MainAxisSize.min,
children: [
Text(
'twonly Backup',
context.lang.settingsBackup,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),

View file

@ -8,7 +8,6 @@ import 'package:twonly/src/visual/components/avatar_icon.comp.dart'
show AvatarIcon;
import 'package:twonly/src/visual/elements/my_button.element.dart';
import 'package:twonly/src/visual/elements/my_input.element.dart';
import 'package:twonly/src/visual/themes/light.dart';
import 'package:twonly/src/visual/views/onboarding/setup/components/next_button.comp.dart';
class ProfileSetupPage extends StatefulWidget {
@ -67,7 +66,7 @@ class _ProfileSetupPageState extends State<ProfileSetupPage> {
foregroundDecoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: primaryColor,
color: context.color.primary,
width: 4,
),
),

View file

@ -6,6 +6,7 @@ import 'package:twonly/locator.dart';
import 'package:twonly/src/providers/settings.provider.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/custom_color_picker_dialog.comp.dart';
import 'package:twonly/src/visual/elements/radio_button.element.dart';
class AppearanceView extends StatefulWidget {
@ -36,7 +37,7 @@ class _AppearanceViewState extends State<AppearanceView> {
RadioButton<ThemeMode>(
value: ThemeMode.system,
groupValue: selectedValue,
label: 'System default',
label: context.lang.themeSystemDefault,
onChanged: (value) {
selectedValue = value;
Navigator.of(context).pop();
@ -45,7 +46,7 @@ class _AppearanceViewState extends State<AppearanceView> {
RadioButton<ThemeMode>(
value: ThemeMode.light,
groupValue: selectedValue,
label: 'Light',
label: context.lang.themeLight,
onChanged: (value) {
selectedValue = value;
Navigator.of(context).pop();
@ -54,7 +55,7 @@ class _AppearanceViewState extends State<AppearanceView> {
RadioButton<ThemeMode>(
value: ThemeMode.dark,
groupValue: selectedValue,
label: 'Dark',
label: context.lang.themeDark,
onChanged: (value) {
selectedValue = value;
Navigator.of(context).pop();
@ -72,6 +73,124 @@ class _AppearanceViewState extends State<AppearanceView> {
}
}
Future<void> _showSelectPrimaryColor(BuildContext context) async {
const presetColors = <Color>[
Color(0xFF57CC99), // Original Twonly Green
Color(0xFF3A76F0), // Signal Blue
Color(0xFF9D4EDD), // Purple
Color(0xFFFF5964), // Coral Red
Color(0xFFFF9F1C), // Amber Orange
];
await showModalBottomSheet<void>(
context: context,
showDragHandle: true,
builder: (context) {
final activeColor =
context.watch<SettingsChangeProvider>().primaryColor;
return Padding(
padding: const EdgeInsets.fromLTRB(24, 8, 24, 32),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.lang.settingsAppearancePrimaryColor,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: presetColors.map((color) {
final isSelected =
activeColor.toARGB32() == color.toARGB32();
return GestureDetector(
onTap: () async {
await context
.read<SettingsChangeProvider>()
.updatePrimaryColor(color);
},
child: Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: color.withValues(alpha: 0.4),
blurRadius: 8,
offset: const Offset(0, 3),
),
],
border: Border.all(
color:
isSelected ? Colors.white : Colors.transparent,
width: 3,
),
),
child: isSelected
? const Icon(
Icons.check,
color: Colors.white,
size: 24,
)
: null,
),
);
}).toList(),
),
const SizedBox(height: 24),
ListTile(
contentPadding: EdgeInsets.zero,
leading: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: const SweepGradient(
colors: [
Colors.red,
Colors.yellow,
Colors.green,
Colors.cyan,
Colors.blue,
Color(0xFFFF00FF),
Colors.red,
],
),
border: Border.all(color: Colors.white24, width: 2),
),
),
title: Text(context.lang.customColor),
trailing: const Icon(Icons.chevron_right),
onTap: () async {
final pickedValue = await showDialog<int>(
context: context,
builder: (context) => CustomColorPickerDialog(
initialColor: activeColor,
),
);
if (pickedValue != null && context.mounted) {
final pickedColor = Color(pickedValue);
await context
.read<SettingsChangeProvider>()
.updatePrimaryColor(pickedColor);
if (context.mounted) {
Navigator.of(context).pop();
}
}
},
),
],
),
);
},
);
}
Future<void> toggleShowNewsIcon() async {
await UserService.update((u) {
u.showNewsShortcut = !u.showNewsShortcut;
@ -90,9 +209,22 @@ class _AppearanceViewState extends State<AppearanceView> {
});
}
String _themeModeLabel(BuildContext context, ThemeMode mode) {
switch (mode) {
case ThemeMode.system:
return context.lang.themeSystemDefault;
case ThemeMode.light:
return context.lang.themeLight;
case ThemeMode.dark:
return context.lang.themeDark;
}
}
@override
Widget build(BuildContext context) {
final selectedTheme = context.watch<SettingsChangeProvider>().themeMode;
final primaryColor = context.watch<SettingsChangeProvider>().primaryColor;
return Scaffold(
appBar: AppBar(
title: Text(context.lang.settingsAppearance),
@ -105,13 +237,28 @@ class _AppearanceViewState extends State<AppearanceView> {
ListTile(
title: Text(context.lang.settingsAppearanceTheme),
subtitle: Text(
selectedTheme.name,
_themeModeLabel(context, selectedTheme),
style: const TextStyle(color: Colors.grey),
),
onTap: () async {
await _showSelectThemeMode(context);
},
),
ListTile(
title: Text(context.lang.settingsAppearancePrimaryColor),
trailing: Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: primaryColor,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 1.5),
),
),
onTap: () async {
await _showSelectPrimaryColor(context);
},
),
ListTile(
title: Text(context.lang.hideNewsIcon),
onTap: toggleShowNewsIcon,

View file

@ -1,13 +1,23 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/model/json/backup.model.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'
as server;
import 'package:twonly/src/providers/purchases.provider.dart';
import 'package:twonly/src/services/backup.service.dart';
import 'package:twonly/src/services/memories/memories_cloud.service.dart';
import 'package:twonly/src/services/subscription.service.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/elements/better_list_title.element.dart';
import 'package:twonly/src/visual/elements/my_button.element.dart';
import 'package:twonly/src/visual/views/settings/backup/memories_backup_detail.view.dart';
import 'package:twonly/src/visual/views/settings/backup/passwordless_recovery/components/status.passwordless_recovery.comp.dart';
import 'package:twonly/src/visual/views/settings/backup/passwordless_recovery/setup.passwordless_recovery.view.dart';
@ -21,6 +31,7 @@ class BackupView extends StatefulWidget {
class _BackupViewState extends State<BackupView> {
bool _isLoading = false;
CurrentBackupStatus? _backupStatus;
server.Response_MemoriesUsage? _memoriesUsage;
StreamSubscription<void>? _backupUpdateSub;
@override
@ -41,51 +52,29 @@ class _BackupViewState extends State<BackupView> {
Future<void> _loadBackupStatus() async {
setState(() => _isLoading = true);
final status = await BackupService.getData();
final memoriesUsage = await apiService.getMemoriesUsage();
if (!mounted) return;
setState(() {
_backupStatus = status;
_memoriesUsage = memoriesUsage;
_isLoading = false;
});
}
String _getBackupStatusString(LastBackupUploadState status) {
switch (status) {
case LastBackupUploadState.none:
return context.lang.backupPending;
case LastBackupUploadState.pending:
return context.lang.backupPending;
case LastBackupUploadState.failed:
return context.lang.backupFailed;
case LastBackupUploadState.success:
return context.lang.backupSuccess;
String _buildTileSubtitle(DateTime? date, int? size) {
if (date == null) return '-';
final dateStr = formatRelativeDateTime(context, date);
if (size != null && size > 0) {
return '$dateStr${formatBytes(size)}';
}
}
List<TableRow> _buildTableRows(List<(String, String)> rows) {
return rows.map((pair) {
return TableRow(
children: [
TableCell(
child: Text(pair.$1),
),
TableCell(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 4,
),
child: Text(
pair.$2,
textAlign: TextAlign.right,
),
),
),
],
);
}).toList();
return dateStr;
}
@override
Widget build(BuildContext context) {
final currentPlan = context.watch<PurchasesProvider>().plan;
final isFreePlan = currentPlan == SubscriptionPlan.Free;
return StreamBuilder<void>(
stream: userService.onUserUpdated,
builder: (context, _) {
@ -94,100 +83,18 @@ class _BackupViewState extends State<BackupView> {
title: Text(context.lang.settingsBackup),
),
body: Padding(
padding: const EdgeInsets.all(16),
padding: const EdgeInsets.symmetric(vertical: 16),
child: ListView(
children: [
const SizedBox(height: 8),
Text(
context.lang.backupTwonlySafeDesc,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
if (userService.currentUser.passwordLessRecovery != null)
const PasswordLessRecoveryStatus(),
if (userService.currentUser.isBackupEnabled)
Column(
children: [
const SizedBox(height: 32),
Center(
child: Text(
context.lang.backupIdentityHeader,
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
const SizedBox(height: 8),
Table(
defaultVerticalAlignment:
TableCellVerticalAlignment.middle,
children: _buildTableRows([
(
context.lang.backupLastBackupDate,
_backupStatus?.identityLastSuccessFull != null
? formatDateTime(
context,
_backupStatus!.identityLastSuccessFull,
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: PasswordLessRecoveryStatus(),
)
: '-',
),
(
context.lang.backupLastBackupSize,
_backupStatus?.identitySize != null
? formatBytes(_backupStatus!.identitySize!)
: '-',
),
(
context.lang.backupLastBackupResult,
_getBackupStatusString(
_backupStatus?.identityState ??
LastBackupUploadState.none,
),
),
]),
),
const SizedBox(height: 24),
Center(
child: Text(
context.lang.backupArchiveHeader,
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
const SizedBox(height: 8),
Table(
defaultVerticalAlignment:
TableCellVerticalAlignment.middle,
children: _buildTableRows([
(
context.lang.backupLastBackupDate,
_backupStatus?.archiveLastSuccessFull != null
? formatDateTime(
context,
_backupStatus!.archiveLastSuccessFull,
)
: '-',
),
(
context.lang.backupLastBackupSize,
_backupStatus?.archiveSize != null
? formatBytes(_backupStatus!.archiveSize!)
: '-',
),
(
context.lang.backupLastBackupResult,
_getBackupStatusString(
_backupStatus?.archiveState ??
LastBackupUploadState.none,
),
),
]),
),
],
),
if (userService.currentUser.passwordLessRecovery == null) ...[
const SizedBox(height: 20),
Center(
else
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Center(
child: MyButton(
variant: MyButtonVariant.primaryMiddle,
onPressed: () =>
@ -195,8 +102,86 @@ class _BackupViewState extends State<BackupView> {
child: Text(context.lang.passwordlessRecoveryEnableBtn),
),
),
],
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
context.lang.backupTwonlySafeDesc,
textAlign: TextAlign.center,
style: TextStyle(
color: context.color.onSurfaceVariant,
fontSize: 13,
),
),
),
const SizedBox(height: 20),
const Divider(),
const SizedBox(height: 8),
if (userService.currentUser.isBackupEnabled) ...[
// 1. Identity tile
BetterListTile(
icon: FontAwesomeIcons.userCheck,
text: context.lang.backupIdentityHeader,
subtitle: Text(
_buildTileSubtitle(
_backupStatus?.identityLastSuccessFull,
_backupStatus?.identitySize,
),
),
),
// 2. Contacts & Messages tile
BetterListTile(
icon: FontAwesomeIcons.comments,
text: context.lang.backupArchiveHeader,
subtitle: Text(
_buildTileSubtitle(
_backupStatus?.archiveLastSuccessFull,
_backupStatus?.archiveSize,
),
),
),
// 3. Memories tile
BetterListTile(
icon: FontAwesomeIcons.photoFilm,
text: context.lang.memoriesBackupTitle,
subtitle: Text(
isFreePlan
? context.lang.backupMemoriesUpgradeRequired
: (!userService.currentUser.isCloudBackupEnabled
? context.lang.backupMemoriesNotEnabled
: (_memoriesUsage != null
? '${formatBytes(_memoriesUsage!.currentBytes.toInt())} / ${formatBytes(_memoriesUsage!.maxBytes.toInt())}'
: '-')),
),
trailing: Icon(
Icons.chevron_right_rounded,
color: context.color.onSurfaceVariant,
),
onTap: () async {
if (isFreePlan) {
await context.push(Routes.settingsSubscription);
} else if (!userService
.currentUser
.isCloudBackupEnabled) {
await UserService.update(
(u) => u.isCloudBackupEnabled = true,
);
if (mounted) setState(() {});
unawaited(memoriesCloudService.checkUploads());
} else {
await context.navPush(const MemoriesBackupDetailView());
}
},
),
const SizedBox(height: 16),
const Divider(),
const SizedBox(height: 20),
],
Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
@ -212,9 +197,7 @@ class _BackupViewState extends State<BackupView> {
_isLoading = true;
});
await BackupService.makeBackup(force: true);
setState(() {
_isLoading = false;
});
await _loadBackupStatus();
},
child: Text(context.lang.backupTwonlySaveNow),
),

View file

@ -82,7 +82,7 @@ class _SetupBackupViewState extends State<SetupBackupView> {
onTap: () => FocusScope.of(context).unfocus(),
child: Scaffold(
appBar: AppBar(
title: const Text('twonly Backup'),
title: Text(context.lang.settingsBackup),
actions: [
IconButton(
onPressed: () => showBackupExplanation(context),
@ -127,12 +127,6 @@ class _SetupBackupViewState extends State<SetupBackupView> {
_repeatedController.text.isNotEmpty,
),
const SizedBox(height: 10),
Text(
context.lang.backupNoPasswordRecovery,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 12),
),
const SizedBox(height: 10),
Center(
child: MyButton(
variant: MyButtonVariant.primaryMiddle,

View file

@ -128,7 +128,7 @@ void showBackupExplanation(BuildContext context) {
),
const SizedBox(height: 24),
Text(
'twonly Backup',
context.lang.settingsBackup,
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,

View file

@ -0,0 +1,312 @@
import 'dart:async';
import 'package:drift/drift.dart' hide Column;
import 'package:flutter/material.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'
as server;
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/services/memories/memories_cloud.service.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/snackbar.dart';
import 'package:twonly/src/visual/elements/my_button.element.dart';
import 'package:twonly/src/visual/views/settings/data_and_storage/storage_contents.view.dart';
class MemoriesBackupDetailView extends StatefulWidget {
const MemoriesBackupDetailView({super.key});
@override
State<MemoriesBackupDetailView> createState() =>
_MemoriesBackupDetailViewState();
}
class _MemoriesBackupDetailViewState extends State<MemoriesBackupDetailView> {
server.Response_MemoriesUsage? _memoriesUsage;
int _cloudOnlyCount = 0;
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadStats();
}
Future<void> _loadStats() async {
final memoriesUsage = await apiService.getMemoriesUsage();
final cloudOnlyCount = await twonlyDB.mediaFilesDao
.getCloudOnlyMemoriesCount();
if (mounted) {
setState(() {
_memoriesUsage = memoriesUsage;
_cloudOnlyCount = cloudOnlyCount;
_isLoading = false;
});
}
}
Future<void> _disableBackup() async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
),
backgroundColor: Theme.of(context).colorScheme.surface,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
context.lang.settingsStorageDisableBackupTitle,
textAlign: TextAlign.center,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
const SizedBox(height: 16),
Text.rich(
TextSpan(
children: formattedText(
context,
context.lang.settingsStorageDisableBackupBody(
_cloudOnlyCount,
),
textColor: Theme.of(
context,
).colorScheme.onSurface.withValues(alpha: 0.8),
),
),
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 15),
),
const SizedBox(height: 24),
Row(
children: [
Expanded(
child: MyButton(
variant: MyButtonVariant.secondaryMiddle,
onPressed: () => Navigator.pop(context, false),
child: Text(context.lang.galleryCancel),
),
),
const SizedBox(width: 12),
Expanded(
child: MyButton(
variant: MyButtonVariant.errorMiddle,
onPressed: () => Navigator.pop(context, true),
child: Text(
context.lang.settingsStorageDisableBackupBtn,
),
),
),
],
),
],
),
),
),
);
if (confirmed == true) {
try {
await apiService.disableMemoriesBackup();
final allMedias = await (twonlyDB.select(
twonlyDB.mediaFiles,
)..where((t) => t.stored.equals(true))).get();
for (final media in allMedias) {
final ms = MediaFileService(media);
if (!ms.storedPath.existsSync()) {
ms.fullMediaRemoval();
await twonlyDB.mediaFilesDao.deleteMediaFile(media.mediaId);
}
}
await twonlyDB.mediaFilesDao.updateAllMediaFiles(
const MediaFilesCompanion(
cloudState: Value(CloudState.none),
),
);
await UserService.update((u) => u.isCloudBackupEnabled = false);
if (mounted) {
Navigator.pop(context);
}
} catch (e) {
if (mounted) {
showSnackbar(context, e.toString());
}
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(context.lang.memoriesBackupTitle),
),
body: _isLoading
? const Center(child: CircularProgressIndicator.adaptive())
: ListView(
padding: const EdgeInsets.all(24),
children: [
Text(
_memoriesUsage != null
? '${formatBytes(_memoriesUsage!.currentBytes.toInt())} / ${formatBytes(_memoriesUsage!.maxBytes.toInt())}'
: '-',
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
Container(
height: 24,
width: double.infinity,
decoration: BoxDecoration(
color: Colors.grey.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(12),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: LayoutBuilder(
builder: (context, constraints) {
if (_memoriesUsage == null ||
_memoriesUsage!.maxBytes == 0) {
return const SizedBox.shrink();
}
final maxWidth = constraints.maxWidth;
final current = _memoriesUsage!.currentBytes.toDouble();
final max = _memoriesUsage!.maxBytes.toDouble();
final usageWidth =
((current / max).clamp(0.0, 1.0)) * maxWidth;
return Row(
children: [
if (usageWidth > 0)
Container(
width: usageWidth,
color: Colors.blue,
),
],
);
},
),
),
),
const SizedBox(height: 32),
StreamBuilder<MemoriesBackupProgress?>(
initialData: memoriesCloudService.currentProgress,
stream: memoriesCloudService.progressStream,
builder: (context, snapshot) {
final progress = snapshot.data;
final isSyncing =
progress != null && progress.totalPending > 0;
final percent = isSyncing
? ((progress.currentUploaded / progress.totalPending) +
(progress.currentUploadProgress /
progress.totalPending))
.clamp(0.0, 1.0)
: 0.0;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (isSyncing) ...[
Text(
'Syncing: ${progress.currentUploaded} / ${progress.totalPending} files (${(percent * 100).toStringAsFixed(1)}%)',
style: const TextStyle(fontSize: 14),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
LinearProgressIndicator(
value: percent,
),
const SizedBox(height: 24),
],
Center(
child: MyButton(
variant: MyButtonVariant.primaryMiddle,
onPressed: () =>
context.navPush(const StorageContentsView()),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.folder_open_outlined,
size: 18,
),
const SizedBox(width: 8),
Text(context.lang.settingsStorageContents),
],
),
),
),
const SizedBox(height: 16),
Center(
child: MyButton(
variant: MyButtonVariant.secondaryDense,
onPressed: isSyncing
? null
: () async {
final memories = await twonlyDB
.mediaFilesDao
.getMemoriesToBackup();
if (memories.isEmpty) {
if (context.mounted) {
showSnackbar(
context,
context
.lang
.settingsStorageSyncUpToDate,
level: SnackbarLevel.success,
);
}
} else {
unawaited(
memoriesCloudService.checkUploads(),
);
}
},
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.sync, size: 18),
const SizedBox(width: 8),
Text(context.lang.settingsStorageSyncNow),
],
),
),
),
const SizedBox(height: 16),
Center(
child: MyButton(
variant: MyButtonVariant.secondaryDense,
onPressed: _disableBackup,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.cloud_off_outlined, size: 18),
const SizedBox(width: 8),
Text(
context
.lang
.settingsStorageDisableBackupAction,
),
],
),
),
),
],
);
},
),
],
),
);
}
}

View file

@ -3,7 +3,6 @@ import 'package:flutter/services.dart';
import 'package:twonly/src/services/passwordless_recovery.service.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/elements/my_input.element.dart';
import 'package:twonly/src/visual/themes/light.dart';
class _FactorOption {
const _FactorOption({
@ -113,7 +112,7 @@ class SecondFactorPicker extends StatelessWidget {
child: Container(
padding: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
color: isSelected ? primaryColor : Colors.transparent,
color: isSelected ? context.color.primary : Colors.transparent,
borderRadius: borderRadius,
),
child: Column(

View file

@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/themes/light.dart';
class ThresholdPicker extends StatelessWidget {
const ThresholdPicker({
@ -103,7 +102,7 @@ class ThresholdPicker extends StatelessWidget {
child: Container(
padding: const EdgeInsets.symmetric(vertical: 14),
decoration: BoxDecoration(
color: isSelected ? primaryColor : Colors.transparent,
color: isSelected ? context.color.primary : Colors.transparent,
borderRadius: borderRadius,
),
child: Center(

View file

@ -24,6 +24,12 @@ class _ChatSettingsViewState extends State<ChatSettingsView> {
});
}
Future<void> setShowRestoreFlame(bool value) async {
await UserService.update((u) {
u.showRestoreFlame = value;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
@ -55,6 +61,13 @@ class _ChatSettingsViewState extends State<ChatSettingsView> {
.automaticallyMarkEqualMediaFilesAsOpened,
onChanged: setAutomaticallyMarkEqualMediaFilesAsOpened,
),
SwitchListTile.adaptive(
title: Text(
context.lang.settingsShowRestoreFlameTitle,
),
value: userService.currentUser.showRestoreFlame,
onChanged: setShowRestoreFlame,
),
],
);
},

View file

@ -1,21 +1,12 @@
import 'dart:async';
import 'package:drift/drift.dart' hide Column;
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'
as server;
import 'package:twonly/src/providers/purchases.provider.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/services/memories/memories_cloud.service.dart';
import 'package:twonly/src/services/subscription.service.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/snackbar.dart';
import 'package:twonly/src/visual/elements/my_button.element.dart';
import 'package:twonly/src/visual/views/settings/data_and_storage/storage_contents.view.dart';
@ -28,8 +19,6 @@ class ManageStorageView extends StatefulWidget {
class _ManageStorageViewState extends State<ManageStorageView> {
Map<MediaType, int> _stats = {};
server.Response_MemoriesUsage? _memoriesUsage;
int _cloudOnlyCount = 0;
@override
void initState() {
@ -39,110 +28,13 @@ class _ManageStorageViewState extends State<ManageStorageView> {
Future<void> _loadStats() async {
final stats = await twonlyDB.mediaFilesDao.getStorageStats();
final memoriesUsage = await apiService.getMemoriesUsage();
final cloudOnlyCount = await twonlyDB.mediaFilesDao
.getCloudOnlyMemoriesCount();
if (mounted) {
setState(() {
_stats = stats;
_memoriesUsage = memoriesUsage;
_cloudOnlyCount = cloudOnlyCount;
});
}
}
Future<void> _disableBackup() async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
),
backgroundColor: Theme.of(context).colorScheme.surface,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
context.lang.settingsStorageDisableBackupTitle,
textAlign: TextAlign.center,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
const SizedBox(height: 16),
Text.rich(
TextSpan(
children: formattedText(
context,
context.lang.settingsStorageDisableBackupBody(
_cloudOnlyCount,
),
textColor: Theme.of(
context,
).colorScheme.onSurface.withValues(alpha: 0.8),
),
),
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 15),
),
const SizedBox(height: 24),
Row(
children: [
Expanded(
child: MyButton(
variant: MyButtonVariant.secondaryMiddle,
onPressed: () => Navigator.pop(context, false),
child: Text(context.lang.galleryCancel),
),
),
const SizedBox(width: 12),
Expanded(
child: MyButton(
variant: MyButtonVariant.errorMiddle,
onPressed: () => Navigator.pop(context, true),
child: Text(context.lang.settingsStorageDisableBackupBtn),
),
),
],
),
],
),
),
),
);
if (confirmed == true) {
try {
await apiService.disableMemoriesBackup();
final allMedias = await (twonlyDB.select(
twonlyDB.mediaFiles,
)..where((t) => t.stored.equals(true))).get();
for (final media in allMedias) {
final ms = MediaFileService(media);
if (!ms.storedPath.existsSync()) {
ms.fullMediaRemoval();
await twonlyDB.mediaFilesDao.deleteMediaFile(media.mediaId);
}
}
await twonlyDB.mediaFilesDao.updateAllMediaFiles(
const MediaFilesCompanion(
cloudState: Value(CloudState.none),
),
);
await UserService.update((u) => u.isCloudBackupEnabled = false);
await _loadStats();
} catch (e) {
if (mounted) {
showSnackbar(context, e.toString());
}
}
}
}
@override
Widget build(BuildContext context) {
final currentPlan = context.watch<PurchasesProvider>().plan;
@ -162,7 +54,7 @@ class _ManageStorageViewState extends State<ManageStorageView> {
body: ListView(
padding: const EdgeInsets.all(16),
children: [
if (isFreePlan) ...[
if (isFreePlan || !userService.currentUser.isCloudBackupEnabled) ...[
Card(
elevation: 0,
color: Theme.of(
@ -177,223 +69,44 @@ class _ManageStorageViewState extends State<ManageStorageView> {
),
),
child: InkWell(
onTap: () => context.push(Routes.settingsSubscription),
onTap: () {
if (isFreePlan) {
context.push(Routes.settingsSubscription);
} else {
context.push(Routes.settingsBackup);
}
},
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Icon(
Icons.cloud_off_outlined,
Icons.cloud_queue_rounded,
color: Theme.of(context).colorScheme.primary,
size: 24,
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.lang.settingsStorageNoCloudBackupTitle,
style: Theme.of(context).textTheme.titleSmall
?.copyWith(
child: Text(
context.lang.backupFreeSpaceWithCloud,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
context.lang.settingsStorageNoCloudBackupCard,
style: Theme.of(context).textTheme.bodySmall
?.copyWith(
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
Icon(
Icons.chevron_right_rounded,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
],
),
),
],
),
),
),
),
const SizedBox(height: 24),
] else ...[
Text(
context.lang.memoriesBackupTitle,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
if (!userService.currentUser.isCloudBackupEnabled) ...[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.lang.settingsStorageNoCloudBackupCard,
style: Theme.of(context).textTheme.bodyMedium
?.copyWith(
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
),
],
),
),
const SizedBox(width: 16),
MyButton(
variant: MyButtonVariant.primaryMiddle,
onPressed: () async {
await UserService.update(
(u) => u.isCloudBackupEnabled = true,
);
setState(() {});
unawaited(memoriesCloudService.checkUploads());
},
child: Text(context.lang.enable),
),
],
),
const SizedBox(height: 24),
const Divider(),
const SizedBox(height: 24),
] else ...[
Text(
_memoriesUsage != null
? '${formatBytes(_memoriesUsage!.currentBytes.toInt())} / ${formatBytes(_memoriesUsage!.maxBytes.toInt())}'
: '-',
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 24),
Container(
height: 24,
width: double.infinity,
decoration: BoxDecoration(
color: Colors.grey.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(12),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: LayoutBuilder(
builder: (context, constraints) {
if (_memoriesUsage == null ||
_memoriesUsage!.maxBytes == 0) {
return const SizedBox.shrink();
}
final maxWidth = constraints.maxWidth;
final current = _memoriesUsage!.currentBytes.toDouble();
final max = _memoriesUsage!.maxBytes.toDouble();
final usageWidth =
((current / max).clamp(0.0, 1.0)) * maxWidth;
return Row(
children: [
if (usageWidth > 0)
Container(
width: usageWidth,
color: Colors.blue,
),
],
);
},
),
),
),
StreamBuilder<MemoriesBackupProgress?>(
initialData: memoriesCloudService.currentProgress,
stream: memoriesCloudService.progressStream,
builder: (context, snapshot) {
final progress = snapshot.data;
final isSyncing =
progress != null && progress.totalPending > 0;
final percent = isSyncing
? ((progress.currentUploaded / progress.totalPending) +
(progress.currentUploadProgress /
progress.totalPending))
.clamp(0.0, 1.0)
: 0.0;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (isSyncing) ...[
const SizedBox(height: 12),
Text(
'Syncing: ${progress.currentUploaded} / ${progress.totalPending} files (${(percent * 100).toStringAsFixed(1)}%)',
style: const TextStyle(fontSize: 14),
),
const SizedBox(height: 6),
LinearProgressIndicator(
value: percent,
),
],
const SizedBox(height: 16),
Align(
child: MyButton(
variant: MyButtonVariant.primaryDense,
onPressed: isSyncing
? null
: () async {
final memories = await twonlyDB.mediaFilesDao
.getMemoriesToBackup();
if (memories.isEmpty) {
if (context.mounted) {
showSnackbar(
context,
context
.lang
.settingsStorageSyncUpToDate,
);
}
} else {
unawaited(
memoriesCloudService.checkUploads(),
);
}
},
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.sync, size: 18),
const SizedBox(width: 8),
Text(context.lang.settingsStorageSyncNow),
],
),
),
),
const SizedBox(height: 12),
Align(
child: MyButton(
variant: MyButtonVariant.secondaryDense,
onPressed: _disableBackup,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.cloud_off_outlined, size: 18),
const SizedBox(width: 8),
Text(
context.lang.settingsStorageDisableBackupAction,
),
],
),
),
),
],
);
},
),
const SizedBox(height: 24),
const Divider(),
const SizedBox(height: 24),
],
],
Text(
isFreePlan

Some files were not shown because too many files have changed in this diff Show more