Merge pull request #407 from twonlyapp/marmot

- New: Create custom shortcuts to quickly share images with pre-selected groups
- New: Seamless recovery for iOS reinstallations
- Improved: Redesigned snackbar notifications
- Improved: New backup mechanism to allow larger backup files
- Improved: Move keys into a centralized Rust-owned structure stored in secure storage
- Fix: Messages occasionally not received until app restart
- Fix: Multiple smaller issues
This commit is contained in:
Tobi 2026-05-13 19:44:22 +02:00 committed by GitHub
commit f7211fed08
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
169 changed files with 14658 additions and 3449 deletions

View file

@ -1,5 +1,15 @@
# Changelog # Changelog
## 0.2.11
- New: Create custom shortcuts to quickly share images with pre-selected groups
- New: Seamless recovery for iOS reinstallations
- Improved: Redesigned snackbar notifications
- Improved: New backup mechanism to allow larger backup files
- Improved: Move keys into a centralized Rust-owned structure stored in secure storage
- Fix: Messages occasionally not received until app restart
- Fix: Multiple smaller issues
## 0.2.10 ## 0.2.10
- Fix: Issue with push notifications on Android - Fix: Issue with push notifications on Android

View file

@ -6,6 +6,8 @@ import dev.darttools.flutter_android_volume_keydown.FlutterAndroidVolumeKeydownP
import android.view.KeyEvent.KEYCODE_VOLUME_DOWN import android.view.KeyEvent.KEYCODE_VOLUME_DOWN
import android.view.KeyEvent.KEYCODE_VOLUME_UP import android.view.KeyEvent.KEYCODE_VOLUME_UP
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
import android.content.Context
import io.crates.keyring.Keyring
class MainActivity : FlutterFragmentActivity() { class MainActivity : FlutterFragmentActivity() {
@ -24,6 +26,8 @@ class MainActivity : FlutterFragmentActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) { override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine) super.configureFlutterEngine(flutterEngine)
Keyring.initializeNdkContext(applicationContext)
MediaStoreChannel.configure(flutterEngine, applicationContext) MediaStoreChannel.configure(flutterEngine, applicationContext)
VideoCompressionChannel.configure(flutterEngine, applicationContext) VideoCompressionChannel.configure(flutterEngine, applicationContext)
} }

View file

@ -3,10 +3,12 @@ package eu.twonly
import io.flutter.app.FlutterApplication import io.flutter.app.FlutterApplication
import dev.fluttercommunity.workmanager.WorkmanagerDebug import dev.fluttercommunity.workmanager.WorkmanagerDebug
import dev.fluttercommunity.workmanager.LoggingDebugHandler import dev.fluttercommunity.workmanager.LoggingDebugHandler
import io.crates.keyring.Keyring
class MyApplication : FlutterApplication() { class MyApplication : FlutterApplication() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
Keyring.initializeNdkContext(this)
// This enables the internal plugin logging to Logcat // This enables the internal plugin logging to Logcat
WorkmanagerDebug.setCurrent(LoggingDebugHandler()) WorkmanagerDebug.setCurrent(LoggingDebugHandler())
} }

View file

@ -0,0 +1,14 @@
package io.crates.keyring
import android.content.Context
class Keyring {
companion object {
init {
// Replace with the name of your compiled Rust library
System.loadLibrary("rust_lib_twonly")
}
// The underlying Rust crate provides the implementation for this
external fun initializeNdkContext(context: Context)
}
}

File diff suppressed because one or more lines are too long

View file

@ -18,11 +18,17 @@ import 'package:twonly/src/visual/views/home.view.dart';
import 'package:twonly/src/visual/views/onboarding/onboarding.view.dart'; import 'package:twonly/src/visual/views/onboarding/onboarding.view.dart';
import 'package:twonly/src/visual/views/onboarding/register.view.dart'; import 'package:twonly/src/visual/views/onboarding/register.view.dart';
import 'package:twonly/src/visual/views/onboarding/setup.view.dart'; import 'package:twonly/src/visual/views/onboarding/setup.view.dart';
import 'package:twonly/src/visual/views/recovery.view.dart';
import 'package:twonly/src/visual/views/unlock_twonly.view.dart'; import 'package:twonly/src/visual/views/unlock_twonly.view.dart';
class App extends StatefulWidget { class App extends StatefulWidget {
const App({required this.storageError, super.key}); const App({
required this.storageError,
required this.recoveryPossible,
super.key,
});
final bool storageError; final bool storageError;
final bool recoveryPossible;
@override @override
State<App> createState() => _AppState(); State<App> createState() => _AppState();
} }
@ -77,7 +83,6 @@ class _AppState extends State<App> with WidgetsBindingObserver {
if (widget.storageError) { if (widget.storageError) {
return MaterialApp( return MaterialApp(
scaffoldMessengerKey: AppGlobalKeys.scaffoldMessengerKey,
localizationsDelegates: localizationsDelegates, localizationsDelegates: localizationsDelegates,
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
supportedLocales: supportedLocales, supportedLocales: supportedLocales,
@ -89,9 +94,21 @@ class _AppState extends State<App> with WidgetsBindingObserver {
); );
} }
if (widget.recoveryPossible) {
return MaterialApp(
localizationsDelegates: localizationsDelegates,
debugShowCheckedModeBanner: false,
supportedLocales: supportedLocales,
title: 'twonly',
theme: lightTheme,
darkTheme: darkTheme,
themeMode: context.read<SettingsChangeProvider>().themeMode,
home: const RecoveryView(),
);
}
return MaterialApp.router( return MaterialApp.router(
routerConfig: routerProvider, routerConfig: routerProvider,
scaffoldMessengerKey: AppGlobalKeys.scaffoldMessengerKey,
localizationsDelegates: localizationsDelegates, localizationsDelegates: localizationsDelegates,
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
supportedLocales: supportedLocales, supportedLocales: supportedLocales,

View file

@ -0,0 +1,29 @@
// 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 '../lib.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
class BackupPasswordKeys {
final U8Array32 backupId;
final U8Array32 encryptionKey;
const BackupPasswordKeys({
required this.backupId,
required this.encryptionKey,
});
@override
int get hashCode => backupId.hashCode ^ encryptionKey.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BackupPasswordKeys &&
runtimeType == other.runtimeType &&
backupId == other.backupId &&
encryptionKey == other.encryptionKey;
}

View file

@ -9,7 +9,7 @@ import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
// These functions are ignored because they are not marked as `pub`: `get_twonly_flutter` // These functions are ignored because they are not marked as `pub`: `get_twonly_flutter`
// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `TwonlyFlutter` // These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `TwonlyFlutter`
Future<void> initializeTwonlyFlutter({required TwonlyConfig config}) => Future<void> initializeTwonlyFlutter({required InitConfig config}) =>
RustLib.instance.api.crateBridgeInitializeTwonlyFlutter(config: config); RustLib.instance.api.crateBridgeInitializeTwonlyFlutter(config: config);
class AnnouncedUser { class AnnouncedUser {
@ -36,6 +36,27 @@ class AnnouncedUser {
publicId == other.publicId; publicId == other.publicId;
} }
class InitConfig {
final String databaseDir;
final String dataDir;
const InitConfig({
required this.databaseDir,
required this.dataDir,
});
@override
int get hashCode => databaseDir.hashCode ^ dataDir.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is InitConfig &&
runtimeType == other.runtimeType &&
databaseDir == other.databaseDir &&
dataDir == other.dataDir;
}
class OtherPromotion { class OtherPromotion {
final int promotionId; final int promotionId;
final PlatformInt64 publicId; final PlatformInt64 publicId;
@ -74,24 +95,3 @@ class OtherPromotion {
announcementShare == other.announcementShare && announcementShare == other.announcementShare &&
publicKeyVerifiedTimestamp == other.publicKeyVerifiedTimestamp; publicKeyVerifiedTimestamp == other.publicKeyVerifiedTimestamp;
} }
class TwonlyConfig {
final String databasePath;
final String dataDirectory;
const TwonlyConfig({
required this.databasePath,
required this.dataDirectory,
});
@override
int get hashCode => databasePath.hashCode ^ dataDirectory.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is TwonlyConfig &&
runtimeType == other.runtimeType &&
databasePath == other.databasePath &&
dataDirectory == other.dataDirectory;
}

View file

@ -0,0 +1,87 @@
// 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 '../../keys/backup_password_keys.dart';
import '../../lib.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
class RustBackupArchive {
const RustBackupArchive();
static Future<(String, String)> createBackupArchive() => RustLib.instance.api
.crateBridgeWrapperBackupRustBackupArchiveCreateBackupArchive();
static Future<String?> getBackupDownloadToken() => RustLib.instance.api
.crateBridgeWrapperBackupRustBackupArchiveGetBackupDownloadToken();
static Future<void> restoreBackupArchive({required String filePath}) =>
RustLib.instance.api
.crateBridgeWrapperBackupRustBackupArchiveRestoreBackupArchive(
filePath: filePath,
);
@override
int get hashCode => 0;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RustBackupArchive && runtimeType == other.runtimeType;
}
class RustBackupIdentity {
const RustBackupIdentity();
static Future<String?> getBackupId() => RustLib.instance.api
.crateBridgeWrapperBackupRustBackupIdentityGetBackupId();
static Future<BackupPasswordKeys> getBackupPasswordKeys({
required PlatformInt64 userId,
required String password,
}) => RustLib.instance.api
.crateBridgeWrapperBackupRustBackupIdentityGetBackupPasswordKeys(
userId: userId,
password: password,
);
static Future<Uint8List> getIdentityBackupBytes() => RustLib.instance.api
.crateBridgeWrapperBackupRustBackupIdentityGetIdentityBackupBytes();
static Future<void> importBackupPasswordKeys({
required List<int> backupId,
required List<int> encryptionKey,
}) => RustLib.instance.api
.crateBridgeWrapperBackupRustBackupIdentityImportBackupPasswordKeys(
backupId: backupId,
encryptionKey: encryptionKey,
);
static Future<void> restoreIdentityBackup({
required BackupPasswordKeys keys,
required List<int> encryptedBytes,
}) => RustLib.instance.api
.crateBridgeWrapperBackupRustBackupIdentityRestoreIdentityBackup(
keys: keys,
encryptedBytes: encryptedBytes,
);
static Future<void> setBackupPasswordKeys({
required PlatformInt64 userId,
required String password,
}) => RustLib.instance.api
.crateBridgeWrapperBackupRustBackupIdentitySetBackupPasswordKeys(
userId: userId,
password: password,
);
@override
int get hashCode => 0;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RustBackupIdentity && runtimeType == other.runtimeType;
}

View file

@ -0,0 +1,77 @@
// 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 RustKeyManager {
const RustKeyManager();
static Future<Uint8List> getLoginToken() => RustLib.instance.api
.crateBridgeWrapperKeyManagerRustKeyManagerGetLoginToken();
static Future<(Uint8List, PlatformInt64)> getSignalIdentity() => RustLib
.instance
.api
.crateBridgeWrapperKeyManagerRustKeyManagerGetSignalIdentity();
static Future<PlatformInt64?> getUserId() => RustLib.instance.api
.crateBridgeWrapperKeyManagerRustKeyManagerGetUserId();
static Future<void> importSignalIdentity({
required List<int> identityKeyPairStructure,
required PlatformInt64 registrationId,
required Map<PlatformInt64, Uint8List> signedPreKeyStore,
}) => RustLib.instance.api
.crateBridgeWrapperKeyManagerRustKeyManagerImportSignalIdentity(
identityKeyPairStructure: identityKeyPairStructure,
registrationId: registrationId,
signedPreKeyStore: signedPreKeyStore,
);
static Future<Uint8List?> loadSignedPrekey({
required PlatformInt64 signedPreKeyId,
}) => RustLib.instance.api
.crateBridgeWrapperKeyManagerRustKeyManagerLoadSignedPrekey(
signedPreKeyId: signedPreKeyId,
);
static Future<Map<PlatformInt64, Uint8List>> loadSignedPrekeys() => RustLib
.instance
.api
.crateBridgeWrapperKeyManagerRustKeyManagerLoadSignedPrekeys();
static Future<void> removeKeyManager() => RustLib.instance.api
.crateBridgeWrapperKeyManagerRustKeyManagerRemoveKeyManager();
static Future<void> removeSignedPrekey({
required PlatformInt64 signedPreKeyId,
}) => RustLib.instance.api
.crateBridgeWrapperKeyManagerRustKeyManagerRemoveSignedPrekey(
signedPreKeyId: signedPreKeyId,
);
static Future<void> setUserId({required PlatformInt64 userId}) => RustLib
.instance
.api
.crateBridgeWrapperKeyManagerRustKeyManagerSetUserId(userId: userId);
static Future<void> storeSignedPrekey({
required PlatformInt64 signedPreKeyId,
required List<int> record,
}) => RustLib.instance.api
.crateBridgeWrapperKeyManagerRustKeyManagerStoreSignedPrekey(
signedPreKeyId: signedPreKeyId,
record: record,
);
@override
int get hashCode => 0;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RustKeyManager && runtimeType == other.runtimeType;
}

28
lib/core/context.dart Normal file
View file

@ -0,0 +1,28 @@
// 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 InitConfig {
final String databasePath;
final String dataDirectory;
const InitConfig({
required this.databasePath,
required this.dataDirectory,
});
@override
int get hashCode => databasePath.hashCode ^ dataDirectory.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is InitConfig &&
runtimeType == other.runtimeType &&
databasePath == other.databasePath &&
dataDirectory == other.dataDirectory;
}

File diff suppressed because it is too large Load diff

View file

@ -5,11 +5,15 @@
import 'bridge.dart'; import 'bridge.dart';
import 'bridge/callbacks.dart'; import 'bridge/callbacks.dart';
import 'bridge/wrapper/backup.dart';
import 'bridge/wrapper/key_manager.dart';
import 'bridge/wrapper/user_discovery.dart'; import 'bridge/wrapper/user_discovery.dart';
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:ffi' as ffi; import 'dart:ffi' as ffi;
import 'frb_generated.dart'; 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 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_io.dart';
abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> { abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@ -98,6 +102,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
Object dco_decode_DartOpaque(dynamic raw); Object dco_decode_DartOpaque(dynamic raw);
@protected
Map<PlatformInt64, Uint8List> dco_decode_Map_i_64_list_prim_u_8_strict_None(
dynamic raw,
);
@protected @protected
RustStreamSink<String> dco_decode_StreamSink_String_Sse(dynamic raw); RustStreamSink<String> dco_decode_StreamSink_String_Sse(dynamic raw);
@ -107,17 +116,23 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
AnnouncedUser dco_decode_announced_user(dynamic raw); AnnouncedUser dco_decode_announced_user(dynamic raw);
@protected
BackupPasswordKeys dco_decode_backup_password_keys(dynamic raw);
@protected @protected
bool dco_decode_bool(dynamic raw); bool dco_decode_bool(dynamic raw);
@protected @protected
AnnouncedUser dco_decode_box_autoadd_announced_user(dynamic raw); AnnouncedUser dco_decode_box_autoadd_announced_user(dynamic raw);
@protected
BackupPasswordKeys dco_decode_box_autoadd_backup_password_keys(dynamic raw);
@protected @protected
PlatformInt64 dco_decode_box_autoadd_i_64(dynamic raw); PlatformInt64 dco_decode_box_autoadd_i_64(dynamic raw);
@protected @protected
TwonlyConfig dco_decode_box_autoadd_twonly_config(dynamic raw); InitConfig dco_decode_box_autoadd_init_config(dynamic raw);
@protected @protected
FlutterUserDiscovery dco_decode_flutter_user_discovery(dynamic raw); FlutterUserDiscovery dco_decode_flutter_user_discovery(dynamic raw);
@ -125,6 +140,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
PlatformInt64 dco_decode_i_64(dynamic raw); PlatformInt64 dco_decode_i_64(dynamic raw);
@protected
InitConfig dco_decode_init_config(dynamic raw);
@protected @protected
PlatformInt64 dco_decode_isize(dynamic raw); PlatformInt64 dco_decode_isize(dynamic raw);
@ -140,6 +158,13 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw); Uint8List dco_decode_list_prim_u_8_strict(dynamic raw);
@protected
List<(PlatformInt64, Uint8List)>
dco_decode_list_record_i_64_list_prim_u_8_strict(dynamic raw);
@protected
String? dco_decode_opt_String(dynamic raw);
@protected @protected
AnnouncedUser? dco_decode_opt_box_autoadd_announced_user(dynamic raw); AnnouncedUser? dco_decode_opt_box_autoadd_announced_user(dynamic raw);
@ -159,7 +184,26 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
OtherPromotion dco_decode_other_promotion(dynamic raw); OtherPromotion dco_decode_other_promotion(dynamic raw);
@protected @protected
TwonlyConfig dco_decode_twonly_config(dynamic raw); (PlatformInt64, Uint8List) dco_decode_record_i_64_list_prim_u_8_strict(
dynamic raw,
);
@protected
(Uint8List, PlatformInt64) dco_decode_record_list_prim_u_8_strict_i_64(
dynamic raw,
);
@protected
(String, String) dco_decode_record_string_string(dynamic raw);
@protected
RustBackupArchive dco_decode_rust_backup_archive(dynamic raw);
@protected
RustBackupIdentity dco_decode_rust_backup_identity(dynamic raw);
@protected
RustKeyManager dco_decode_rust_key_manager(dynamic raw);
@protected @protected
int dco_decode_u_32(dynamic raw); int dco_decode_u_32(dynamic raw);
@ -167,6 +211,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
int dco_decode_u_8(dynamic raw); int dco_decode_u_8(dynamic raw);
@protected
U8Array32 dco_decode_u_8_array_32(dynamic raw);
@protected @protected
void dco_decode_unit(dynamic raw); void dco_decode_unit(dynamic raw);
@ -179,6 +226,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
Object sse_decode_DartOpaque(SseDeserializer deserializer); Object sse_decode_DartOpaque(SseDeserializer deserializer);
@protected
Map<PlatformInt64, Uint8List> sse_decode_Map_i_64_list_prim_u_8_strict_None(
SseDeserializer deserializer,
);
@protected @protected
RustStreamSink<String> sse_decode_StreamSink_String_Sse( RustStreamSink<String> sse_decode_StreamSink_String_Sse(
SseDeserializer deserializer, SseDeserializer deserializer,
@ -190,6 +242,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
AnnouncedUser sse_decode_announced_user(SseDeserializer deserializer); AnnouncedUser sse_decode_announced_user(SseDeserializer deserializer);
@protected
BackupPasswordKeys sse_decode_backup_password_keys(
SseDeserializer deserializer,
);
@protected @protected
bool sse_decode_bool(SseDeserializer deserializer); bool sse_decode_bool(SseDeserializer deserializer);
@ -198,13 +255,16 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer, SseDeserializer deserializer,
); );
@protected
BackupPasswordKeys sse_decode_box_autoadd_backup_password_keys(
SseDeserializer deserializer,
);
@protected @protected
PlatformInt64 sse_decode_box_autoadd_i_64(SseDeserializer deserializer); PlatformInt64 sse_decode_box_autoadd_i_64(SseDeserializer deserializer);
@protected @protected
TwonlyConfig sse_decode_box_autoadd_twonly_config( InitConfig sse_decode_box_autoadd_init_config(SseDeserializer deserializer);
SseDeserializer deserializer,
);
@protected @protected
FlutterUserDiscovery sse_decode_flutter_user_discovery( FlutterUserDiscovery sse_decode_flutter_user_discovery(
@ -214,6 +274,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
PlatformInt64 sse_decode_i_64(SseDeserializer deserializer); PlatformInt64 sse_decode_i_64(SseDeserializer deserializer);
@protected
InitConfig sse_decode_init_config(SseDeserializer deserializer);
@protected @protected
PlatformInt64 sse_decode_isize(SseDeserializer deserializer); PlatformInt64 sse_decode_isize(SseDeserializer deserializer);
@ -233,6 +296,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer); Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer);
@protected
List<(PlatformInt64, Uint8List)>
sse_decode_list_record_i_64_list_prim_u_8_strict(
SseDeserializer deserializer,
);
@protected
String? sse_decode_opt_String(SseDeserializer deserializer);
@protected @protected
AnnouncedUser? sse_decode_opt_box_autoadd_announced_user( AnnouncedUser? sse_decode_opt_box_autoadd_announced_user(
SseDeserializer deserializer, SseDeserializer deserializer,
@ -258,7 +330,32 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
OtherPromotion sse_decode_other_promotion(SseDeserializer deserializer); OtherPromotion sse_decode_other_promotion(SseDeserializer deserializer);
@protected @protected
TwonlyConfig sse_decode_twonly_config(SseDeserializer deserializer); (PlatformInt64, Uint8List) sse_decode_record_i_64_list_prim_u_8_strict(
SseDeserializer deserializer,
);
@protected
(Uint8List, PlatformInt64) sse_decode_record_list_prim_u_8_strict_i_64(
SseDeserializer deserializer,
);
@protected
(String, String) sse_decode_record_string_string(
SseDeserializer deserializer,
);
@protected
RustBackupArchive sse_decode_rust_backup_archive(
SseDeserializer deserializer,
);
@protected
RustBackupIdentity sse_decode_rust_backup_identity(
SseDeserializer deserializer,
);
@protected
RustKeyManager sse_decode_rust_key_manager(SseDeserializer deserializer);
@protected @protected
int sse_decode_u_32(SseDeserializer deserializer); int sse_decode_u_32(SseDeserializer deserializer);
@ -266,6 +363,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
int sse_decode_u_8(SseDeserializer deserializer); int sse_decode_u_8(SseDeserializer deserializer);
@protected
U8Array32 sse_decode_u_8_array_32(SseDeserializer deserializer);
@protected @protected
void sse_decode_unit(SseDeserializer deserializer); void sse_decode_unit(SseDeserializer deserializer);
@ -366,6 +466,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
void sse_encode_DartOpaque(Object self, SseSerializer serializer); void sse_encode_DartOpaque(Object self, SseSerializer serializer);
@protected
void sse_encode_Map_i_64_list_prim_u_8_strict_None(
Map<PlatformInt64, Uint8List> self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_StreamSink_String_Sse( void sse_encode_StreamSink_String_Sse(
RustStreamSink<String> self, RustStreamSink<String> self,
@ -378,6 +484,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
void sse_encode_announced_user(AnnouncedUser self, SseSerializer serializer); void sse_encode_announced_user(AnnouncedUser self, SseSerializer serializer);
@protected
void sse_encode_backup_password_keys(
BackupPasswordKeys self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_bool(bool self, SseSerializer serializer); void sse_encode_bool(bool self, SseSerializer serializer);
@ -387,6 +499,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void sse_encode_box_autoadd_backup_password_keys(
BackupPasswordKeys self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_box_autoadd_i_64( void sse_encode_box_autoadd_i_64(
PlatformInt64 self, PlatformInt64 self,
@ -394,8 +512,8 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
); );
@protected @protected
void sse_encode_box_autoadd_twonly_config( void sse_encode_box_autoadd_init_config(
TwonlyConfig self, InitConfig self,
SseSerializer serializer, SseSerializer serializer,
); );
@ -408,6 +526,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
void sse_encode_i_64(PlatformInt64 self, SseSerializer serializer); void sse_encode_i_64(PlatformInt64 self, SseSerializer serializer);
@protected
void sse_encode_init_config(InitConfig self, SseSerializer serializer);
@protected @protected
void sse_encode_isize(PlatformInt64 self, SseSerializer serializer); void sse_encode_isize(PlatformInt64 self, SseSerializer serializer);
@ -432,6 +553,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void sse_encode_list_record_i_64_list_prim_u_8_strict(
List<(PlatformInt64, Uint8List)> self,
SseSerializer serializer,
);
@protected
void sse_encode_opt_String(String? self, SseSerializer serializer);
@protected @protected
void sse_encode_opt_box_autoadd_announced_user( void sse_encode_opt_box_autoadd_announced_user(
AnnouncedUser? self, AnnouncedUser? self,
@ -469,7 +599,40 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
); );
@protected @protected
void sse_encode_twonly_config(TwonlyConfig self, SseSerializer serializer); void sse_encode_record_i_64_list_prim_u_8_strict(
(PlatformInt64, Uint8List) self,
SseSerializer serializer,
);
@protected
void sse_encode_record_list_prim_u_8_strict_i_64(
(Uint8List, PlatformInt64) self,
SseSerializer serializer,
);
@protected
void sse_encode_record_string_string(
(String, String) self,
SseSerializer serializer,
);
@protected
void sse_encode_rust_backup_archive(
RustBackupArchive self,
SseSerializer serializer,
);
@protected
void sse_encode_rust_backup_identity(
RustBackupIdentity self,
SseSerializer serializer,
);
@protected
void sse_encode_rust_key_manager(
RustKeyManager self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_u_32(int self, SseSerializer serializer); void sse_encode_u_32(int self, SseSerializer serializer);
@ -477,6 +640,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
void sse_encode_u_8(int self, SseSerializer serializer); void sse_encode_u_8(int self, SseSerializer serializer);
@protected
void sse_encode_u_8_array_32(U8Array32 self, SseSerializer serializer);
@protected @protected
void sse_encode_unit(void self, SseSerializer serializer); void sse_encode_unit(void self, SseSerializer serializer);

View file

@ -8,10 +8,14 @@
import 'bridge.dart'; import 'bridge.dart';
import 'bridge/callbacks.dart'; import 'bridge/callbacks.dart';
import 'bridge/wrapper/backup.dart';
import 'bridge/wrapper/key_manager.dart';
import 'bridge/wrapper/user_discovery.dart'; import 'bridge/wrapper/user_discovery.dart';
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'frb_generated.dart'; 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 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_web.dart';
abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> { abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@ -100,6 +104,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
Object dco_decode_DartOpaque(dynamic raw); Object dco_decode_DartOpaque(dynamic raw);
@protected
Map<PlatformInt64, Uint8List> dco_decode_Map_i_64_list_prim_u_8_strict_None(
dynamic raw,
);
@protected @protected
RustStreamSink<String> dco_decode_StreamSink_String_Sse(dynamic raw); RustStreamSink<String> dco_decode_StreamSink_String_Sse(dynamic raw);
@ -109,17 +118,23 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
AnnouncedUser dco_decode_announced_user(dynamic raw); AnnouncedUser dco_decode_announced_user(dynamic raw);
@protected
BackupPasswordKeys dco_decode_backup_password_keys(dynamic raw);
@protected @protected
bool dco_decode_bool(dynamic raw); bool dco_decode_bool(dynamic raw);
@protected @protected
AnnouncedUser dco_decode_box_autoadd_announced_user(dynamic raw); AnnouncedUser dco_decode_box_autoadd_announced_user(dynamic raw);
@protected
BackupPasswordKeys dco_decode_box_autoadd_backup_password_keys(dynamic raw);
@protected @protected
PlatformInt64 dco_decode_box_autoadd_i_64(dynamic raw); PlatformInt64 dco_decode_box_autoadd_i_64(dynamic raw);
@protected @protected
TwonlyConfig dco_decode_box_autoadd_twonly_config(dynamic raw); InitConfig dco_decode_box_autoadd_init_config(dynamic raw);
@protected @protected
FlutterUserDiscovery dco_decode_flutter_user_discovery(dynamic raw); FlutterUserDiscovery dco_decode_flutter_user_discovery(dynamic raw);
@ -127,6 +142,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
PlatformInt64 dco_decode_i_64(dynamic raw); PlatformInt64 dco_decode_i_64(dynamic raw);
@protected
InitConfig dco_decode_init_config(dynamic raw);
@protected @protected
PlatformInt64 dco_decode_isize(dynamic raw); PlatformInt64 dco_decode_isize(dynamic raw);
@ -142,6 +160,13 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw); Uint8List dco_decode_list_prim_u_8_strict(dynamic raw);
@protected
List<(PlatformInt64, Uint8List)>
dco_decode_list_record_i_64_list_prim_u_8_strict(dynamic raw);
@protected
String? dco_decode_opt_String(dynamic raw);
@protected @protected
AnnouncedUser? dco_decode_opt_box_autoadd_announced_user(dynamic raw); AnnouncedUser? dco_decode_opt_box_autoadd_announced_user(dynamic raw);
@ -161,7 +186,26 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
OtherPromotion dco_decode_other_promotion(dynamic raw); OtherPromotion dco_decode_other_promotion(dynamic raw);
@protected @protected
TwonlyConfig dco_decode_twonly_config(dynamic raw); (PlatformInt64, Uint8List) dco_decode_record_i_64_list_prim_u_8_strict(
dynamic raw,
);
@protected
(Uint8List, PlatformInt64) dco_decode_record_list_prim_u_8_strict_i_64(
dynamic raw,
);
@protected
(String, String) dco_decode_record_string_string(dynamic raw);
@protected
RustBackupArchive dco_decode_rust_backup_archive(dynamic raw);
@protected
RustBackupIdentity dco_decode_rust_backup_identity(dynamic raw);
@protected
RustKeyManager dco_decode_rust_key_manager(dynamic raw);
@protected @protected
int dco_decode_u_32(dynamic raw); int dco_decode_u_32(dynamic raw);
@ -169,6 +213,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
int dco_decode_u_8(dynamic raw); int dco_decode_u_8(dynamic raw);
@protected
U8Array32 dco_decode_u_8_array_32(dynamic raw);
@protected @protected
void dco_decode_unit(dynamic raw); void dco_decode_unit(dynamic raw);
@ -181,6 +228,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
Object sse_decode_DartOpaque(SseDeserializer deserializer); Object sse_decode_DartOpaque(SseDeserializer deserializer);
@protected
Map<PlatformInt64, Uint8List> sse_decode_Map_i_64_list_prim_u_8_strict_None(
SseDeserializer deserializer,
);
@protected @protected
RustStreamSink<String> sse_decode_StreamSink_String_Sse( RustStreamSink<String> sse_decode_StreamSink_String_Sse(
SseDeserializer deserializer, SseDeserializer deserializer,
@ -192,6 +244,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
AnnouncedUser sse_decode_announced_user(SseDeserializer deserializer); AnnouncedUser sse_decode_announced_user(SseDeserializer deserializer);
@protected
BackupPasswordKeys sse_decode_backup_password_keys(
SseDeserializer deserializer,
);
@protected @protected
bool sse_decode_bool(SseDeserializer deserializer); bool sse_decode_bool(SseDeserializer deserializer);
@ -200,13 +257,16 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer, SseDeserializer deserializer,
); );
@protected
BackupPasswordKeys sse_decode_box_autoadd_backup_password_keys(
SseDeserializer deserializer,
);
@protected @protected
PlatformInt64 sse_decode_box_autoadd_i_64(SseDeserializer deserializer); PlatformInt64 sse_decode_box_autoadd_i_64(SseDeserializer deserializer);
@protected @protected
TwonlyConfig sse_decode_box_autoadd_twonly_config( InitConfig sse_decode_box_autoadd_init_config(SseDeserializer deserializer);
SseDeserializer deserializer,
);
@protected @protected
FlutterUserDiscovery sse_decode_flutter_user_discovery( FlutterUserDiscovery sse_decode_flutter_user_discovery(
@ -216,6 +276,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
PlatformInt64 sse_decode_i_64(SseDeserializer deserializer); PlatformInt64 sse_decode_i_64(SseDeserializer deserializer);
@protected
InitConfig sse_decode_init_config(SseDeserializer deserializer);
@protected @protected
PlatformInt64 sse_decode_isize(SseDeserializer deserializer); PlatformInt64 sse_decode_isize(SseDeserializer deserializer);
@ -235,6 +298,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer); Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer);
@protected
List<(PlatformInt64, Uint8List)>
sse_decode_list_record_i_64_list_prim_u_8_strict(
SseDeserializer deserializer,
);
@protected
String? sse_decode_opt_String(SseDeserializer deserializer);
@protected @protected
AnnouncedUser? sse_decode_opt_box_autoadd_announced_user( AnnouncedUser? sse_decode_opt_box_autoadd_announced_user(
SseDeserializer deserializer, SseDeserializer deserializer,
@ -260,7 +332,32 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
OtherPromotion sse_decode_other_promotion(SseDeserializer deserializer); OtherPromotion sse_decode_other_promotion(SseDeserializer deserializer);
@protected @protected
TwonlyConfig sse_decode_twonly_config(SseDeserializer deserializer); (PlatformInt64, Uint8List) sse_decode_record_i_64_list_prim_u_8_strict(
SseDeserializer deserializer,
);
@protected
(Uint8List, PlatformInt64) sse_decode_record_list_prim_u_8_strict_i_64(
SseDeserializer deserializer,
);
@protected
(String, String) sse_decode_record_string_string(
SseDeserializer deserializer,
);
@protected
RustBackupArchive sse_decode_rust_backup_archive(
SseDeserializer deserializer,
);
@protected
RustBackupIdentity sse_decode_rust_backup_identity(
SseDeserializer deserializer,
);
@protected
RustKeyManager sse_decode_rust_key_manager(SseDeserializer deserializer);
@protected @protected
int sse_decode_u_32(SseDeserializer deserializer); int sse_decode_u_32(SseDeserializer deserializer);
@ -268,6 +365,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
int sse_decode_u_8(SseDeserializer deserializer); int sse_decode_u_8(SseDeserializer deserializer);
@protected
U8Array32 sse_decode_u_8_array_32(SseDeserializer deserializer);
@protected @protected
void sse_decode_unit(SseDeserializer deserializer); void sse_decode_unit(SseDeserializer deserializer);
@ -368,6 +468,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
void sse_encode_DartOpaque(Object self, SseSerializer serializer); void sse_encode_DartOpaque(Object self, SseSerializer serializer);
@protected
void sse_encode_Map_i_64_list_prim_u_8_strict_None(
Map<PlatformInt64, Uint8List> self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_StreamSink_String_Sse( void sse_encode_StreamSink_String_Sse(
RustStreamSink<String> self, RustStreamSink<String> self,
@ -380,6 +486,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
void sse_encode_announced_user(AnnouncedUser self, SseSerializer serializer); void sse_encode_announced_user(AnnouncedUser self, SseSerializer serializer);
@protected
void sse_encode_backup_password_keys(
BackupPasswordKeys self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_bool(bool self, SseSerializer serializer); void sse_encode_bool(bool self, SseSerializer serializer);
@ -389,6 +501,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void sse_encode_box_autoadd_backup_password_keys(
BackupPasswordKeys self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_box_autoadd_i_64( void sse_encode_box_autoadd_i_64(
PlatformInt64 self, PlatformInt64 self,
@ -396,8 +514,8 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
); );
@protected @protected
void sse_encode_box_autoadd_twonly_config( void sse_encode_box_autoadd_init_config(
TwonlyConfig self, InitConfig self,
SseSerializer serializer, SseSerializer serializer,
); );
@ -410,6 +528,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
void sse_encode_i_64(PlatformInt64 self, SseSerializer serializer); void sse_encode_i_64(PlatformInt64 self, SseSerializer serializer);
@protected
void sse_encode_init_config(InitConfig self, SseSerializer serializer);
@protected @protected
void sse_encode_isize(PlatformInt64 self, SseSerializer serializer); void sse_encode_isize(PlatformInt64 self, SseSerializer serializer);
@ -434,6 +555,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void sse_encode_list_record_i_64_list_prim_u_8_strict(
List<(PlatformInt64, Uint8List)> self,
SseSerializer serializer,
);
@protected
void sse_encode_opt_String(String? self, SseSerializer serializer);
@protected @protected
void sse_encode_opt_box_autoadd_announced_user( void sse_encode_opt_box_autoadd_announced_user(
AnnouncedUser? self, AnnouncedUser? self,
@ -471,7 +601,40 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
); );
@protected @protected
void sse_encode_twonly_config(TwonlyConfig self, SseSerializer serializer); void sse_encode_record_i_64_list_prim_u_8_strict(
(PlatformInt64, Uint8List) self,
SseSerializer serializer,
);
@protected
void sse_encode_record_list_prim_u_8_strict_i_64(
(Uint8List, PlatformInt64) self,
SseSerializer serializer,
);
@protected
void sse_encode_record_string_string(
(String, String) self,
SseSerializer serializer,
);
@protected
void sse_encode_rust_backup_archive(
RustBackupArchive self,
SseSerializer serializer,
);
@protected
void sse_encode_rust_backup_identity(
RustBackupIdentity self,
SseSerializer serializer,
);
@protected
void sse_encode_rust_key_manager(
RustKeyManager self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_u_32(int self, SseSerializer serializer); void sse_encode_u_32(int self, SseSerializer serializer);
@ -479,6 +642,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
void sse_encode_u_8(int self, SseSerializer serializer); void sse_encode_u_8(int self, SseSerializer serializer);
@protected
void sse_encode_u_8_array_32(U8Array32 self, SseSerializer serializer);
@protected @protected
void sse_encode_unit(void self, SseSerializer serializer); void sse_encode_unit(void self, SseSerializer serializer);

View file

@ -0,0 +1,29 @@
// 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 '../lib.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
class BackupPasswordKeys {
final U8Array32 backupId;
final U8Array32 encryptionKey;
const BackupPasswordKeys({
required this.backupId,
required this.encryptionKey,
});
@override
int get hashCode => backupId.hashCode ^ encryptionKey.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BackupPasswordKeys &&
runtimeType == other.runtimeType &&
backupId == other.backupId &&
encryptionKey == other.encryptionKey;
}

20
lib/core/lib.dart Normal file
View file

@ -0,0 +1,20 @@
// 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:collection/collection.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
class U8Array32 extends NonGrowableListView<int> {
static const arraySize = 32;
@internal
Uint8List get inner => _inner;
final Uint8List _inner;
U8Array32(this._inner) : assert(_inner.length == arraySize), super(_inner);
U8Array32.init() : this(Uint8List(arraySize));
}

View file

@ -1,13 +1,13 @@
import 'dart:async'; import 'dart:async';
import 'package:camera/camera.dart'; import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
class AppEnvironment { class AppEnvironment {
static late final String cacheDir; static late String cacheDir;
static late final String supportDir; static late String supportDir;
static bool _isInitialized = false;
static bool _isInitialized = false; static bool _isInitialized = false;
@ -22,10 +22,9 @@ class AppEnvironment {
_isInitialized = true; _isInitialized = true;
} }
static void initTesting() { static void initTesting({String? customCacheDir, String? customSupportDir}) {
if (_isInitialized) return; cacheDir = customCacheDir ?? '/tmp/twonly_cache';
cacheDir = '/tmp/twonly_cache'; supportDir = customSupportDir ?? '/tmp/twonly_support';
supportDir = '/tmp/twonly_support';
_isInitialized = true; _isInitialized = true;
} }
} }
@ -35,9 +34,5 @@ class AppState {
static bool isInBackgroundTask = false; static bool isInBackgroundTask = false;
static bool allowErrorTrackingViaSentry = false; static bool allowErrorTrackingViaSentry = false;
static bool gotMessageFromServer = false; static bool gotMessageFromServer = false;
static int latestAppVersionId = 110; static int latestAppVersionId = 113;
}
class AppGlobalKeys {
static final scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
} }

View file

@ -1,6 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
@ -8,11 +8,16 @@ import 'package:provider/provider.dart';
import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:twonly/app.dart'; import 'package:twonly/app.dart';
import 'package:twonly/core/bridge.dart' as bridge; import 'package:twonly/core/bridge.dart' as bridge;
import 'package:twonly/core/bridge/wrapper/key_manager.dart';
import 'package:twonly/core/frb_generated.dart'; import 'package:twonly/core/frb_generated.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/callbacks/callbacks.dart'; import 'package:twonly/src/callbacks/callbacks.dart';
import 'package:twonly/src/constants/secure_storage.keys.dart';
import 'package:twonly/src/database/signal/signal_signed_pre_key_store.dart'
show getSignalSignedPreKeyStoreOld;
import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/model/json/signal_identity.model.dart';
import 'package:twonly/src/providers/connection.provider.dart'; import 'package:twonly/src/providers/connection.provider.dart';
import 'package:twonly/src/providers/image_editor.provider.dart'; import 'package:twonly/src/providers/image_editor.provider.dart';
import 'package:twonly/src/providers/purchases.provider.dart'; import 'package:twonly/src/providers/purchases.provider.dart';
@ -21,7 +26,7 @@ import 'package:twonly/src/services/api/mediafiles/download.api.dart';
import 'package:twonly/src/services/api/mediafiles/media_background.api.dart'; import 'package:twonly/src/services/api/mediafiles/media_background.api.dart';
import 'package:twonly/src/services/api/mediafiles/upload.api.dart'; import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
import 'package:twonly/src/services/background/callback_dispatcher.background.dart'; import 'package:twonly/src/services/background/callback_dispatcher.background.dart';
import 'package:twonly/src/services/backup/create.backup.dart'; import 'package:twonly/src/services/backup.service.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/services/notifications/fcm.notifications.dart'; import 'package:twonly/src/services/notifications/fcm.notifications.dart';
import 'package:twonly/src/services/notifications/setup.notifications.dart'; import 'package:twonly/src/services/notifications/setup.notifications.dart';
@ -38,9 +43,9 @@ final _initMutex = Mutex();
/// This function is used to initialized the absolute minimum so it /// This function is used to initialized the absolute minimum so it
/// can also be used by the backend without the UI was loaded. /// can also be used by the backend without the UI was loaded.
Future<void> twonlyMinimumInitialization() async { Future<bool> twonlyMinimumInitialization() async {
Log.info('twonlyMinimumInitialization: called'); Log.info('twonlyMinimumInitialization: called');
await exclusiveAccess( final hasStorageError = await exclusiveAccess(
lockName: 'init', lockName: 'init',
mutex: _initMutex, mutex: _initMutex,
action: () async { action: () async {
@ -54,15 +59,22 @@ Future<void> twonlyMinimumInitialization() async {
await initFlutterCallbacksForRust(); await initFlutterCallbacksForRust();
Log.info('twonlyMinimumInitialization: bridge.initializeTwonlyFlutter()'); Log.info('twonlyMinimumInitialization: bridge.initializeTwonlyFlutter()');
try {
await bridge.initializeTwonlyFlutter( await bridge.initializeTwonlyFlutter(
config: bridge.TwonlyConfig( config: bridge.InitConfig(
databasePath: '${AppEnvironment.supportDir}/twonly.sqlite', databaseDir: AppEnvironment.supportDir,
dataDirectory: AppEnvironment.supportDir, dataDir: AppEnvironment.supportDir,
), ),
); );
} catch (e) {
Log.error(e);
return true;
}
Log.info('twonlyMinimumInitialization: finished'); Log.info('twonlyMinimumInitialization: finished');
return false;
}, },
); );
return hasStorageError;
} }
void main() async { void main() async {
@ -72,26 +84,30 @@ void main() async {
unawaited(StartupGuard.markAppStartup()); unawaited(StartupGuard.markAppStartup());
await twonlyMinimumInitialization(); var storageError = await twonlyMinimumInitialization();
await initFCMService();
unawaited(initFCMService());
var userExists = false; var userExists = false;
var storageError = false;
var recoveryPossible = false;
if (!storageError) {
try { try {
userExists = await userService.tryInit(); userExists = await userService.tryInit();
} catch (e) { } catch (e) {
Log.error('Failed to initialize user session due to storage error: $e'); Log.error('Failed to initialize user session due to storage error: $e');
storageError = true; storageError = true;
} }
}
if (Platform.isIOS && userExists) { if (!userExists && !storageError) {
final dbFile = File('${AppEnvironment.supportDir}/twonly.sqlite'); try {
if (!dbFile.existsSync()) { final userId = await RustKeyManager.getUserId();
Log.error('[twonly] IOS: App was removed and then reinstalled again...'); if (userId != null) {
await SecureStorage.instance.deleteAll(); recoveryPossible = true;
userExists = false; }
} catch (e) {
Log.error('Could not check KeyManager userId for iOS recovery: $e');
} }
} }
@ -139,7 +155,10 @@ void main() async {
ChangeNotifierProvider(create: (_) => ImageEditorProvider()), ChangeNotifierProvider(create: (_) => ImageEditorProvider()),
ChangeNotifierProvider(create: (_) => PurchasesProvider()), ChangeNotifierProvider(create: (_) => PurchasesProvider()),
], ],
child: App(storageError: storageError), child: App(
storageError: storageError,
recoveryPossible: recoveryPossible,
),
), ),
); );
} }
@ -178,6 +197,66 @@ Future<void> runMigrations() async {
} }
}); });
} }
if (userService.currentUser.appVersion < 113) {
var migrationSuccess = true;
final signalIdentity = await SecureStorage.instance.read(
// ignore: deprecated_member_use_from_same_package
key: SecureStorageKeys.signalIdentity,
);
if (signalIdentity != null) {
try {
final decoded = jsonDecode(signalIdentity);
final identity = SignalIdentity.fromJson(
decoded as Map<String, dynamic>,
);
await RustKeyManager.importSignalIdentity(
identityKeyPairStructure: identity.identityKeyPairU8List,
registrationId: identity.registrationId,
signedPreKeyStore: await getSignalSignedPreKeyStoreOld(),
);
Log.info('Importing signal identiy to the rust key manager');
// Clean up old keys after successful migration
await SecureStorage.instance.delete(
// ignore: deprecated_member_use_from_same_package
key: SecureStorageKeys.signalIdentity,
);
await SecureStorage.instance.delete(
// ignore: deprecated_member_use_from_same_package
key: SecureStorageKeys.signalSignedPreKey,
);
} catch (e) {
Log.error('Failed to migrate signal identity: $e');
migrationSuccess = false;
}
}
if (migrationSuccess) {
await UserService.update((u) {
u
..appVersion = 113
..canUseLoginTokenForAuth = false
// As usernames changes where not considered in the old version force users
// to reenter there passwords.
// ignore: deprecated_member_use_from_same_package
..twonlySafeBackup?.encryptionKey = []
// ignore: deprecated_member_use_from_same_package
..twonlySafeBackup?.backupId = [];
});
}
}
if (kDebugMode) {
assert(
AppState.latestAppVersionId == 113,
'Forgot to update the target version in runMigrations() after incrementing AppState.latestAppVersionId.',
);
assert(
AppState.latestAppVersionId == userService.currentUser.appVersion,
"Migration incomplete: currentUser.appVersion (${userService.currentUser.appVersion}) does not match AppState.latestAppVersionId (${AppState.latestAppVersionId}). Ensure the user's appVersion is updated in the migration block.",
);
}
} }
Future<void> postStartupTasks() async { Future<void> postStartupTasks() async {
@ -185,7 +264,6 @@ Future<void> postStartupTasks() async {
// 1. Immediate background cleanup (Non-blocking for UI) // 1. Immediate background cleanup (Non-blocking for UI)
await twonlyDB.messagesDao.purgeMessageTable(); await twonlyDB.messagesDao.purgeMessageTable();
unawaited(twonlyDB.receiptsDao.purgeReceivedReceipts()); unawaited(twonlyDB.receiptsDao.purgeReceivedReceipts());
unawaited(UserDiscoveryService.removeDeletedContacts());
unawaited(MediaFileService.purgeTempFolder()); unawaited(MediaFileService.purgeTempFolder());
// 2. Service initializations // 2. Service initializations
@ -193,25 +271,12 @@ Future<void> postStartupTasks() async {
unawaited(finishStartedPreprocessing()); unawaited(finishStartedPreprocessing());
unawaited(createPushAvatars()); unawaited(createPushAvatars());
if (userService.currentUser.userDiscoveryInitializationError) { unawaited(UserDiscoveryService.verifyInitializationOnStartup());
unawaited(() async {
try {
await UserDiscoveryService.initializeOrUpdate(
threshold: userService.currentUser.userDiscoveryThreshold,
sharePromotion: userService.currentUser.userDiscoverySharePromotion,
);
} catch (e) {
Log.error(
'Failed to retry UserDiscovery initialization on startup: $e',
);
}
}());
}
await Future.delayed(const Duration(seconds: 10)); await Future.delayed(const Duration(seconds: 10));
unawaited(initializeBackgroundTaskManager()); unawaited(initializeBackgroundTaskManager());
// 3. Delayed tasks (Wait for app to settle) // 3. Delayed tasks (Wait for app to settle)
await Future.delayed(const Duration(minutes: 2)); await Future.delayed(const Duration(minutes: 2));
unawaited(performTwonlySafeBackup()); unawaited(BackupService.makeBackup());
unawaited(cleanLogFile()); unawaited(cleanLogFile());
} }

View file

@ -38,23 +38,31 @@ class UserDiscoveryCallbacks {
Uint8List pubKey, Uint8List pubKey,
Uint8List signature, Uint8List signature,
) async { ) async {
try {
return Curve.verifySignature( return Curve.verifySignature(
IdentityKey.fromBytes(pubKey, 0).publicKey, IdentityKey.fromBytes(pubKey, 0).publicKey,
inputData, inputData,
signature, signature,
); );
} catch (_) {
return false;
}
} }
static Future<bool> verifyStoredPubKey( static Future<bool> verifyStoredPubKey(
int contactId, int contactId,
Uint8List pubKey, Uint8List pubKey,
) async { ) async {
try {
final storedPublicKey = await getPublicKeyFromContact(contactId); final storedPublicKey = await getPublicKeyFromContact(contactId);
if (storedPublicKey != null) { if (storedPublicKey != null) {
return storedPublicKey.equals(pubKey); return storedPublicKey.equals(pubKey);
} else { } else {
return false; return false;
} }
} catch (_) {
return false;
}
} }
static Future<bool> setShares(List<Uint8List> shares) async { static Future<bool> setShares(List<Uint8List> shares) async {

View file

@ -1,4 +1,6 @@
class KeyValueKeys { class KeyValueKeys {
static const String lastPeriodicTaskExecution = static const String lastPeriodicTaskExecution =
'last_periodic_task_execution'; 'last_periodic_task_execution';
static const String currentBackupState = 'current_backup_state';
static const String backupRecoveryState = 'backup_recovery_state';
} }

View file

@ -25,7 +25,6 @@ class Routes {
static const String settingsAccount = '/settings/account'; static const String settingsAccount = '/settings/account';
static const String settingsSubscription = '/settings/subscription'; static const String settingsSubscription = '/settings/subscription';
static const String settingsBackup = '/settings/backup'; static const String settingsBackup = '/settings/backup';
static const String settingsBackupServer = '/settings/backup/server';
static const String settingsBackupRecovery = '/settings/backup/recovery'; static const String settingsBackupRecovery = '/settings/backup/recovery';
static const String settingsBackupSetup = '/settings/backup/setup'; static const String settingsBackupSetup = '/settings/backup/setup';
static const String settingsAppearance = '/settings/appearance'; static const String settingsAppearance = '/settings/appearance';

View file

@ -1,11 +1,15 @@
class SecureStorageKeys { class SecureStorageKeys {
@Deprecated('Use the secure storage in rust')
static const String signalIdentity = 'signal_identity'; static const String signalIdentity = 'signal_identity';
@Deprecated('Use the secure storage in rust')
static const String signalSignedPreKey = 'signed_pre_key_store'; static const String signalSignedPreKey = 'signed_pre_key_store';
@Deprecated('Use the login token')
static const String apiAuthToken = 'api_auth_token'; static const String apiAuthToken = 'api_auth_token';
static const String googleFcm = 'google_fcm';
static const String userData = 'userData';
static const String twonlySafeLastBackupHash = 'twonly_safe_last_backup_hash';
@Deprecated('Use user.json file')
static const String userData = 'userData';
// Not required for backup...
static const String receivingPushKeys = 'push_keys_receiving'; static const String receivingPushKeys = 'push_keys_receiving';
static const String sendingPushKeys = 'push_keys_sending'; static const String sendingPushKeys = 'push_keys_sending';
} }

View file

@ -139,15 +139,10 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
} }
Future<Group?> _insertGroup(GroupsCompanion group) async { Future<Group?> _insertGroup(GroupsCompanion group) async {
try { await into(groups).insertOnConflictUpdate(group);
await into(groups).insert(group); return (select(
return await (select(
groups, groups,
)..where((t) => t.groupId.equals(group.groupId.value))).getSingle(); )..where((t) => t.groupId.equals(group.groupId.value))).getSingleOrNull();
} catch (e) {
Log.error('Could not insert group: $e');
return null;
}
} }
Future<List<Contact>> getGroupContact(String groupId) async { Future<List<Contact>> getGroupContact(String groupId) async {
@ -277,7 +272,7 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
groups.groupId.equalsExp(groupMembers.groupId), groups.groupId.equalsExp(groupMembers.groupId),
), ),
], ],
)..where(groups.isDirectChat.isNull())); )..where(groups.isDirectChat.equals(false)));
return query.map((row) => row.readTable(groupMembers)).get(); return query.map((row) => row.readTable(groupMembers)).get();
} catch (e) { } catch (e) {
Log.error(e); Log.error(e);

View file

@ -6,6 +6,7 @@ import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/tables/groups.table.dart'; import 'package:twonly/src/database/tables/groups.table.dart';
import 'package:twonly/src/database/tables/user_discovery.table.dart'; import 'package:twonly/src/database/tables/user_discovery.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/log.dart';
part 'key_verification.dao.g.dart'; part 'key_verification.dao.g.dart';
@ -89,10 +90,12 @@ class KeyVerificationDao extends DatabaseAccessor<TwonlyDB>
), ),
innerJoin(kv, kv.contactId.equalsExp(ur.fromContactId)), innerJoin(kv, kv.contactId.equalsExp(ur.fromContactId)),
], ],
)..where( )
..where(
ur.announcedUserId.equals(contactId) & ur.announcedUserId.equals(contactId) &
ur.publicKeyVerifiedTimestamp.isNotNull(), ur.publicKeyVerifiedTimestamp.isNotNull(),
); )
..groupBy([contacts.userId]);
return query.watch().map((rows) { return query.watch().map((rows) {
return rows.map((row) { return rows.map((row) {
@ -116,7 +119,8 @@ class KeyVerificationDao extends DatabaseAccessor<TwonlyDB>
..where( ..where(
ur.publicKeyVerifiedTimestamp.isNotNull() & ur.publicKeyVerifiedTimestamp.isNotNull() &
ur.announcedUserId.equalsExp(ur.fromContactId).not(), ur.announcedUserId.equalsExp(ur.fromContactId).not(),
); )
..groupBy([ur.announcedUserId]);
final rows = await query.get(); final rows = await query.get();
return rows.length; return rows.length;
@ -173,6 +177,7 @@ class KeyVerificationDao extends DatabaseAccessor<TwonlyDB>
} }
Future<void> addKeyVerification(int contactId, VerificationType type) async { Future<void> addKeyVerification(int contactId, VerificationType type) async {
try {
await into(keyVerifications).insertOnConflictUpdate( await into(keyVerifications).insertOnConflictUpdate(
KeyVerificationsCompanion( KeyVerificationsCompanion(
contactId: Value(contactId), contactId: Value(contactId),
@ -185,5 +190,8 @@ class KeyVerificationDao extends DatabaseAccessor<TwonlyDB>
publicKeyVerifiedTimestamp: clock.now().millisecondsSinceEpoch, publicKeyVerifiedTimestamp: clock.now().millisecondsSinceEpoch,
); );
} }
} catch (e) {
Log.error(e);
}
} }
} }

View file

@ -0,0 +1,79 @@
import 'package:drift/drift.dart';
import 'package:twonly/src/database/tables/shortcuts.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
part 'shortcuts.dao.g.dart';
@DriftAccessor(
tables: [
Shortcuts,
ShortcutMembers,
],
)
class ShortcutsDao extends DatabaseAccessor<TwonlyDB> with _$ShortcutsDaoMixin {
ShortcutsDao(super.db);
Stream<List<Shortcut>> watchAllShortcuts() {
return select(shortcuts).watch();
}
Future<Shortcut?> getShortcutByEmoji(String emoji) {
return (select(
shortcuts,
)..where((t) => t.emoji.equals(emoji))).getSingleOrNull();
}
Future<void> createShortcut(String emoji) async {
try {
await into(shortcuts).insert(
ShortcutsCompanion.insert(emoji: emoji),
);
// ignore: empty_catches
} catch (e) {}
}
Future<void> addShortcutMembers(int shortcutId, List<String> groupIds) async {
await batch((b) {
b.insertAll(
shortcutMembers,
groupIds.map(
(gId) => ShortcutMembersCompanion.insert(
shortcutId: shortcutId,
groupId: gId,
),
),
);
});
}
Future<List<ShortcutMember>> getShortcutMembers(int shortcutId) {
return (select(
shortcutMembers,
)..where((t) => t.shortcutId.equals(shortcutId))).get();
}
Future<void> incrementUsage(int shortcutId) async {
await customStatement(
'UPDATE shortcuts SET usage_counter = usage_counter + 1 WHERE id = ?',
[shortcutId],
);
// Notify updates to trigger streams
notifyUpdates({TableUpdate.onTable(shortcuts, kind: UpdateKind.update)});
}
Future<void> updateShortcut(int shortcutId, String emoji) async {
await (update(shortcuts)..where((t) => t.id.equals(shortcutId))).write(
ShortcutsCompanion(emoji: Value(emoji)),
);
}
Future<void> deleteShortcutMembers(int shortcutId) async {
await (delete(
shortcutMembers,
)..where((t) => t.shortcutId.equals(shortcutId))).go();
}
Future<void> deleteShortcut(int shortcutId) async {
await (delete(shortcuts)..where((t) => t.id.equals(shortcutId))).go();
}
}

View file

@ -0,0 +1,25 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'shortcuts.dao.dart';
// ignore_for_file: type=lint
mixin _$ShortcutsDaoMixin on DatabaseAccessor<TwonlyDB> {
$ShortcutsTable get shortcuts => attachedDatabase.shortcuts;
$GroupsTable get groups => attachedDatabase.groups;
$ShortcutMembersTable get shortcutMembers => attachedDatabase.shortcutMembers;
ShortcutsDaoManager get managers => ShortcutsDaoManager(this);
}
class ShortcutsDaoManager {
final _$ShortcutsDaoMixin _db;
ShortcutsDaoManager(this._db);
$$ShortcutsTableTableManager get shortcuts =>
$$ShortcutsTableTableManager(_db.attachedDatabase, _db.shortcuts);
$$GroupsTableTableManager get groups =>
$$GroupsTableTableManager(_db.attachedDatabase, _db.groups);
$$ShortcutMembersTableTableManager get shortcutMembers =>
$$ShortcutMembersTableTableManager(
_db.attachedDatabase,
_db.shortcutMembers,
);
}

File diff suppressed because it is too large Load diff

View file

@ -3,11 +3,11 @@ import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/core/bridge/wrapper/key_manager.dart';
import 'package:twonly/src/constants/secure_storage.keys.dart'; import 'package:twonly/src/constants/secure_storage.keys.dart';
import 'package:twonly/src/utils/secure_storage.dart'; import 'package:twonly/src/utils/secure_storage.dart';
class SignalSignedPreKeyStore extends SignedPreKeyStore { Future<HashMap<int, Uint8List>> getSignalSignedPreKeyStoreOld() async {
Future<HashMap<int, Uint8List>> getStore() async {
final storeSerialized = await SecureStorage.instance.read( final storeSerialized = await SecureStorage.instance.read(
key: SecureStorageKeys.signalSignedPreKey, key: SecureStorageKeys.signalSignedPreKey,
); );
@ -23,32 +23,23 @@ class SignalSignedPreKeyStore extends SignedPreKeyStore {
return store; return store;
} }
Future<void> safeStore(HashMap<int, Uint8List> store) async { class SignalSignedPreKeyStore extends SignedPreKeyStore {
final storeHashMap = <List<dynamic>>[];
for (final item in store.entries) {
storeHashMap.add([item.key, base64Encode(item.value)]);
}
final storeSerialized = json.encode(storeHashMap);
await SecureStorage.instance.write(
key: SecureStorageKeys.signalSignedPreKey,
value: storeSerialized,
);
}
@override @override
Future<SignedPreKeyRecord> loadSignedPreKey(int signedPreKeyId) async { Future<SignedPreKeyRecord> loadSignedPreKey(int signedPreKeyId) async {
final store = await getStore(); final store = await RustKeyManager.loadSignedPrekey(
if (!store.containsKey(signedPreKeyId)) { signedPreKeyId: signedPreKeyId,
);
if (store == null) {
throw InvalidKeyIdException( throw InvalidKeyIdException(
'No such signed prekey record! $signedPreKeyId', 'No such signed prekey record! $signedPreKeyId',
); );
} }
return SignedPreKeyRecord.fromSerialized(store[signedPreKeyId]!); return SignedPreKeyRecord.fromSerialized(store);
} }
@override @override
Future<List<SignedPreKeyRecord>> loadSignedPreKeys() async { Future<List<SignedPreKeyRecord>> loadSignedPreKeys() async {
final store = await getStore(); final store = await RustKeyManager.loadSignedPrekeys();
final results = <SignedPreKeyRecord>[]; final results = <SignedPreKeyRecord>[];
for (final serialized in store.values) { for (final serialized in store.values) {
results.add(SignedPreKeyRecord.fromSerialized(serialized)); results.add(SignedPreKeyRecord.fromSerialized(serialized));
@ -61,19 +52,21 @@ class SignalSignedPreKeyStore extends SignedPreKeyStore {
int signedPreKeyId, int signedPreKeyId,
SignedPreKeyRecord record, SignedPreKeyRecord record,
) async { ) async {
final store = await getStore(); await RustKeyManager.storeSignedPrekey(
store[signedPreKeyId] = record.serialize(); signedPreKeyId: signedPreKeyId,
await safeStore(store); record: record.serialize(),
);
} }
@override @override
Future<bool> containsSignedPreKey(int signedPreKeyId) async => Future<bool> containsSignedPreKey(int signedPreKeyId) async =>
(await getStore()).containsKey(signedPreKeyId); await RustKeyManager.loadSignedPrekey(
signedPreKeyId: signedPreKeyId,
) !=
null;
@override @override
Future<void> removeSignedPreKey(int signedPreKeyId) async { Future<void> removeSignedPreKey(int signedPreKeyId) async {
final store = await getStore(); await RustKeyManager.removeSignedPrekey(signedPreKeyId: signedPreKeyId);
store.remove(signedPreKeyId);
await safeStore(store);
} }
} }

View file

@ -0,0 +1,26 @@
import 'package:drift/drift.dart';
import 'package:twonly/src/database/tables/groups.table.dart';
@DataClassName('Shortcut')
class Shortcuts extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get emoji => text().unique()();
IntColumn get usageCounter => integer().withDefault(const Constant(0))();
}
@DataClassName('ShortcutMember')
class ShortcutMembers extends Table {
IntColumn get shortcutId => integer().references(
Shortcuts,
#id,
onDelete: KeyAction.cascade,
)();
TextColumn get groupId => text().references(
Groups,
#groupId,
onDelete: KeyAction.cascade,
)();
@override
Set<Column> get primaryKey => {shortcutId, groupId};
}

View file

@ -10,6 +10,7 @@ import 'package:twonly/src/database/daos/mediafiles.dao.dart';
import 'package:twonly/src/database/daos/messages.dao.dart'; import 'package:twonly/src/database/daos/messages.dao.dart';
import 'package:twonly/src/database/daos/reactions.dao.dart'; import 'package:twonly/src/database/daos/reactions.dao.dart';
import 'package:twonly/src/database/daos/receipts.dao.dart'; import 'package:twonly/src/database/daos/receipts.dao.dart';
import 'package:twonly/src/database/daos/shortcuts.dao.dart';
import 'package:twonly/src/database/daos/user_discovery.dao.dart'; import 'package:twonly/src/database/daos/user_discovery.dao.dart';
import 'package:twonly/src/database/tables/contacts.table.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/groups.table.dart';
@ -17,6 +18,7 @@ import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/tables/reactions.table.dart'; import 'package:twonly/src/database/tables/reactions.table.dart';
import 'package:twonly/src/database/tables/receipts.table.dart'; import 'package:twonly/src/database/tables/receipts.table.dart';
import 'package:twonly/src/database/tables/shortcuts.table.dart';
import 'package:twonly/src/database/tables/signal_identity_key_store.table.dart'; import 'package:twonly/src/database/tables/signal_identity_key_store.table.dart';
import 'package:twonly/src/database/tables/signal_pre_key_store.table.dart'; import 'package:twonly/src/database/tables/signal_pre_key_store.table.dart';
import 'package:twonly/src/database/tables/signal_sender_key_store.table.dart'; import 'package:twonly/src/database/tables/signal_sender_key_store.table.dart';
@ -52,6 +54,8 @@ part 'twonly.db.g.dart';
UserDiscoveryOtherPromotions, UserDiscoveryOtherPromotions,
UserDiscoveryOwnPromotions, UserDiscoveryOwnPromotions,
UserDiscoveryShares, UserDiscoveryShares,
Shortcuts,
ShortcutMembers,
], ],
daos: [ daos: [
MessagesDao, MessagesDao,
@ -62,6 +66,7 @@ part 'twonly.db.g.dart';
MediaFilesDao, MediaFilesDao,
UserDiscoveryDao, UserDiscoveryDao,
KeyVerificationDao, KeyVerificationDao,
ShortcutsDao,
], ],
) )
class TwonlyDB extends _$TwonlyDB { class TwonlyDB extends _$TwonlyDB {
@ -74,7 +79,7 @@ class TwonlyDB extends _$TwonlyDB {
TwonlyDB.forTesting(DatabaseConnection super.connection); TwonlyDB.forTesting(DatabaseConnection super.connection);
@override @override
int get schemaVersion => 12; int get schemaVersion => 13;
static QueryExecutor _openConnection() { static QueryExecutor _openConnection() {
return driftDatabase( return driftDatabase(
@ -186,6 +191,10 @@ class TwonlyDB extends _$TwonlyDB {
await m.addColumn(schema.contacts, column); await m.addColumn(schema.contacts, column);
} }
}, },
from12To13: (m, schema) async {
await m.createTable(schema.shortcuts);
await m.createTable(schema.shortcutMembers);
},
)(m, from, to); )(m, from, to);
}, },
); );

File diff suppressed because it is too large Load diff

View file

@ -6582,6 +6582,480 @@ i1.GeneratedColumn<i2.Uint8List> _column_235(String aliasedName) =>
type: i1.DriftSqlType.blob, type: i1.DriftSqlType.blob,
$customConstraints: 'NOT NULL', $customConstraints: 'NOT NULL',
); );
final class Schema13 extends i0.VersionedSchema {
Schema13({required super.database}) : super(version: 13);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
contacts,
groups,
mediaFiles,
messages,
messageHistories,
reactions,
groupMembers,
receipts,
receivedReceipts,
signalIdentityKeyStores,
signalPreKeyStores,
signalSenderKeyStores,
signalSessionStores,
messageActions,
groupHistories,
keyVerifications,
verificationTokens,
userDiscoveryAnnouncedUsers,
userDiscoveryUserRelations,
userDiscoveryOtherPromotions,
userDiscoveryOwnPromotions,
userDiscoveryShares,
shortcuts,
shortcutMembers,
];
late final Shape39 contacts = Shape39(
source: i0.VersionedTable(
entityName: 'contacts',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(user_id)'],
columns: [
_column_106,
_column_107,
_column_108,
_column_109,
_column_110,
_column_111,
_column_112,
_column_113,
_column_114,
_column_115,
_column_116,
_column_117,
_column_118,
_column_211,
_column_212,
_column_213,
_column_214,
_column_215,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape23 groups = Shape23(
source: i0.VersionedTable(
entityName: 'groups',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(group_id)'],
columns: [
_column_119,
_column_120,
_column_121,
_column_122,
_column_123,
_column_124,
_column_125,
_column_126,
_column_127,
_column_128,
_column_129,
_column_130,
_column_131,
_column_132,
_column_133,
_column_134,
_column_118,
_column_135,
_column_136,
_column_137,
_column_138,
_column_139,
_column_140,
_column_141,
_column_142,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape36 mediaFiles = Shape36(
source: i0.VersionedTable(
entityName: 'media_files',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(media_id)'],
columns: [
_column_143,
_column_144,
_column_145,
_column_146,
_column_147,
_column_148,
_column_149,
_column_207,
_column_150,
_column_151,
_column_152,
_column_153,
_column_154,
_column_155,
_column_156,
_column_157,
_column_118,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape25 messages = Shape25(
source: i0.VersionedTable(
entityName: 'messages',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(message_id)'],
columns: [
_column_158,
_column_159,
_column_160,
_column_144,
_column_161,
_column_162,
_column_163,
_column_164,
_column_165,
_column_153,
_column_166,
_column_167,
_column_168,
_column_169,
_column_118,
_column_170,
_column_171,
_column_172,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape26 messageHistories = Shape26(
source: i0.VersionedTable(
entityName: 'message_histories',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_173,
_column_174,
_column_175,
_column_161,
_column_118,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape27 reactions = Shape27(
source: i0.VersionedTable(
entityName: 'reactions',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(message_id, sender_id, emoji)'],
columns: [_column_174, _column_176, _column_177, _column_118],
attachedDatabase: database,
),
alias: null,
);
late final Shape38 groupMembers = Shape38(
source: i0.VersionedTable(
entityName: 'group_members',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(group_id, contact_id)'],
columns: [
_column_158,
_column_178,
_column_179,
_column_180,
_column_209,
_column_210,
_column_181,
_column_118,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape37 receipts = Shape37(
source: i0.VersionedTable(
entityName: 'receipts',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(receipt_id)'],
columns: [
_column_182,
_column_183,
_column_184,
_column_185,
_column_186,
_column_208,
_column_187,
_column_188,
_column_189,
_column_190,
_column_191,
_column_118,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape30 receivedReceipts = Shape30(
source: i0.VersionedTable(
entityName: 'received_receipts',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(receipt_id)'],
columns: [_column_182, _column_118],
attachedDatabase: database,
),
alias: null,
);
late final Shape31 signalIdentityKeyStores = Shape31(
source: i0.VersionedTable(
entityName: 'signal_identity_key_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(device_id, name)'],
columns: [_column_192, _column_193, _column_194, _column_118],
attachedDatabase: database,
),
alias: null,
);
late final Shape32 signalPreKeyStores = Shape32(
source: i0.VersionedTable(
entityName: 'signal_pre_key_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(pre_key_id)'],
columns: [_column_195, _column_196, _column_118],
attachedDatabase: database,
),
alias: null,
);
late final Shape11 signalSenderKeyStores = Shape11(
source: i0.VersionedTable(
entityName: 'signal_sender_key_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(sender_key_name)'],
columns: [_column_197, _column_198],
attachedDatabase: database,
),
alias: null,
);
late final Shape33 signalSessionStores = Shape33(
source: i0.VersionedTable(
entityName: 'signal_session_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(device_id, name)'],
columns: [_column_192, _column_193, _column_199, _column_118],
attachedDatabase: database,
),
alias: null,
);
late final Shape34 messageActions = Shape34(
source: i0.VersionedTable(
entityName: 'message_actions',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(message_id, contact_id, type)'],
columns: [_column_174, _column_183, _column_144, _column_200],
attachedDatabase: database,
),
alias: null,
);
late final Shape35 groupHistories = Shape35(
source: i0.VersionedTable(
entityName: 'group_histories',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(group_history_id)'],
columns: [
_column_201,
_column_158,
_column_202,
_column_203,
_column_204,
_column_205,
_column_206,
_column_144,
_column_200,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape40 keyVerifications = Shape40(
source: i0.VersionedTable(
entityName: 'key_verifications',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [_column_216, _column_183, _column_144, _column_118],
attachedDatabase: database,
),
alias: null,
);
late final Shape41 verificationTokens = Shape41(
source: i0.VersionedTable(
entityName: 'verification_tokens',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [_column_217, _column_218, _column_118],
attachedDatabase: database,
),
alias: null,
);
late final Shape42 userDiscoveryAnnouncedUsers = Shape42(
source: i0.VersionedTable(
entityName: 'user_discovery_announced_users',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(announced_user_id)'],
columns: [
_column_219,
_column_220,
_column_221,
_column_222,
_column_223,
_column_224,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape43 userDiscoveryUserRelations = Shape43(
source: i0.VersionedTable(
entityName: 'user_discovery_user_relations',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(announced_user_id, from_contact_id)'],
columns: [_column_225, _column_226, _column_227],
attachedDatabase: database,
),
alias: null,
);
late final Shape44 userDiscoveryOtherPromotions = Shape44(
source: i0.VersionedTable(
entityName: 'user_discovery_other_promotions',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(from_contact_id, public_id)'],
columns: [
_column_226,
_column_228,
_column_229,
_column_230,
_column_231,
_column_227,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape45 userDiscoveryOwnPromotions = Shape45(
source: i0.VersionedTable(
entityName: 'user_discovery_own_promotions',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [_column_232, _column_183, _column_233],
attachedDatabase: database,
),
alias: null,
);
late final Shape46 userDiscoveryShares = Shape46(
source: i0.VersionedTable(
entityName: 'user_discovery_shares',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [_column_234, _column_235, _column_175],
attachedDatabase: database,
),
alias: null,
);
late final Shape47 shortcuts = Shape47(
source: i0.VersionedTable(
entityName: 'shortcuts',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [_column_173, _column_236, _column_237],
attachedDatabase: database,
),
alias: null,
);
late final Shape48 shortcutMembers = Shape48(
source: i0.VersionedTable(
entityName: 'shortcut_members',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(shortcut_id, group_id)'],
columns: [_column_238, _column_158],
attachedDatabase: database,
),
alias: null,
);
}
class Shape47 extends i0.VersionedTable {
Shape47({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get emoji =>
columnsByName['emoji']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get usageCounter =>
columnsByName['usage_counter']! as i1.GeneratedColumn<int>;
}
i1.GeneratedColumn<String> _column_236(String aliasedName) =>
i1.GeneratedColumn<String>(
'emoji',
aliasedName,
false,
type: i1.DriftSqlType.string,
$customConstraints: 'NOT NULL UNIQUE',
);
i1.GeneratedColumn<int> _column_237(String aliasedName) =>
i1.GeneratedColumn<int>(
'usage_counter',
aliasedName,
false,
type: i1.DriftSqlType.int,
$customConstraints: 'NOT NULL DEFAULT 0',
defaultValue: const i1.CustomExpression('0'),
);
class Shape48 extends i0.VersionedTable {
Shape48({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get shortcutId =>
columnsByName['shortcut_id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get groupId =>
columnsByName['group_id']! as i1.GeneratedColumn<String>;
}
i1.GeneratedColumn<int> _column_238(String aliasedName) =>
i1.GeneratedColumn<int>(
'shortcut_id',
aliasedName,
false,
type: i1.DriftSqlType.int,
$customConstraints: 'NOT NULL REFERENCES shortcuts(id)ON DELETE CASCADE',
);
i0.MigrationStepWithVersion migrationSteps({ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2, required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3, required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
@ -6594,6 +7068,7 @@ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema10 schema) from9To10, required Future<void> Function(i1.Migrator m, Schema10 schema) from9To10,
required Future<void> Function(i1.Migrator m, Schema11 schema) from10To11, required Future<void> Function(i1.Migrator m, Schema11 schema) from10To11,
required Future<void> Function(i1.Migrator m, Schema12 schema) from11To12, required Future<void> Function(i1.Migrator m, Schema12 schema) from11To12,
required Future<void> Function(i1.Migrator m, Schema13 schema) from12To13,
}) { }) {
return (currentVersion, database) async { return (currentVersion, database) async {
switch (currentVersion) { switch (currentVersion) {
@ -6652,6 +7127,11 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema); final migrator = i1.Migrator(database, schema);
await from11To12(migrator, schema); await from11To12(migrator, schema);
return 12; return 12;
case 12:
final schema = Schema13(database: database);
final migrator = i1.Migrator(database, schema);
await from12To13(migrator, schema);
return 13;
default: default:
throw ArgumentError.value('Unknown migration from $currentVersion'); throw ArgumentError.value('Unknown migration from $currentVersion');
} }
@ -6670,6 +7150,7 @@ i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema10 schema) from9To10, required Future<void> Function(i1.Migrator m, Schema10 schema) from9To10,
required Future<void> Function(i1.Migrator m, Schema11 schema) from10To11, required Future<void> Function(i1.Migrator m, Schema11 schema) from10To11,
required Future<void> Function(i1.Migrator m, Schema12 schema) from11To12, required Future<void> Function(i1.Migrator m, Schema12 schema) from11To12,
required Future<void> Function(i1.Migrator m, Schema13 schema) from12To13,
}) => i0.VersionedSchema.stepByStepHelper( }) => i0.VersionedSchema.stepByStepHelper(
step: migrationSteps( step: migrationSteps(
from1To2: from1To2, from1To2: from1To2,
@ -6683,5 +7164,6 @@ i1.OnUpgrade stepByStep({
from9To10: from9To10, from9To10: from9To10,
from10To11: from10To11, from10To11: from10To11,
from11To12: from11To12, from11To12: from11To12,
from12To13: from12To13,
), ),
); );

View file

@ -596,6 +596,18 @@ abstract class AppLocalizations {
/// **'Notification'** /// **'Notification'**
String get settingsNotification; String get settingsNotification;
/// No description provided for @settingsNotifyPermission.
///
/// In en, this message translates to:
/// **'Notification permissions'**
String get settingsNotifyPermission;
/// No description provided for @settingsNotifyPermissionDesc.
///
/// In en, this message translates to:
/// **'Open system settings to allow push notifications.'**
String get settingsNotifyPermissionDesc;
/// No description provided for @settingsNotifyTroubleshooting. /// No description provided for @settingsNotifyTroubleshooting.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -1286,18 +1298,6 @@ abstract class AppLocalizations {
/// **'Open'** /// **'Open'**
String get open; String get open;
/// No description provided for @createVoucher.
///
/// In en, this message translates to:
/// **'Buy voucher'**
String get createVoucher;
/// No description provided for @redeemVoucher.
///
/// In en, this message translates to:
/// **'Redeem voucher'**
String get redeemVoucher;
/// No description provided for @buy. /// No description provided for @buy.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -1412,23 +1412,17 @@ abstract class AppLocalizations {
/// **'Due to twonly\'s security system, there is (currently) no password recovery function. Therefore, you must remember your password or, better yet, write it down.'** /// **'Due to twonly\'s security system, there is (currently) no password recovery function. Therefore, you must remember your password or, better yet, write it down.'**
String get backupNoPasswordRecovery; String get backupNoPasswordRecovery;
/// No description provided for @backupServer. /// No description provided for @backupIdentityHeader.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Server'** /// **'Identity'**
String get backupServer; String get backupIdentityHeader;
/// No description provided for @backupMaxBackupSize. /// No description provided for @backupArchiveHeader.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'max. backup size'** /// **'Contacts, Settings and Messages'**
String get backupMaxBackupSize; String get backupArchiveHeader;
/// No description provided for @backupStorageRetention.
///
/// In en, this message translates to:
/// **'Storage retention'**
String get backupStorageRetention;
/// No description provided for @backupLastBackupDate. /// No description provided for @backupLastBackupDate.
/// ///
@ -1448,12 +1442,6 @@ abstract class AppLocalizations {
/// **'Result'** /// **'Result'**
String get backupLastBackupResult; String get backupLastBackupResult;
/// No description provided for @backupData.
///
/// In en, this message translates to:
/// **'Data-Backup'**
String get backupData;
/// No description provided for @backupInsecurePassword. /// No description provided for @backupInsecurePassword.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -1511,39 +1499,15 @@ abstract class AppLocalizations {
/// No description provided for @backupPasswordRequirement. /// No description provided for @backupPasswordRequirement.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Password must be at least 8 characters long.'** /// **'Password must be at least 10 characters long.'**
String get backupPasswordRequirement; String get backupPasswordRequirement;
/// No description provided for @backupExpertSettings.
///
/// In en, this message translates to:
/// **'Expert settings'**
String get backupExpertSettings;
/// No description provided for @backupEnableBackup. /// No description provided for @backupEnableBackup.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Activate automatic backup'** /// **'Activate automatic backup'**
String get backupEnableBackup; String get backupEnableBackup;
/// No description provided for @backupOwnServerDesc.
///
/// In en, this message translates to:
/// **'Save your twonly Backup at twonly or on any server of your choice.'**
String get backupOwnServerDesc;
/// No description provided for @backupUseOwnServer.
///
/// In en, this message translates to:
/// **'Use server'**
String get backupUseOwnServer;
/// No description provided for @backupResetServer.
///
/// In en, this message translates to:
/// **'Use standard server'**
String get backupResetServer;
/// No description provided for @backupTwonlySaveNow. /// No description provided for @backupTwonlySaveNow.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -2330,12 +2294,6 @@ abstract class AppLocalizations {
/// **'Open your own QR code'** /// **'Open your own QR code'**
String get openYourOwnQRcode; String get openYourOwnQRcode;
/// No description provided for @skipForNow.
///
/// In en, this message translates to:
/// **'Skip for now'**
String get skipForNow;
/// No description provided for @finishSetupCardTitle. /// No description provided for @finishSetupCardTitle.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -2354,6 +2312,24 @@ abstract class AppLocalizations {
/// **'Resume Setup'** /// **'Resume Setup'**
String get finishSetupCardAction; String get finishSetupCardAction;
/// No description provided for @missingBackupCardTitle.
///
/// In en, this message translates to:
/// **'Setup backup'**
String get missingBackupCardTitle;
/// No description provided for @missingBackupCardDesc.
///
/// In en, this message translates to:
/// **'We have improved the backup mechanism, which requires you to set it up again.'**
String get missingBackupCardDesc;
/// No description provided for @missingBackupCardAction.
///
/// In en, this message translates to:
/// **'Set up now'**
String get missingBackupCardAction;
/// No description provided for @onboardingFinishLater. /// No description provided for @onboardingFinishLater.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -3061,6 +3037,126 @@ abstract class AppLocalizations {
/// In en, this message translates to: /// In en, this message translates to:
/// **'{maker} changed their display name from {oldName} to {newName}.'** /// **'{maker} changed their display name from {oldName} to {newName}.'**
String makerChangedDisplayName(Object maker, Object oldName, Object newName); String makerChangedDisplayName(Object maker, Object oldName, Object newName);
/// No description provided for @recoverErrorNoInternet.
///
/// In en, this message translates to:
/// **'No internet connection. Please check your network and try again.'**
String get recoverErrorNoInternet;
/// No description provided for @recoverErrorUsernameNotValid.
///
/// In en, this message translates to:
/// **'The username provided is not valid or does not exist.'**
String get recoverErrorUsernameNotValid;
/// No description provided for @recoverErrorPasswordInvalid.
///
/// In en, this message translates to:
/// **'The password provided is incorrect.'**
String get recoverErrorPasswordInvalid;
/// No description provided for @recoverErrorTryAgainLater.
///
/// In en, this message translates to:
/// **'The server is currently unavailable. Please try again later.'**
String get recoverErrorTryAgainLater;
/// No description provided for @recoverErrorUnknown.
///
/// In en, this message translates to:
/// **'An unknown error occurred. Please try again.'**
String get recoverErrorUnknown;
/// No description provided for @recoverSuccessTitle.
///
/// In en, this message translates to:
/// **'Backup successfully recovered.'**
String get recoverSuccessTitle;
/// No description provided for @recoverSuccessBody.
///
/// In en, this message translates to:
/// **'Click here to open the app again'**
String get recoverSuccessBody;
/// No description provided for @iosRecoveryWelcomeBack.
///
/// In en, this message translates to:
/// **'Welcome Back'**
String get iosRecoveryWelcomeBack;
/// No description provided for @iosRecoveryPrompt.
///
/// In en, this message translates to:
/// **'We detected a previously secured twonly identity on this device. Would you like to automatically download and restore your contacts, messages, and settings from your cloud archive?'**
String get iosRecoveryPrompt;
/// No description provided for @iosRecoveryNoBackupFound.
///
/// In en, this message translates to:
/// **'No backup archive could be retrieved from the server for this device.\n\nError: {error}\n\nPlease proceed to register a new twonly account.'**
String iosRecoveryNoBackupFound(Object error);
/// No description provided for @registerNewAccount.
///
/// In en, this message translates to:
/// **'Register New Account'**
String get registerNewAccount;
/// No description provided for @tryRestoreAgain.
///
/// In en, this message translates to:
/// **'Try Restore Again'**
String get tryRestoreAgain;
/// No description provided for @registeringNewAccount.
///
/// In en, this message translates to:
/// **'Registering new account'**
String get registeringNewAccount;
/// No description provided for @createShortcut.
///
/// In en, this message translates to:
/// **'Create shortcut'**
String get createShortcut;
/// No description provided for @editShortcut.
///
/// In en, this message translates to:
/// **'Edit shortcut'**
String get editShortcut;
/// No description provided for @deleteShortcut.
///
/// In en, this message translates to:
/// **'Delete shortcut'**
String get deleteShortcut;
/// No description provided for @deleteShortcutBody.
///
/// In en, this message translates to:
/// **'Are you sure you want to delete this shortcut?'**
String get deleteShortcutBody;
/// No description provided for @updateShortcut.
///
/// In en, this message translates to:
/// **'Update shortcut'**
String get updateShortcut;
/// No description provided for @selectEmoji.
///
/// In en, this message translates to:
/// **'Select Emoji'**
String get selectEmoji;
/// No description provided for @errorEmojiUsedOrInvalid.
///
/// In en, this message translates to:
/// **'Emoji already used or invalid'**
String get errorEmojiUsedOrInvalid;
} }
class _AppLocalizationsDelegate class _AppLocalizationsDelegate

View file

@ -277,6 +277,13 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get settingsNotification => 'Benachrichtigung'; String get settingsNotification => 'Benachrichtigung';
@override
String get settingsNotifyPermission => 'Benachrichtigungsberechtigung';
@override
String get settingsNotifyPermissionDesc =>
'Systemeinstellungen öffnen, um Push-Benachrichtigungen zu erlauben.';
@override @override
String get settingsNotifyTroubleshooting => 'Fehlersuche'; String get settingsNotifyTroubleshooting => 'Fehlersuche';
@ -658,12 +665,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get open => 'Offene'; String get open => 'Offene';
@override
String get createVoucher => 'Gutschein kaufen';
@override
String get redeemVoucher => 'Gutschein einlösen';
@override @override
String get buy => 'Kaufen'; String get buy => 'Kaufen';
@ -725,13 +726,10 @@ class AppLocalizationsDe extends AppLocalizations {
'Aufgrund des Sicherheitssystems von twonly gibt es (derzeit) keine Funktion zur Wiederherstellung des Passworts. Daher musst du dir dein Passwort merken oder, besser noch, aufschreiben.'; 'Aufgrund des Sicherheitssystems von twonly gibt es (derzeit) keine Funktion zur Wiederherstellung des Passworts. Daher musst du dir dein Passwort merken oder, besser noch, aufschreiben.';
@override @override
String get backupServer => 'Server'; String get backupIdentityHeader => 'Identität';
@override @override
String get backupMaxBackupSize => 'max. Backup-Größe'; String get backupArchiveHeader => 'Kontakte, Einstellungen und Nachrichten';
@override
String get backupStorageRetention => 'Speicheraufbewahrung';
@override @override
String get backupLastBackupDate => 'Letztes Backup'; String get backupLastBackupDate => 'Letztes Backup';
@ -742,9 +740,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get backupLastBackupResult => 'Ergebnis'; String get backupLastBackupResult => 'Ergebnis';
@override
String get backupData => 'Daten-Backup';
@override @override
String get backupInsecurePassword => 'Unsicheres Passwort'; String get backupInsecurePassword => 'Unsicheres Passwort';
@ -777,24 +772,11 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get backupPasswordRequirement => String get backupPasswordRequirement =>
'Das Passwort muss mindestens 8 Zeichen lang sein.'; 'Das Passwort muss mindestens 10 Zeichen lang sein.';
@override
String get backupExpertSettings => 'Experteneinstellungen';
@override @override
String get backupEnableBackup => 'Automatische Sicherung aktivieren'; String get backupEnableBackup => 'Automatische Sicherung aktivieren';
@override
String get backupOwnServerDesc =>
'Speichere dein twonly Backup auf einem Server deiner Wahl.';
@override
String get backupUseOwnServer => 'Server verwenden';
@override
String get backupResetServer => 'Standardserver verwenden';
@override @override
String get backupTwonlySaveNow => 'Jetzt speichern'; String get backupTwonlySaveNow => 'Jetzt speichern';
@ -1271,9 +1253,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get openYourOwnQRcode => 'Eigenen QR-Code öffnen'; String get openYourOwnQRcode => 'Eigenen QR-Code öffnen';
@override
String get skipForNow => 'Vorerst überspringen';
@override @override
String get finishSetupCardTitle => 'Profil vervollständigen'; String get finishSetupCardTitle => 'Profil vervollständigen';
@ -1284,6 +1263,16 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get finishSetupCardAction => 'Setup fortsetzen'; String get finishSetupCardAction => 'Setup fortsetzen';
@override
String get missingBackupCardTitle => 'Backup einrichten';
@override
String get missingBackupCardDesc =>
'Wir haben den Backup-Mechanismus verbessert, weshalb du ihn erneut einrichten musst.';
@override
String get missingBackupCardAction => 'Jetzt einrichten';
@override @override
String get onboardingFinishLater => 'Später abschließen'; String get onboardingFinishLater => 'Später abschließen';
@ -1714,11 +1703,81 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String makerChangedUsername(Object maker, Object oldName, Object newName) { String makerChangedUsername(Object maker, Object oldName, Object newName) {
return '$maker hat seinen Benutzernamen von $oldName zu $newName geändert.'; return '$maker hat den Benutzernamen von $oldName zu $newName geändert.';
} }
@override @override
String makerChangedDisplayName(Object maker, Object oldName, Object newName) { String makerChangedDisplayName(Object maker, Object oldName, Object newName) {
return '$maker hat seinen Anzeigenamen von $oldName zu $newName geändert.'; return '$maker hat den Anzeigenamen von $oldName zu $newName geändert.';
} }
@override
String get recoverErrorNoInternet =>
'Keine Internetverbindung. Bitte überprüfe deine Netzwerkverbindung und versuche es erneut.';
@override
String get recoverErrorUsernameNotValid =>
'Der eingegebene Benutzername ist ungültig oder existiert nicht.';
@override
String get recoverErrorPasswordInvalid =>
'Das eingegebene Passwort ist falsch.';
@override
String get recoverErrorTryAgainLater =>
'Der Server ist derzeit nicht erreichbar. Bitte versuche es später erneut.';
@override
String get recoverErrorUnknown =>
'Ein unbekannter Fehler ist aufgetreten. Bitte versuche es erneut.';
@override
String get recoverSuccessTitle => 'Backup erfolgreich wiederhergestellt.';
@override
String get recoverSuccessBody => 'Klicke hier, um die App wieder zu öffnen';
@override
String get iosRecoveryWelcomeBack => 'Willkommen zurück';
@override
String get iosRecoveryPrompt =>
'Wir haben eine zuvor gesicherte twonly-Identität auf diesem Gerät erkannt. Möchtest du deine Kontakte, Nachrichten und Einstellungen automatisch aus deinem Cloud-Archiv herunterladen und wiederherstellen?';
@override
String iosRecoveryNoBackupFound(Object error) {
return 'Für dieses Gerät konnte kein Backup-Archiv vom Server abgerufen werden.\n\nFehler: $error\n\nBitte fahre mit der Registrierung eines neuen twonly-Kontos fort.';
}
@override
String get registerNewAccount => 'Neues Konto registrieren';
@override
String get tryRestoreAgain => 'Wiederherstellung erneut versuchen';
@override
String get registeringNewAccount => 'Neues Konto wird registriert';
@override
String get createShortcut => 'Shortcut erstellen';
@override
String get editShortcut => 'Shortcut bearbeiten';
@override
String get deleteShortcut => 'Shortcut löschen';
@override
String get deleteShortcutBody =>
'Bist du sicher, dass du diesen Shortcut löschen möchtest?';
@override
String get updateShortcut => 'Shortcut aktualisieren';
@override
String get selectEmoji => 'Emoji auswählen';
@override
String get errorEmojiUsedOrInvalid =>
'Emoji wird bereits verwendet oder ist ungültig';
} }

View file

@ -273,6 +273,13 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get settingsNotification => 'Notification'; String get settingsNotification => 'Notification';
@override
String get settingsNotifyPermission => 'Notification permissions';
@override
String get settingsNotifyPermissionDesc =>
'Open system settings to allow push notifications.';
@override @override
String get settingsNotifyTroubleshooting => 'Troubleshooting'; String get settingsNotifyTroubleshooting => 'Troubleshooting';
@ -652,12 +659,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get open => 'Open'; String get open => 'Open';
@override
String get createVoucher => 'Buy voucher';
@override
String get redeemVoucher => 'Redeem voucher';
@override @override
String get buy => 'Buy'; String get buy => 'Buy';
@ -719,13 +720,10 @@ class AppLocalizationsEn extends AppLocalizations {
'Due to twonly\'s security system, there is (currently) no password recovery function. Therefore, you must remember your password or, better yet, write it down.'; 'Due to twonly\'s security system, there is (currently) no password recovery function. Therefore, you must remember your password or, better yet, write it down.';
@override @override
String get backupServer => 'Server'; String get backupIdentityHeader => 'Identity';
@override @override
String get backupMaxBackupSize => 'max. backup size'; String get backupArchiveHeader => 'Contacts, Settings and Messages';
@override
String get backupStorageRetention => 'Storage retention';
@override @override
String get backupLastBackupDate => 'Last backup'; String get backupLastBackupDate => 'Last backup';
@ -736,9 +734,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get backupLastBackupResult => 'Result'; String get backupLastBackupResult => 'Result';
@override
String get backupData => 'Data-Backup';
@override @override
String get backupInsecurePassword => 'Insecure password'; String get backupInsecurePassword => 'Insecure password';
@ -771,24 +766,11 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get backupPasswordRequirement => String get backupPasswordRequirement =>
'Password must be at least 8 characters long.'; 'Password must be at least 10 characters long.';
@override
String get backupExpertSettings => 'Expert settings';
@override @override
String get backupEnableBackup => 'Activate automatic backup'; String get backupEnableBackup => 'Activate automatic backup';
@override
String get backupOwnServerDesc =>
'Save your twonly Backup at twonly or on any server of your choice.';
@override
String get backupUseOwnServer => 'Use server';
@override
String get backupResetServer => 'Use standard server';
@override @override
String get backupTwonlySaveNow => 'Save now'; String get backupTwonlySaveNow => 'Save now';
@ -1262,9 +1244,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get openYourOwnQRcode => 'Open your own QR code'; String get openYourOwnQRcode => 'Open your own QR code';
@override
String get skipForNow => 'Skip for now';
@override @override
String get finishSetupCardTitle => 'Complete your profile'; String get finishSetupCardTitle => 'Complete your profile';
@ -1275,6 +1254,16 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get finishSetupCardAction => 'Resume Setup'; String get finishSetupCardAction => 'Resume Setup';
@override
String get missingBackupCardTitle => 'Setup backup';
@override
String get missingBackupCardDesc =>
'We have improved the backup mechanism, which requires you to set it up again.';
@override
String get missingBackupCardAction => 'Set up now';
@override @override
String get onboardingFinishLater => 'Finish later'; String get onboardingFinishLater => 'Finish later';
@ -1706,4 +1695,73 @@ class AppLocalizationsEn extends AppLocalizations {
String makerChangedDisplayName(Object maker, Object oldName, Object newName) { String makerChangedDisplayName(Object maker, Object oldName, Object newName) {
return '$maker changed their display name from $oldName to $newName.'; return '$maker changed their display name from $oldName to $newName.';
} }
@override
String get recoverErrorNoInternet =>
'No internet connection. Please check your network and try again.';
@override
String get recoverErrorUsernameNotValid =>
'The username provided is not valid or does not exist.';
@override
String get recoverErrorPasswordInvalid =>
'The password provided is incorrect.';
@override
String get recoverErrorTryAgainLater =>
'The server is currently unavailable. Please try again later.';
@override
String get recoverErrorUnknown =>
'An unknown error occurred. Please try again.';
@override
String get recoverSuccessTitle => 'Backup successfully recovered.';
@override
String get recoverSuccessBody => 'Click here to open the app again';
@override
String get iosRecoveryWelcomeBack => 'Welcome Back';
@override
String get iosRecoveryPrompt =>
'We detected a previously secured twonly identity on this device. Would you like to automatically download and restore your contacts, messages, and settings from your cloud archive?';
@override
String iosRecoveryNoBackupFound(Object error) {
return 'No backup archive could be retrieved from the server for this device.\n\nError: $error\n\nPlease proceed to register a new twonly account.';
}
@override
String get registerNewAccount => 'Register New Account';
@override
String get tryRestoreAgain => 'Try Restore Again';
@override
String get registeringNewAccount => 'Registering new account';
@override
String get createShortcut => 'Create shortcut';
@override
String get editShortcut => 'Edit shortcut';
@override
String get deleteShortcut => 'Delete shortcut';
@override
String get deleteShortcutBody =>
'Are you sure you want to delete this shortcut?';
@override
String get updateShortcut => 'Update shortcut';
@override
String get selectEmoji => 'Select Emoji';
@override
String get errorEmojiUsedOrInvalid => 'Emoji already used or invalid';
} }

@ -1 +1 @@
Subproject commit fccd366e119671b96730cb09d8bb8aa1057bd1c5 Subproject commit 9218abf0961c072edd2f8aa5035d06a331b853c6

View file

@ -0,0 +1,51 @@
import 'package:json_annotation/json_annotation.dart';
part 'backup.model.g.dart';
enum LastBackupUploadState { none, pending, failed, success }
@JsonSerializable()
class CurrentBackupStatus {
CurrentBackupStatus();
factory CurrentBackupStatus.fromJson(Map<String, dynamic> json) =>
_$CurrentBackupStatusFromJson(json);
LastBackupUploadState identityState = LastBackupUploadState.none;
DateTime? identityLastSuccessFull;
int? identitySize;
LastBackupUploadState archiveState = LastBackupUploadState.none;
DateTime? archiveLastSuccessFull;
int? archiveSize;
Map<String, dynamic> toJson() => _$CurrentBackupStatusToJson(this);
}
enum BackupRecoveryState {
// The userId was loaded from the server and the user is asked to enter his password.
identityBackupStarted,
// -> Download identity, replace keymanager
// Identity was downloaded and Keymanager was updated
archiveBackupStarted,
// -> Download archive, replace files, restart app
}
@JsonSerializable()
class BackupRecovery {
BackupRecovery({
required this.username,
required this.password,
required this.userId,
});
factory BackupRecovery.fromJson(Map<String, dynamic> json) =>
_$BackupRecoveryFromJson(json);
String username;
String password;
int userId;
BackupRecoveryState state = BackupRecoveryState.identityBackupStarted;
Map<String, dynamic> toJson() => _$BackupRecoveryToJson(this);
}

View file

@ -0,0 +1,65 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'backup.model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
CurrentBackupStatus _$CurrentBackupStatusFromJson(Map<String, dynamic> json) =>
CurrentBackupStatus()
..identityState = $enumDecode(
_$LastBackupUploadStateEnumMap,
json['identityState'],
)
..identityLastSuccessFull = json['identityLastSuccessFull'] == null
? null
: DateTime.parse(json['identityLastSuccessFull'] as String)
..identitySize = (json['identitySize'] as num?)?.toInt()
..archiveState = $enumDecode(
_$LastBackupUploadStateEnumMap,
json['archiveState'],
)
..archiveLastSuccessFull = json['archiveLastSuccessFull'] == null
? null
: DateTime.parse(json['archiveLastSuccessFull'] as String)
..archiveSize = (json['archiveSize'] as num?)?.toInt();
Map<String, dynamic> _$CurrentBackupStatusToJson(
CurrentBackupStatus instance,
) => <String, dynamic>{
'identityState': _$LastBackupUploadStateEnumMap[instance.identityState]!,
'identityLastSuccessFull': instance.identityLastSuccessFull
?.toIso8601String(),
'identitySize': instance.identitySize,
'archiveState': _$LastBackupUploadStateEnumMap[instance.archiveState]!,
'archiveLastSuccessFull': instance.archiveLastSuccessFull?.toIso8601String(),
'archiveSize': instance.archiveSize,
};
const _$LastBackupUploadStateEnumMap = {
LastBackupUploadState.none: 'none',
LastBackupUploadState.pending: 'pending',
LastBackupUploadState.failed: 'failed',
LastBackupUploadState.success: 'success',
};
BackupRecovery _$BackupRecoveryFromJson(Map<String, dynamic> json) =>
BackupRecovery(
username: json['username'] as String,
password: json['password'] as String,
userId: (json['userId'] as num).toInt(),
)..state = $enumDecode(_$BackupRecoveryStateEnumMap, json['state']);
Map<String, dynamic> _$BackupRecoveryToJson(BackupRecovery instance) =>
<String, dynamic>{
'username': instance.username,
'password': instance.password,
'userId': instance.userId,
'state': _$BackupRecoveryStateEnumMap[instance.state]!,
};
const _$BackupRecoveryStateEnumMap = {
BackupRecoveryState.identityBackupStarted: 'identityBackupStarted',
BackupRecoveryState.archiveBackupStarted: 'archiveBackupStarted',
};

View file

@ -128,12 +128,20 @@ class UserData {
@JsonKey(defaultValue: true) @JsonKey(defaultValue: true)
bool updateFCMToken = true; bool updateFCMToken = true;
@JsonKey(defaultValue: true)
bool canUseLoginTokenForAuth = true;
// --- BACKUP --- // --- BACKUP ---
DateTime? nextTimeToShowBackupNotice; @Deprecated('Use the secure storage in rust')
BackupServer? backupServer;
TwonlySafeBackup? twonlySafeBackup; TwonlySafeBackup? twonlySafeBackup;
@JsonKey(defaultValue: false)
bool isBackupEnabled = false;
// Used for push notifcation via FCM.
String? fcmToken;
// For my master thesis I want to create a anonymous user study: // For my master thesis I want to create a anonymous user study:
// - users in the "Tester" Plan can, if they want, take part of the user study // - users in the "Tester" Plan can, if they want, take part of the user study
@ -175,19 +183,3 @@ class TwonlySafeBackup {
List<int> encryptionKey; List<int> encryptionKey;
Map<String, dynamic> toJson() => _$TwonlySafeBackupToJson(this); Map<String, dynamic> toJson() => _$TwonlySafeBackupToJson(this);
} }
@JsonSerializable()
class BackupServer {
BackupServer({
required this.serverUrl,
required this.retentionDays,
required this.maxBackupBytes,
});
factory BackupServer.fromJson(Map<String, dynamic> json) =>
_$BackupServerFromJson(json);
String serverUrl;
int retentionDays;
int maxBackupBytes;
Map<String, dynamic> toJson() => _$BackupServerToJson(this);
}

View file

@ -71,6 +71,8 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
json['userDiscoveryRequiresManualApproval'] as bool? ?? false json['userDiscoveryRequiresManualApproval'] as bool? ?? false
..userDiscoverySharePromotion = ..userDiscoverySharePromotion =
json['userDiscoverySharePromotion'] as bool? ?? true json['userDiscoverySharePromotion'] as bool? ?? true
..userDiscoveryInitializationError =
json['userDiscoveryInitializationError'] as bool? ?? false
..currentPreKeyIndexStart = ..currentPreKeyIndexStart =
(json['currentPreKeyIndexStart'] as num?)?.toInt() ?? 100000 (json['currentPreKeyIndexStart'] as num?)?.toInt() ?? 100000
..currentSignedPreKeyIndexStart = ..currentSignedPreKeyIndexStart =
@ -80,17 +82,15 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
.toList() .toList()
..hideChangeLog = json['hideChangeLog'] as bool? ?? true ..hideChangeLog = json['hideChangeLog'] as bool? ?? true
..updateFCMToken = json['updateFCMToken'] as bool? ?? true ..updateFCMToken = json['updateFCMToken'] as bool? ?? true
..nextTimeToShowBackupNotice = json['nextTimeToShowBackupNotice'] == null ..canUseLoginTokenForAuth =
? null json['canUseLoginTokenForAuth'] as bool? ?? true
: DateTime.parse(json['nextTimeToShowBackupNotice'] as String)
..backupServer = json['backupServer'] == null
? null
: BackupServer.fromJson(json['backupServer'] as Map<String, dynamic>)
..twonlySafeBackup = json['twonlySafeBackup'] == null ..twonlySafeBackup = json['twonlySafeBackup'] == null
? null ? null
: TwonlySafeBackup.fromJson( : TwonlySafeBackup.fromJson(
json['twonlySafeBackup'] as Map<String, dynamic>, json['twonlySafeBackup'] as Map<String, dynamic>,
) )
..isBackupEnabled = json['isBackupEnabled'] as bool? ?? false
..fcmToken = json['fcmToken'] as String?
..askedForUserStudyPermission = ..askedForUserStudyPermission =
json['askedForUserStudyPermission'] as bool? ?? false json['askedForUserStudyPermission'] as bool? ?? false
..userStudyParticipantsToken = ..userStudyParticipantsToken =
@ -142,15 +142,16 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'userDiscoveryRequiresManualApproval': 'userDiscoveryRequiresManualApproval':
instance.userDiscoveryRequiresManualApproval, instance.userDiscoveryRequiresManualApproval,
'userDiscoverySharePromotion': instance.userDiscoverySharePromotion, 'userDiscoverySharePromotion': instance.userDiscoverySharePromotion,
'userDiscoveryInitializationError': instance.userDiscoveryInitializationError,
'currentPreKeyIndexStart': instance.currentPreKeyIndexStart, 'currentPreKeyIndexStart': instance.currentPreKeyIndexStart,
'currentSignedPreKeyIndexStart': instance.currentSignedPreKeyIndexStart, 'currentSignedPreKeyIndexStart': instance.currentSignedPreKeyIndexStart,
'lastChangeLogHash': instance.lastChangeLogHash, 'lastChangeLogHash': instance.lastChangeLogHash,
'hideChangeLog': instance.hideChangeLog, 'hideChangeLog': instance.hideChangeLog,
'updateFCMToken': instance.updateFCMToken, 'updateFCMToken': instance.updateFCMToken,
'nextTimeToShowBackupNotice': instance.nextTimeToShowBackupNotice 'canUseLoginTokenForAuth': instance.canUseLoginTokenForAuth,
?.toIso8601String(),
'backupServer': instance.backupServer,
'twonlySafeBackup': instance.twonlySafeBackup, 'twonlySafeBackup': instance.twonlySafeBackup,
'isBackupEnabled': instance.isBackupEnabled,
'fcmToken': instance.fcmToken,
'askedForUserStudyPermission': instance.askedForUserStudyPermission, 'askedForUserStudyPermission': instance.askedForUserStudyPermission,
'userStudyParticipantsToken': instance.userStudyParticipantsToken, 'userStudyParticipantsToken': instance.userStudyParticipantsToken,
'userStudyCountNewFriendsViaSuggestion': 'userStudyCountNewFriendsViaSuggestion':
@ -201,16 +202,3 @@ const _$LastBackupUploadStateEnumMap = {
LastBackupUploadState.failed: 'failed', LastBackupUploadState.failed: 'failed',
LastBackupUploadState.success: 'success', LastBackupUploadState.success: 'success',
}; };
BackupServer _$BackupServerFromJson(Map<String, dynamic> json) => BackupServer(
serverUrl: json['serverUrl'] as String,
retentionDays: (json['retentionDays'] as num).toInt(),
maxBackupBytes: (json['maxBackupBytes'] as num).toInt(),
);
Map<String, dynamic> _$BackupServerToJson(BackupServer instance) =>
<String, dynamic>{
'serverUrl': instance.serverUrl,
'retentionDays': instance.retentionDays,
'maxBackupBytes': instance.maxBackupBytes,
};

File diff suppressed because it is too large Load diff

View file

@ -134,13 +134,33 @@ const Handshake$json = {
'9': 0, '9': 0,
'10': 'requestPOW' '10': 'requestPOW'
}, },
{
'1': 'authenticate_with_login_token',
'3': 6,
'4': 1,
'5': 11,
'6': '.client_to_server.Handshake.AuthenticateWithLoginToken',
'9': 0,
'10': 'authenticateWithLoginToken'
},
{
'1': 'get_userid_by_username',
'3': 7,
'4': 1,
'5': 11,
'6': '.client_to_server.Handshake.GetUserIdByUsername',
'9': 0,
'10': 'getUseridByUsername'
},
], ],
'3': [ '3': [
Handshake_RequestPOW$json, Handshake_RequestPOW$json,
Handshake_Register$json, Handshake_Register$json,
Handshake_GetAuthChallenge$json, Handshake_GetAuthChallenge$json,
Handshake_GetUserIdByUsername$json,
Handshake_GetAuthToken$json, Handshake_GetAuthToken$json,
Handshake_Authenticate$json Handshake_Authenticate$json,
Handshake_AuthenticateWithLoginToken$json
], ],
'8': [ '8': [
{'1': 'Handshake'}, {'1': 'Handshake'},
@ -186,9 +206,19 @@ const Handshake_Register$json = {
{'1': 'is_ios', '3': 8, '4': 1, '5': 8, '10': 'isIos'}, {'1': 'is_ios', '3': 8, '4': 1, '5': 8, '10': 'isIos'},
{'1': 'lang_code', '3': 9, '4': 1, '5': 9, '10': 'langCode'}, {'1': 'lang_code', '3': 9, '4': 1, '5': 9, '10': 'langCode'},
{'1': 'proof_of_work', '3': 10, '4': 1, '5': 3, '10': 'proofOfWork'}, {'1': 'proof_of_work', '3': 10, '4': 1, '5': 3, '10': 'proofOfWork'},
{
'1': 'login_token',
'3': 11,
'4': 1,
'5': 12,
'9': 1,
'10': 'loginToken',
'17': true
},
], ],
'8': [ '8': [
{'1': '_invite_code'}, {'1': '_invite_code'},
{'1': '_login_token'},
], ],
}; };
@ -197,6 +227,14 @@ const Handshake_GetAuthChallenge$json = {
'1': 'GetAuthChallenge', '1': 'GetAuthChallenge',
}; };
@$core.Deprecated('Use handshakeDescriptor instead')
const Handshake_GetUserIdByUsername$json = {
'1': 'GetUserIdByUsername',
'2': [
{'1': 'username', '3': 1, '4': 1, '5': 9, '10': 'username'},
],
};
@$core.Deprecated('Use handshakeDescriptor instead') @$core.Deprecated('Use handshakeDescriptor instead')
const Handshake_GetAuthToken$json = { const Handshake_GetAuthToken$json = {
'1': 'GetAuthToken', '1': 'GetAuthToken',
@ -247,6 +285,24 @@ const Handshake_Authenticate$json = {
], ],
}; };
@$core.Deprecated('Use handshakeDescriptor instead')
const Handshake_AuthenticateWithLoginToken$json = {
'1': 'AuthenticateWithLoginToken',
'2': [
{'1': 'user_id', '3': 1, '4': 1, '5': 3, '10': 'userId'},
{
'1': 'secret_login_token',
'3': 2,
'4': 1,
'5': 12,
'10': 'secretLoginToken'
},
{'1': 'app_version', '3': 3, '4': 1, '5': 9, '10': 'appVersion'},
{'1': 'device_id', '3': 4, '4': 1, '5': 3, '10': 'deviceId'},
{'1': 'in_background', '3': 5, '4': 1, '5': 8, '10': 'inBackground'},
],
};
/// Descriptor for `Handshake`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `Handshake`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List handshakeDescriptor = $convert.base64Decode( final $typed_data.Uint8List handshakeDescriptor = $convert.base64Decode(
'CglIYW5kc2hha2USQgoIcmVnaXN0ZXIYASABKAsyJC5jbGllbnRfdG9fc2VydmVyLkhhbmRzaG' 'CglIYW5kc2hha2USQgoIcmVnaXN0ZXIYASABKAsyJC5jbGllbnRfdG9fc2VydmVyLkhhbmRzaG'
@ -256,20 +312,30 @@ final $typed_data.Uint8List handshakeDescriptor = $convert.base64Decode(
'LkdldEF1dGhUb2tlbkgAUgxnZXRBdXRoVG9rZW4STgoMYXV0aGVudGljYXRlGAQgASgLMiguY2' 'LkdldEF1dGhUb2tlbkgAUgxnZXRBdXRoVG9rZW4STgoMYXV0aGVudGljYXRlGAQgASgLMiguY2'
'xpZW50X3RvX3NlcnZlci5IYW5kc2hha2UuQXV0aGVudGljYXRlSABSDGF1dGhlbnRpY2F0ZRJI' 'xpZW50X3RvX3NlcnZlci5IYW5kc2hha2UuQXV0aGVudGljYXRlSABSDGF1dGhlbnRpY2F0ZRJI'
'CgpyZXF1ZXN0UE9XGAUgASgLMiYuY2xpZW50X3RvX3NlcnZlci5IYW5kc2hha2UuUmVxdWVzdF' 'CgpyZXF1ZXN0UE9XGAUgASgLMiYuY2xpZW50X3RvX3NlcnZlci5IYW5kc2hha2UuUmVxdWVzdF'
'BPV0gAUgpyZXF1ZXN0UE9XGgwKClJlcXVlc3RQT1calAMKCFJlZ2lzdGVyEhoKCHVzZXJuYW1l' 'BPV0gAUgpyZXF1ZXN0UE9XEnsKHWF1dGhlbnRpY2F0ZV93aXRoX2xvZ2luX3Rva2VuGAYgASgL'
'GAEgASgJUgh1c2VybmFtZRIkCgtpbnZpdGVfY29kZRgCIAEoCUgAUgppbnZpdGVDb2RliAEBEi' 'MjYuY2xpZW50X3RvX3NlcnZlci5IYW5kc2hha2UuQXV0aGVudGljYXRlV2l0aExvZ2luVG9rZW'
'4KE3B1YmxpY19pZGVudGl0eV9rZXkYAyABKAxSEXB1YmxpY0lkZW50aXR5S2V5EiMKDXNpZ25l' '5IAFIaYXV0aGVudGljYXRlV2l0aExvZ2luVG9rZW4SZgoWZ2V0X3VzZXJpZF9ieV91c2VybmFt'
'ZF9wcmVrZXkYBCABKAxSDHNpZ25lZFByZWtleRI2ChdzaWduZWRfcHJla2V5X3NpZ25hdHVyZR' 'ZRgHIAEoCzIvLmNsaWVudF90b19zZXJ2ZXIuSGFuZHNoYWtlLkdldFVzZXJJZEJ5VXNlcm5hbW'
'gFIAEoDFIVc2lnbmVkUHJla2V5U2lnbmF0dXJlEigKEHNpZ25lZF9wcmVrZXlfaWQYBiABKANS' 'VIAFITZ2V0VXNlcmlkQnlVc2VybmFtZRoMCgpSZXF1ZXN0UE9XGsoDCghSZWdpc3RlchIaCgh1'
'DnNpZ25lZFByZWtleUlkEicKD3JlZ2lzdHJhdGlvbl9pZBgHIAEoA1IOcmVnaXN0cmF0aW9uSW' 'c2VybmFtZRgBIAEoCVIIdXNlcm5hbWUSJAoLaW52aXRlX2NvZGUYAiABKAlIAFIKaW52aXRlQ2'
'QSFQoGaXNfaW9zGAggASgIUgVpc0lvcxIbCglsYW5nX2NvZGUYCSABKAlSCGxhbmdDb2RlEiIK' '9kZYgBARIuChNwdWJsaWNfaWRlbnRpdHlfa2V5GAMgASgMUhFwdWJsaWNJZGVudGl0eUtleRIj'
'DXByb29mX29mX3dvcmsYCiABKANSC3Byb29mT2ZXb3JrQg4KDF9pbnZpdGVfY29kZRoSChBHZX' 'Cg1zaWduZWRfcHJla2V5GAQgASgMUgxzaWduZWRQcmVrZXkSNgoXc2lnbmVkX3ByZWtleV9zaW'
'RBdXRoQ2hhbGxlbmdlGkMKDEdldEF1dGhUb2tlbhIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQS' 'duYXR1cmUYBSABKAxSFXNpZ25lZFByZWtleVNpZ25hdHVyZRIoChBzaWduZWRfcHJla2V5X2lk'
'GgoIcmVzcG9uc2UYAiABKAxSCHJlc3BvbnNlGugBCgxBdXRoZW50aWNhdGUSFwoHdXNlcl9pZB' 'GAYgASgDUg5zaWduZWRQcmVrZXlJZBInCg9yZWdpc3RyYXRpb25faWQYByABKANSDnJlZ2lzdH'
'gBIAEoA1IGdXNlcklkEh0KCmF1dGhfdG9rZW4YAiABKAxSCWF1dGhUb2tlbhIkCgthcHBfdmVy' 'JhdGlvbklkEhUKBmlzX2lvcxgIIAEoCFIFaXNJb3MSGwoJbGFuZ19jb2RlGAkgASgJUghsYW5n'
'c2lvbhgDIAEoCUgAUgphcHBWZXJzaW9uiAEBEiAKCWRldmljZV9pZBgEIAEoA0gBUghkZXZpY2' 'Q29kZRIiCg1wcm9vZl9vZl93b3JrGAogASgDUgtwcm9vZk9mV29yaxIkCgtsb2dpbl90b2tlbh'
'VJZIgBARIoCg1pbl9iYWNrZ3JvdW5kGAUgASgISAJSDGluQmFja2dyb3VuZIgBAUIOCgxfYXBw' 'gLIAEoDEgBUgpsb2dpblRva2VuiAEBQg4KDF9pbnZpdGVfY29kZUIOCgxfbG9naW5fdG9rZW4a'
'X3ZlcnNpb25CDAoKX2RldmljZV9pZEIQCg5faW5fYmFja2dyb3VuZEILCglIYW5kc2hha2U='); 'EgoQR2V0QXV0aENoYWxsZW5nZRoxChNHZXRVc2VySWRCeVVzZXJuYW1lEhoKCHVzZXJuYW1lGA'
'EgASgJUgh1c2VybmFtZRpDCgxHZXRBdXRoVG9rZW4SFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklk'
'EhoKCHJlc3BvbnNlGAIgASgMUghyZXNwb25zZRroAQoMQXV0aGVudGljYXRlEhcKB3VzZXJfaW'
'QYASABKANSBnVzZXJJZBIdCgphdXRoX3Rva2VuGAIgASgMUglhdXRoVG9rZW4SJAoLYXBwX3Zl'
'cnNpb24YAyABKAlIAFIKYXBwVmVyc2lvbogBARIgCglkZXZpY2VfaWQYBCABKANIAVIIZGV2aW'
'NlSWSIAQESKAoNaW5fYmFja2dyb3VuZBgFIAEoCEgCUgxpbkJhY2tncm91bmSIAQFCDgoMX2Fw'
'cF92ZXJzaW9uQgwKCl9kZXZpY2VfaWRCEAoOX2luX2JhY2tncm91bmQaxgEKGkF1dGhlbnRpY2'
'F0ZVdpdGhMb2dpblRva2VuEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBIsChJzZWNyZXRfbG9n'
'aW5fdG9rZW4YAiABKAxSEHNlY3JldExvZ2luVG9rZW4SHwoLYXBwX3ZlcnNpb24YAyABKAlSCm'
'FwcFZlcnNpb24SGwoJZGV2aWNlX2lkGAQgASgDUghkZXZpY2VJZBIjCg1pbl9iYWNrZ3JvdW5k'
'GAUgASgIUgxpbkJhY2tncm91bmRCCwoJSGFuZHNoYWtl');
@$core.Deprecated('Use applicationDataDescriptor instead') @$core.Deprecated('Use applicationDataDescriptor instead')
const ApplicationData$json = { const ApplicationData$json = {
@ -321,13 +387,13 @@ const ApplicationData$json = {
'10': 'updateGoogleFcmToken' '10': 'updateGoogleFcmToken'
}, },
{ {
'1': 'getLocation', '1': 'deprecated_9',
'3': 9, '3': 9,
'4': 1, '4': 1,
'5': 11, '5': 11,
'6': '.client_to_server.ApplicationData.GetLocation', '6': '.client_to_server.ApplicationData.Deprecated',
'9': 0, '9': 0,
'10': 'getLocation' '10': 'deprecated9'
}, },
{ {
'1': 'getCurrentPlanInfos', '1': 'getCurrentPlanInfos',
@ -339,13 +405,13 @@ const ApplicationData$json = {
'10': 'getCurrentPlanInfos' '10': 'getCurrentPlanInfos'
}, },
{ {
'1': 'redeemVoucher', '1': 'deprecated_11',
'3': 11, '3': 11,
'4': 1, '4': 1,
'5': 11, '5': 11,
'6': '.client_to_server.ApplicationData.RedeemVoucher', '6': '.client_to_server.ApplicationData.Deprecated',
'9': 0, '9': 0,
'10': 'redeemVoucher' '10': 'deprecated11'
}, },
{ {
'1': 'getAvailablePlans', '1': 'getAvailablePlans',
@ -357,58 +423,58 @@ const ApplicationData$json = {
'10': 'getAvailablePlans' '10': 'getAvailablePlans'
}, },
{ {
'1': 'createVoucher', '1': 'deprecated_13',
'3': 13, '3': 13,
'4': 1, '4': 1,
'5': 11, '5': 11,
'6': '.client_to_server.ApplicationData.CreateVoucher', '6': '.client_to_server.ApplicationData.Deprecated',
'9': 0, '9': 0,
'10': 'createVoucher' '10': 'deprecated13'
}, },
{ {
'1': 'getVouchers', '1': 'deprecated_14',
'3': 14, '3': 14,
'4': 1, '4': 1,
'5': 11, '5': 11,
'6': '.client_to_server.ApplicationData.GetVouchers', '6': '.client_to_server.ApplicationData.Deprecated',
'9': 0, '9': 0,
'10': 'getVouchers' '10': 'deprecated14'
}, },
{ {
'1': 'switchtoPayedPlan', '1': 'deprecated_15',
'3': 15, '3': 15,
'4': 1, '4': 1,
'5': 11, '5': 11,
'6': '.client_to_server.ApplicationData.SwitchToPayedPlan', '6': '.client_to_server.ApplicationData.Deprecated',
'9': 0, '9': 0,
'10': 'switchtoPayedPlan' '10': 'deprecated15'
}, },
{ {
'1': 'getAddaccountsInvites', '1': 'deprecated_16',
'3': 16, '3': 16,
'4': 1, '4': 1,
'5': 11, '5': 11,
'6': '.client_to_server.ApplicationData.GetAddAccountsInvites', '6': '.client_to_server.ApplicationData.Deprecated',
'9': 0, '9': 0,
'10': 'getAddaccountsInvites' '10': 'deprecated16'
}, },
{ {
'1': 'redeemAdditionalCode', '1': 'deprecated_17',
'3': 17, '3': 17,
'4': 1, '4': 1,
'5': 11, '5': 11,
'6': '.client_to_server.ApplicationData.RedeemAdditionalCode', '6': '.client_to_server.ApplicationData.Deprecated',
'9': 0, '9': 0,
'10': 'redeemAdditionalCode' '10': 'deprecated17'
}, },
{ {
'1': 'updatePlanOptions', '1': 'deprecated_19',
'3': 19, '3': 19,
'4': 1, '4': 1,
'5': 11, '5': 11,
'6': '.client_to_server.ApplicationData.UpdatePlanOptions', '6': '.client_to_server.ApplicationData.Deprecated',
'9': 0, '9': 0,
'10': 'updatePlanOptions' '10': 'deprecated19'
}, },
{ {
'1': 'downloadDone', '1': 'downloadDone',
@ -500,6 +566,15 @@ const ApplicationData$json = {
'9': 0, '9': 0,
'10': 'addAdditionalUser' '10': 'addAdditionalUser'
}, },
{
'1': 'set_login_token',
'3': 30,
'4': 1,
'5': 11,
'6': '.client_to_server.ApplicationData.SetLoginToken',
'9': 0,
'10': 'setLoginToken'
},
], ],
'3': [ '3': [
ApplicationData_TextMessage$json, ApplicationData_TextMessage$json,
@ -507,16 +582,9 @@ const ApplicationData$json = {
ApplicationData_ChangeUsername$json, ApplicationData_ChangeUsername$json,
ApplicationData_UpdateGoogleFcmToken$json, ApplicationData_UpdateGoogleFcmToken$json,
ApplicationData_GetUserById$json, ApplicationData_GetUserById$json,
ApplicationData_RedeemVoucher$json,
ApplicationData_SwitchToPayedPlan$json,
ApplicationData_UpdatePlanOptions$json,
ApplicationData_CreateVoucher$json,
ApplicationData_GetLocation$json,
ApplicationData_GetVouchers$json,
ApplicationData_GetAvailablePlans$json, ApplicationData_GetAvailablePlans$json,
ApplicationData_GetAddAccountsInvites$json, ApplicationData_GetAddAccountsInvites$json,
ApplicationData_GetCurrentPlanInfos$json, ApplicationData_GetCurrentPlanInfos$json,
ApplicationData_RedeemAdditionalCode$json,
ApplicationData_RemoveAdditionalUser$json, ApplicationData_RemoveAdditionalUser$json,
ApplicationData_GetPrekeysByUserId$json, ApplicationData_GetPrekeysByUserId$json,
ApplicationData_GetSignedPreKeyByUserId$json, ApplicationData_GetSignedPreKeyByUserId$json,
@ -526,7 +594,9 @@ const ApplicationData$json = {
ApplicationData_IPAPurchase$json, ApplicationData_IPAPurchase$json,
ApplicationData_IPAForceCheck$json, ApplicationData_IPAForceCheck$json,
ApplicationData_DeleteAccount$json, ApplicationData_DeleteAccount$json,
ApplicationData_AddAdditionalUser$json ApplicationData_AddAdditionalUser$json,
ApplicationData_SetLoginToken$json,
ApplicationData_Deprecated$json
], ],
'8': [ '8': [
{'1': 'ApplicationData'}, {'1': 'ApplicationData'},
@ -586,50 +656,6 @@ const ApplicationData_GetUserById$json = {
], ],
}; };
@$core.Deprecated('Use applicationDataDescriptor instead')
const ApplicationData_RedeemVoucher$json = {
'1': 'RedeemVoucher',
'2': [
{'1': 'voucher', '3': 1, '4': 1, '5': 9, '10': 'voucher'},
],
};
@$core.Deprecated('Use applicationDataDescriptor instead')
const ApplicationData_SwitchToPayedPlan$json = {
'1': 'SwitchToPayedPlan',
'2': [
{'1': 'plan_id', '3': 1, '4': 1, '5': 9, '10': 'planId'},
{'1': 'pay_monthly', '3': 2, '4': 1, '5': 8, '10': 'payMonthly'},
{'1': 'auto_renewal', '3': 3, '4': 1, '5': 8, '10': 'autoRenewal'},
],
};
@$core.Deprecated('Use applicationDataDescriptor instead')
const ApplicationData_UpdatePlanOptions$json = {
'1': 'UpdatePlanOptions',
'2': [
{'1': 'auto_renewal', '3': 1, '4': 1, '5': 8, '10': 'autoRenewal'},
],
};
@$core.Deprecated('Use applicationDataDescriptor instead')
const ApplicationData_CreateVoucher$json = {
'1': 'CreateVoucher',
'2': [
{'1': 'value_cents', '3': 1, '4': 1, '5': 13, '10': 'valueCents'},
],
};
@$core.Deprecated('Use applicationDataDescriptor instead')
const ApplicationData_GetLocation$json = {
'1': 'GetLocation',
};
@$core.Deprecated('Use applicationDataDescriptor instead')
const ApplicationData_GetVouchers$json = {
'1': 'GetVouchers',
};
@$core.Deprecated('Use applicationDataDescriptor instead') @$core.Deprecated('Use applicationDataDescriptor instead')
const ApplicationData_GetAvailablePlans$json = { const ApplicationData_GetAvailablePlans$json = {
'1': 'GetAvailablePlans', '1': 'GetAvailablePlans',
@ -645,14 +671,6 @@ const ApplicationData_GetCurrentPlanInfos$json = {
'1': 'GetCurrentPlanInfos', '1': 'GetCurrentPlanInfos',
}; };
@$core.Deprecated('Use applicationDataDescriptor instead')
const ApplicationData_RedeemAdditionalCode$json = {
'1': 'RedeemAdditionalCode',
'2': [
{'1': 'invite_code', '3': 2, '4': 1, '5': 9, '10': 'inviteCode'},
],
};
@$core.Deprecated('Use applicationDataDescriptor instead') @$core.Deprecated('Use applicationDataDescriptor instead')
const ApplicationData_RemoveAdditionalUser$json = { const ApplicationData_RemoveAdditionalUser$json = {
'1': 'RemoveAdditionalUser', '1': 'RemoveAdditionalUser',
@ -744,6 +762,19 @@ const ApplicationData_AddAdditionalUser$json = {
], ],
}; };
@$core.Deprecated('Use applicationDataDescriptor instead')
const ApplicationData_SetLoginToken$json = {
'1': 'SetLoginToken',
'2': [
{'1': 'login_token', '3': 1, '4': 1, '5': 12, '10': 'loginToken'},
],
};
@$core.Deprecated('Use applicationDataDescriptor instead')
const ApplicationData_Deprecated$json = {
'1': 'Deprecated',
};
/// Descriptor for `ApplicationData`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `ApplicationData`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List applicationDataDescriptor = $convert.base64Decode( final $typed_data.Uint8List applicationDataDescriptor = $convert.base64Decode(
'Cg9BcHBsaWNhdGlvbkRhdGESUQoLdGV4dE1lc3NhZ2UYASABKAsyLS5jbGllbnRfdG9fc2Vydm' 'Cg9BcHBsaWNhdGlvbkRhdGESUQoLdGV4dE1lc3NhZ2UYASABKAsyLS5jbGllbnRfdG9fc2Vydm'
@ -755,66 +786,61 @@ final $typed_data.Uint8List applicationDataDescriptor = $convert.base64Decode(
'bnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5HZXRVc2VyQnlJZEgAUgtnZXRVc2VyQnlJZB' 'bnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5HZXRVc2VyQnlJZEgAUgtnZXRVc2VyQnlJZB'
'JsChR1cGRhdGVHb29nbGVGY21Ub2tlbhgIIAEoCzI2LmNsaWVudF90b19zZXJ2ZXIuQXBwbGlj' 'JsChR1cGRhdGVHb29nbGVGY21Ub2tlbhgIIAEoCzI2LmNsaWVudF90b19zZXJ2ZXIuQXBwbGlj'
'YXRpb25EYXRhLlVwZGF0ZUdvb2dsZUZjbVRva2VuSABSFHVwZGF0ZUdvb2dsZUZjbVRva2VuEl' 'YXRpb25EYXRhLlVwZGF0ZUdvb2dsZUZjbVRva2VuSABSFHVwZGF0ZUdvb2dsZUZjbVRva2VuEl'
'EKC2dldExvY2F0aW9uGAkgASgLMi0uY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEu' 'EKDGRlcHJlY2F0ZWRfORgJIAEoCzIsLmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRh'
'R2V0TG9jYXRpb25IAFILZ2V0TG9jYXRpb24SaQoTZ2V0Q3VycmVudFBsYW5JbmZvcxgKIAEoCz' 'LkRlcHJlY2F0ZWRIAFILZGVwcmVjYXRlZDkSaQoTZ2V0Q3VycmVudFBsYW5JbmZvcxgKIAEoCz'
'I1LmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkdldEN1cnJlbnRQbGFuSW5mb3NI' 'I1LmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkdldEN1cnJlbnRQbGFuSW5mb3NI'
'AFITZ2V0Q3VycmVudFBsYW5JbmZvcxJXCg1yZWRlZW1Wb3VjaGVyGAsgASgLMi8uY2xpZW50X3' 'AFITZ2V0Q3VycmVudFBsYW5JbmZvcxJTCg1kZXByZWNhdGVkXzExGAsgASgLMiwuY2xpZW50X3'
'RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuUmVkZWVtVm91Y2hlckgAUg1yZWRlZW1Wb3VjaGVy' 'RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuRGVwcmVjYXRlZEgAUgxkZXByZWNhdGVkMTESYwoR'
'EmMKEWdldEF2YWlsYWJsZVBsYW5zGAwgASgLMjMuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdG' 'Z2V0QXZhaWxhYmxlUGxhbnMYDCABKAsyMy5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRG'
'lvbkRhdGEuR2V0QXZhaWxhYmxlUGxhbnNIAFIRZ2V0QXZhaWxhYmxlUGxhbnMSVwoNY3JlYXRl' 'F0YS5HZXRBdmFpbGFibGVQbGFuc0gAUhFnZXRBdmFpbGFibGVQbGFucxJTCg1kZXByZWNhdGVk'
'Vm91Y2hlchgNIAEoCzIvLmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkNyZWF0ZV' 'XzEzGA0gASgLMiwuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuRGVwcmVjYXRlZE'
'ZvdWNoZXJIAFINY3JlYXRlVm91Y2hlchJRCgtnZXRWb3VjaGVycxgOIAEoCzItLmNsaWVudF90' 'gAUgxkZXByZWNhdGVkMTMSUwoNZGVwcmVjYXRlZF8xNBgOIAEoCzIsLmNsaWVudF90b19zZXJ2'
'b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkdldFZvdWNoZXJzSABSC2dldFZvdWNoZXJzEmMKEX' 'ZXIuQXBwbGljYXRpb25EYXRhLkRlcHJlY2F0ZWRIAFIMZGVwcmVjYXRlZDE0ElMKDWRlcHJlY2'
'N3aXRjaHRvUGF5ZWRQbGFuGA8gASgLMjMuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRh' 'F0ZWRfMTUYDyABKAsyLC5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5EZXByZWNh'
'dGEuU3dpdGNoVG9QYXllZFBsYW5IAFIRc3dpdGNodG9QYXllZFBsYW4SbwoVZ2V0QWRkYWNjb3' 'dGVkSABSDGRlcHJlY2F0ZWQxNRJTCg1kZXByZWNhdGVkXzE2GBAgASgLMiwuY2xpZW50X3RvX3'
'VudHNJbnZpdGVzGBAgASgLMjcuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuR2V0' 'NlcnZlci5BcHBsaWNhdGlvbkRhdGEuRGVwcmVjYXRlZEgAUgxkZXByZWNhdGVkMTYSUwoNZGVw'
'QWRkQWNjb3VudHNJbnZpdGVzSABSFWdldEFkZGFjY291bnRzSW52aXRlcxJsChRyZWRlZW1BZG' 'cmVjYXRlZF8xNxgRIAEoCzIsLmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkRlcH'
'RpdGlvbmFsQ29kZRgRIAEoCzI2LmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLlJl' 'JlY2F0ZWRIAFIMZGVwcmVjYXRlZDE3ElMKDWRlcHJlY2F0ZWRfMTkYEyABKAsyLC5jbGllbnRf'
'ZGVlbUFkZGl0aW9uYWxDb2RlSABSFHJlZGVlbUFkZGl0aW9uYWxDb2RlEmMKEXVwZGF0ZVBsYW' 'dG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5EZXByZWNhdGVkSABSDGRlcHJlY2F0ZWQxORJUCg'
'5PcHRpb25zGBMgASgLMjMuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuVXBkYXRl' 'xkb3dubG9hZERvbmUYFCABKAsyLi5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5E'
'UGxhbk9wdGlvbnNIAFIRdXBkYXRlUGxhbk9wdGlvbnMSVAoMZG93bmxvYWREb25lGBQgASgLMi' 'b3dubG9hZERvbmVIAFIMZG93bmxvYWREb25lEnUKF2dldFNpZ25lZFByZWtleUJ5VXNlcmlkGB'
'4uY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuRG93bmxvYWREb25lSABSDGRvd25s' 'YgASgLMjkuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuR2V0U2lnbmVkUHJlS2V5'
'b2FkRG9uZRJ1ChdnZXRTaWduZWRQcmVrZXlCeVVzZXJpZBgWIAEoCzI5LmNsaWVudF90b19zZX' 'QnlVc2VySWRIAFIXZ2V0U2lnbmVkUHJla2V5QnlVc2VyaWQSZgoSdXBkYXRlU2lnbmVkUHJla2'
'J2ZXIuQXBwbGljYXRpb25EYXRhLkdldFNpZ25lZFByZUtleUJ5VXNlcklkSABSF2dldFNpZ25l' 'V5GBcgASgLMjQuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuVXBkYXRlU2lnbmVk'
'ZFByZWtleUJ5VXNlcmlkEmYKEnVwZGF0ZVNpZ25lZFByZWtleRgXIAEoCzI0LmNsaWVudF90b1' 'UHJlS2V5SABSEnVwZGF0ZVNpZ25lZFByZWtleRJXCg1kZWxldGVBY2NvdW50GBggASgLMi8uY2'
'9zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLlVwZGF0ZVNpZ25lZFByZUtleUgAUhJ1cGRhdGVTaWdu' 'xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuRGVsZXRlQWNjb3VudEgAUg1kZWxldGVB'
'ZWRQcmVrZXkSVwoNZGVsZXRlQWNjb3VudBgYIAEoCzIvLmNsaWVudF90b19zZXJ2ZXIuQXBwbG' 'Y2NvdW50Ek4KCnJlcG9ydFVzZXIYGSABKAsyLC5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW'
'ljYXRpb25EYXRhLkRlbGV0ZUFjY291bnRIAFINZGVsZXRlQWNjb3VudBJOCgpyZXBvcnRVc2Vy' '9uRGF0YS5SZXBvcnRVc2VySABSCnJlcG9ydFVzZXISWgoOY2hhbmdlVXNlcm5hbWUYGiABKAsy'
'GBkgASgLMiwuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuUmVwb3J0VXNlckgAUg' 'MC5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5DaGFuZ2VVc2VybmFtZUgAUg5jaG'
'pyZXBvcnRVc2VyEloKDmNoYW5nZVVzZXJuYW1lGBogASgLMjAuY2xpZW50X3RvX3NlcnZlci5B' 'FuZ2VVc2VybmFtZRJRCgtpcGFQdXJjaGFzZRgbIAEoCzItLmNsaWVudF90b19zZXJ2ZXIuQXBw'
'cHBsaWNhdGlvbkRhdGEuQ2hhbmdlVXNlcm5hbWVIAFIOY2hhbmdlVXNlcm5hbWUSUQoLaXBhUH' 'bGljYXRpb25EYXRhLklQQVB1cmNoYXNlSABSC2lwYVB1cmNoYXNlElcKDWlwYUZvcmNlQ2hlY2'
'VyY2hhc2UYGyABKAsyLS5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5JUEFQdXJj' 'sYHCABKAsyLy5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5JUEFGb3JjZUNoZWNr'
'aGFzZUgAUgtpcGFQdXJjaGFzZRJXCg1pcGFGb3JjZUNoZWNrGBwgASgLMi8uY2xpZW50X3RvX3' 'SABSDWlwYUZvcmNlQ2hlY2sSbAoUcmVtb3ZlQWRkaXRpb25hbFVzZXIYEiABKAsyNi5jbGllbn'
'NlcnZlci5BcHBsaWNhdGlvbkRhdGEuSVBBRm9yY2VDaGVja0gAUg1pcGFGb3JjZUNoZWNrEmwK' 'RfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5SZW1vdmVBZGRpdGlvbmFsVXNlckgAUhRyZW1v'
'FHJlbW92ZUFkZGl0aW9uYWxVc2VyGBIgASgLMjYuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdG' 'dmVBZGRpdGlvbmFsVXNlchJjChFhZGRBZGRpdGlvbmFsVXNlchgdIAEoCzIzLmNsaWVudF90b1'
'lvbkRhdGEuUmVtb3ZlQWRkaXRpb25hbFVzZXJIAFIUcmVtb3ZlQWRkaXRpb25hbFVzZXISYwoR' '9zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkFkZEFkZGl0aW9uYWxVc2VySABSEWFkZEFkZGl0aW9u'
'YWRkQWRkaXRpb25hbFVzZXIYHSABKAsyMy5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRG' 'YWxVc2VyElkKD3NldF9sb2dpbl90b2tlbhgeIAEoCzIvLmNsaWVudF90b19zZXJ2ZXIuQXBwbG'
'F0YS5BZGRBZGRpdGlvbmFsVXNlckgAUhFhZGRBZGRpdGlvbmFsVXNlchpqCgtUZXh0TWVzc2Fn' 'ljYXRpb25EYXRhLlNldExvZ2luVG9rZW5IAFINc2V0TG9naW5Ub2tlbhpqCgtUZXh0TWVzc2Fn'
'ZRIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQSEgoEYm9keRgDIAEoDFIEYm9keRIgCglwdXNoX2' 'ZRIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQSEgoEYm9keRgDIAEoDFIEYm9keRIgCglwdXNoX2'
'RhdGEYBCABKAxIAFIIcHVzaERhdGGIAQFCDAoKX3B1c2hfZGF0YRovChFHZXRVc2VyQnlVc2Vy' 'RhdGEYBCABKAxIAFIIcHVzaERhdGGIAQFCDAoKX3B1c2hfZGF0YRovChFHZXRVc2VyQnlVc2Vy'
'bmFtZRIaCgh1c2VybmFtZRgBIAEoCVIIdXNlcm5hbWUaLAoOQ2hhbmdlVXNlcm5hbWUSGgoIdX' 'bmFtZRIaCgh1c2VybmFtZRgBIAEoCVIIdXNlcm5hbWUaLAoOQ2hhbmdlVXNlcm5hbWUSGgoIdX'
'Nlcm5hbWUYASABKAlSCHVzZXJuYW1lGjUKFFVwZGF0ZUdvb2dsZUZjbVRva2VuEh0KCmdvb2ds' 'Nlcm5hbWUYASABKAlSCHVzZXJuYW1lGjUKFFVwZGF0ZUdvb2dsZUZjbVRva2VuEh0KCmdvb2ds'
'ZV9mY20YASABKAlSCWdvb2dsZUZjbRomCgtHZXRVc2VyQnlJZBIXCgd1c2VyX2lkGAEgASgDUg' 'ZV9mY20YASABKAlSCWdvb2dsZUZjbRomCgtHZXRVc2VyQnlJZBIXCgd1c2VyX2lkGAEgASgDUg'
'Z1c2VySWQaKQoNUmVkZWVtVm91Y2hlchIYCgd2b3VjaGVyGAEgASgJUgd2b3VjaGVyGnAKEVN3' 'Z1c2VySWQaEwoRR2V0QXZhaWxhYmxlUGxhbnMaFwoVR2V0QWRkQWNjb3VudHNJbnZpdGVzGhUK'
'aXRjaFRvUGF5ZWRQbGFuEhcKB3BsYW5faWQYASABKAlSBnBsYW5JZBIfCgtwYXlfbW9udGhseR' 'E0dldEN1cnJlbnRQbGFuSW5mb3MaLwoUUmVtb3ZlQWRkaXRpb25hbFVzZXISFwoHdXNlcl9pZB'
'gCIAEoCFIKcGF5TW9udGhseRIhCgxhdXRvX3JlbmV3YWwYAyABKAhSC2F1dG9SZW5ld2FsGjYK' 'gBIAEoA1IGdXNlcklkGi0KEkdldFByZWtleXNCeVVzZXJJZBIXCgd1c2VyX2lkGAEgASgDUgZ1'
'EVVwZGF0ZVBsYW5PcHRpb25zEiEKDGF1dG9fcmVuZXdhbBgBIAEoCFILYXV0b1JlbmV3YWwaMA' 'c2VySWQaMgoXR2V0U2lnbmVkUHJlS2V5QnlVc2VySWQSFwoHdXNlcl9pZBgBIAEoA1IGdXNlck'
'oNQ3JlYXRlVm91Y2hlchIfCgt2YWx1ZV9jZW50cxgBIAEoDVIKdmFsdWVDZW50cxoNCgtHZXRM' 'lkGpsBChJVcGRhdGVTaWduZWRQcmVLZXkSKAoQc2lnbmVkX3ByZWtleV9pZBgBIAEoA1IOc2ln'
'b2NhdGlvbhoNCgtHZXRWb3VjaGVycxoTChFHZXRBdmFpbGFibGVQbGFucxoXChVHZXRBZGRBY2' 'bmVkUHJla2V5SWQSIwoNc2lnbmVkX3ByZWtleRgCIAEoDFIMc2lnbmVkUHJla2V5EjYKF3NpZ2'
'NvdW50c0ludml0ZXMaFQoTR2V0Q3VycmVudFBsYW5JbmZvcxo3ChRSZWRlZW1BZGRpdGlvbmFs' '5lZF9wcmVrZXlfc2lnbmF0dXJlGAMgASgMUhVzaWduZWRQcmVrZXlTaWduYXR1cmUaNQoMRG93'
'Q29kZRIfCgtpbnZpdGVfY29kZRgCIAEoCVIKaW52aXRlQ29kZRovChRSZW1vdmVBZGRpdGlvbm' 'bmxvYWREb25lEiUKDmRvd25sb2FkX3Rva2VuGAEgASgMUg1kb3dubG9hZFRva2VuGk4KClJlcG'
'FsVXNlchIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQaLQoSR2V0UHJla2V5c0J5VXNlcklkEhcK' '9ydFVzZXISKAoQcmVwb3J0ZWRfdXNlcl9pZBgBIAEoA1IOcmVwb3J0ZWRVc2VySWQSFgoGcmVh'
'B3VzZXJfaWQYASABKANSBnVzZXJJZBoyChdHZXRTaWduZWRQcmVLZXlCeVVzZXJJZBIXCgd1c2' 'c29uGAIgASgJUgZyZWFzb24acQoLSVBBUHVyY2hhc2USHQoKcHJvZHVjdF9pZBgBIAEoCVIJcH'
'VyX2lkGAEgASgDUgZ1c2VySWQamwEKElVwZGF0ZVNpZ25lZFByZUtleRIoChBzaWduZWRfcHJl' 'JvZHVjdElkEhYKBnNvdXJjZRgCIAEoCVIGc291cmNlEisKEXZlcmlmaWNhdGlvbl9kYXRhGAMg'
'a2V5X2lkGAEgASgDUg5zaWduZWRQcmVrZXlJZBIjCg1zaWduZWRfcHJla2V5GAIgASgMUgxzaW' 'ASgJUhB2ZXJpZmljYXRpb25EYXRhGg8KDUlQQUZvcmNlQ2hlY2saDwoNRGVsZXRlQWNjb3VudB'
'duZWRQcmVrZXkSNgoXc2lnbmVkX3ByZWtleV9zaWduYXR1cmUYAyABKAxSFXNpZ25lZFByZWtl' 'osChFBZGRBZGRpdGlvbmFsVXNlchIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQaMAoNU2V0TG9n'
'eVNpZ25hdHVyZRo1CgxEb3dubG9hZERvbmUSJQoOZG93bmxvYWRfdG9rZW4YASABKAxSDWRvd2' 'aW5Ub2tlbhIfCgtsb2dpbl90b2tlbhgBIAEoDFIKbG9naW5Ub2tlbhoMCgpEZXByZWNhdGVkQh'
'5sb2FkVG9rZW4aTgoKUmVwb3J0VXNlchIoChByZXBvcnRlZF91c2VyX2lkGAEgASgDUg5yZXBv' 'EKD0FwcGxpY2F0aW9uRGF0YQ==');
'cnRlZFVzZXJJZBIWCgZyZWFzb24YAiABKAlSBnJlYXNvbhpxCgtJUEFQdXJjaGFzZRIdCgpwcm'
'9kdWN0X2lkGAEgASgJUglwcm9kdWN0SWQSFgoGc291cmNlGAIgASgJUgZzb3VyY2USKwoRdmVy'
'aWZpY2F0aW9uX2RhdGEYAyABKAlSEHZlcmlmaWNhdGlvbkRhdGEaDwoNSVBBRm9yY2VDaGVjax'
'oPCg1EZWxldGVBY2NvdW50GiwKEUFkZEFkZGl0aW9uYWxVc2VyEhcKB3VzZXJfaWQYASABKANS'
'BnVzZXJJZEIRCg9BcHBsaWNhdGlvbkRhdGE=');
@$core.Deprecated('Use responseDescriptor instead') @$core.Deprecated('Use responseDescriptor instead')
const Response$json = { const Response$json = {

View file

@ -16,12 +16,9 @@ import 'package:fixnum/fixnum.dart' as $fixnum;
import 'package:protobuf/protobuf.dart' as $pb; import 'package:protobuf/protobuf.dart' as $pb;
import 'error.pbenum.dart' as $0; import 'error.pbenum.dart' as $0;
import 'server_to_client.pbenum.dart';
export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions; export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions;
export 'server_to_client.pbenum.dart';
enum ServerToClient_V { v0, notSet } enum ServerToClient_V { v0, notSet }
class ServerToClient extends $pb.GeneratedMessage { class ServerToClient extends $pb.GeneratedMessage {
@ -752,90 +749,6 @@ class Response_AddAccountsInvites extends $pb.GeneratedMessage {
$pb.PbList<Response_AddAccountsInvite> get invites => $_getList(0); $pb.PbList<Response_AddAccountsInvite> get invites => $_getList(0);
} }
class Response_Transaction extends $pb.GeneratedMessage {
factory Response_Transaction({
$fixnum.Int64? depositCents,
Response_TransactionTypes? transactionType,
$fixnum.Int64? createdAtUnixTimestamp,
}) {
final result = create();
if (depositCents != null) result.depositCents = depositCents;
if (transactionType != null) result.transactionType = transactionType;
if (createdAtUnixTimestamp != null)
result.createdAtUnixTimestamp = createdAtUnixTimestamp;
return result;
}
Response_Transaction._();
factory Response_Transaction.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory Response_Transaction.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'Response.Transaction',
package:
const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'),
createEmptyInstance: create)
..aInt64(1, _omitFieldNames ? '' : 'depositCents')
..aE<Response_TransactionTypes>(2, _omitFieldNames ? '' : 'transactionType',
enumValues: Response_TransactionTypes.values)
..aInt64(3, _omitFieldNames ? '' : 'createdAtUnixTimestamp')
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
Response_Transaction clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
Response_Transaction copyWith(void Function(Response_Transaction) updates) =>
super.copyWith((message) => updates(message as Response_Transaction))
as Response_Transaction;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static Response_Transaction create() => Response_Transaction._();
@$core.override
Response_Transaction createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static Response_Transaction getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<Response_Transaction>(create);
static Response_Transaction? _defaultInstance;
@$pb.TagNumber(1)
$fixnum.Int64 get depositCents => $_getI64(0);
@$pb.TagNumber(1)
set depositCents($fixnum.Int64 value) => $_setInt64(0, value);
@$pb.TagNumber(1)
$core.bool hasDepositCents() => $_has(0);
@$pb.TagNumber(1)
void clearDepositCents() => $_clearField(1);
@$pb.TagNumber(2)
Response_TransactionTypes get transactionType => $_getN(1);
@$pb.TagNumber(2)
set transactionType(Response_TransactionTypes value) => $_setField(2, value);
@$pb.TagNumber(2)
$core.bool hasTransactionType() => $_has(1);
@$pb.TagNumber(2)
void clearTransactionType() => $_clearField(2);
/// Represents seconds of UTC time since Unix epoch
/// 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to
/// 9999-12-31T23:59:59Z inclusive.
@$pb.TagNumber(3)
$fixnum.Int64 get createdAtUnixTimestamp => $_getI64(2);
@$pb.TagNumber(3)
set createdAtUnixTimestamp($fixnum.Int64 value) => $_setInt64(2, value);
@$pb.TagNumber(3)
$core.bool hasCreatedAtUnixTimestamp() => $_has(2);
@$pb.TagNumber(3)
void clearCreatedAtUnixTimestamp() => $_clearField(3);
}
class Response_AdditionalAccount extends $pb.GeneratedMessage { class Response_AdditionalAccount extends $pb.GeneratedMessage {
factory Response_AdditionalAccount({ factory Response_AdditionalAccount({
$fixnum.Int64? userId, $fixnum.Int64? userId,
@ -905,158 +818,82 @@ class Response_AdditionalAccount extends $pb.GeneratedMessage {
void clearPlanId() => $_clearField(3); void clearPlanId() => $_clearField(3);
} }
class Response_Voucher extends $pb.GeneratedMessage { class Response_Deprecated extends $pb.GeneratedMessage {
factory Response_Voucher({ factory Response_Deprecated() => create();
$core.String? voucherId,
$fixnum.Int64? valueCents,
$core.bool? redeemed,
$core.bool? requested,
$fixnum.Int64? createdAtUnixTimestamp,
}) {
final result = create();
if (voucherId != null) result.voucherId = voucherId;
if (valueCents != null) result.valueCents = valueCents;
if (redeemed != null) result.redeemed = redeemed;
if (requested != null) result.requested = requested;
if (createdAtUnixTimestamp != null)
result.createdAtUnixTimestamp = createdAtUnixTimestamp;
return result;
}
Response_Voucher._(); Response_Deprecated._();
factory Response_Voucher.fromBuffer($core.List<$core.int> data, factory Response_Deprecated.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry); create()..mergeFromBuffer(data, registry);
factory Response_Voucher.fromJson($core.String json, factory Response_Deprecated.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry); create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo( static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'Response.Voucher', _omitMessageNames ? '' : 'Response.Deprecated',
package: package:
const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'), const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'),
createEmptyInstance: create) createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'voucherId')
..aInt64(2, _omitFieldNames ? '' : 'valueCents')
..aOB(3, _omitFieldNames ? '' : 'redeemed')
..aOB(4, _omitFieldNames ? '' : 'requested')
..aInt64(5, _omitFieldNames ? '' : 'createdAtUnixTimestamp')
..hasRequiredFields = false; ..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
Response_Voucher clone() => deepCopy(); Response_Deprecated clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
Response_Voucher copyWith(void Function(Response_Voucher) updates) => Response_Deprecated copyWith(void Function(Response_Deprecated) updates) =>
super.copyWith((message) => updates(message as Response_Voucher)) super.copyWith((message) => updates(message as Response_Deprecated))
as Response_Voucher; as Response_Deprecated;
@$core.override @$core.override
$pb.BuilderInfo get info_ => _i; $pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline') @$core.pragma('dart2js:noInline')
static Response_Voucher create() => Response_Voucher._(); static Response_Deprecated create() => Response_Deprecated._();
@$core.override @$core.override
Response_Voucher createEmptyInstance() => create(); Response_Deprecated createEmptyInstance() => create();
@$core.pragma('dart2js:noInline') @$core.pragma('dart2js:noInline')
static Response_Voucher getDefault() => _defaultInstance ??= static Response_Deprecated getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<Response_Voucher>(create); $pb.GeneratedMessage.$_defaultFor<Response_Deprecated>(create);
static Response_Voucher? _defaultInstance; static Response_Deprecated? _defaultInstance;
@$pb.TagNumber(1)
$core.String get voucherId => $_getSZ(0);
@$pb.TagNumber(1)
set voucherId($core.String value) => $_setString(0, value);
@$pb.TagNumber(1)
$core.bool hasVoucherId() => $_has(0);
@$pb.TagNumber(1)
void clearVoucherId() => $_clearField(1);
@$pb.TagNumber(2)
$fixnum.Int64 get valueCents => $_getI64(1);
@$pb.TagNumber(2)
set valueCents($fixnum.Int64 value) => $_setInt64(1, value);
@$pb.TagNumber(2)
$core.bool hasValueCents() => $_has(1);
@$pb.TagNumber(2)
void clearValueCents() => $_clearField(2);
@$pb.TagNumber(3)
$core.bool get redeemed => $_getBF(2);
@$pb.TagNumber(3)
set redeemed($core.bool value) => $_setBool(2, value);
@$pb.TagNumber(3)
$core.bool hasRedeemed() => $_has(2);
@$pb.TagNumber(3)
void clearRedeemed() => $_clearField(3);
@$pb.TagNumber(4)
$core.bool get requested => $_getBF(3);
@$pb.TagNumber(4)
set requested($core.bool value) => $_setBool(3, value);
@$pb.TagNumber(4)
$core.bool hasRequested() => $_has(3);
@$pb.TagNumber(4)
void clearRequested() => $_clearField(4);
@$pb.TagNumber(5)
$fixnum.Int64 get createdAtUnixTimestamp => $_getI64(4);
@$pb.TagNumber(5)
set createdAtUnixTimestamp($fixnum.Int64 value) => $_setInt64(4, value);
@$pb.TagNumber(5)
$core.bool hasCreatedAtUnixTimestamp() => $_has(4);
@$pb.TagNumber(5)
void clearCreatedAtUnixTimestamp() => $_clearField(5);
} }
class Response_Vouchers extends $pb.GeneratedMessage { class Response_Transaction extends $pb.GeneratedMessage {
factory Response_Vouchers({ factory Response_Transaction() => create();
$core.Iterable<Response_Voucher>? vouchers,
}) {
final result = create();
if (vouchers != null) result.vouchers.addAll(vouchers);
return result;
}
Response_Vouchers._(); Response_Transaction._();
factory Response_Vouchers.fromBuffer($core.List<$core.int> data, factory Response_Transaction.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry); create()..mergeFromBuffer(data, registry);
factory Response_Vouchers.fromJson($core.String json, factory Response_Transaction.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry); create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo( static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'Response.Vouchers', _omitMessageNames ? '' : 'Response.Transaction',
package: package:
const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'), const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'),
createEmptyInstance: create) createEmptyInstance: create)
..pPM<Response_Voucher>(1, _omitFieldNames ? '' : 'vouchers',
subBuilder: Response_Voucher.create)
..hasRequiredFields = false; ..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
Response_Vouchers clone() => deepCopy(); Response_Transaction clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
Response_Vouchers copyWith(void Function(Response_Vouchers) updates) => Response_Transaction copyWith(void Function(Response_Transaction) updates) =>
super.copyWith((message) => updates(message as Response_Vouchers)) super.copyWith((message) => updates(message as Response_Transaction))
as Response_Vouchers; as Response_Transaction;
@$core.override @$core.override
$pb.BuilderInfo get info_ => _i; $pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline') @$core.pragma('dart2js:noInline')
static Response_Vouchers create() => Response_Vouchers._(); static Response_Transaction create() => Response_Transaction._();
@$core.override @$core.override
Response_Vouchers createEmptyInstance() => create(); Response_Transaction createEmptyInstance() => create();
@$core.pragma('dart2js:noInline') @$core.pragma('dart2js:noInline')
static Response_Vouchers getDefault() => _defaultInstance ??= static Response_Transaction getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<Response_Vouchers>(create); $pb.GeneratedMessage.$_defaultFor<Response_Transaction>(create);
static Response_Vouchers? _defaultInstance; static Response_Transaction? _defaultInstance;
@$pb.TagNumber(1)
$pb.PbList<Response_Voucher> get vouchers => $_getList(0);
} }
class Response_PlanBallance extends $pb.GeneratedMessage { class Response_PlanBallance extends $pb.GeneratedMessage {
@ -1195,85 +1032,6 @@ class Response_PlanBallance extends $pb.GeneratedMessage {
void clearAdditionalAccountOwnerId() => $_clearField(8); void clearAdditionalAccountOwnerId() => $_clearField(8);
} }
class Response_Location extends $pb.GeneratedMessage {
factory Response_Location({
$core.String? county,
$core.String? region,
$core.String? city,
}) {
final result = create();
if (county != null) result.county = county;
if (region != null) result.region = region;
if (city != null) result.city = city;
return result;
}
Response_Location._();
factory Response_Location.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory Response_Location.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'Response.Location',
package:
const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'),
createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'county')
..aOS(2, _omitFieldNames ? '' : 'region')
..aOS(3, _omitFieldNames ? '' : 'city')
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
Response_Location clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
Response_Location copyWith(void Function(Response_Location) updates) =>
super.copyWith((message) => updates(message as Response_Location))
as Response_Location;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static Response_Location create() => Response_Location._();
@$core.override
Response_Location createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static Response_Location getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<Response_Location>(create);
static Response_Location? _defaultInstance;
@$pb.TagNumber(1)
$core.String get county => $_getSZ(0);
@$pb.TagNumber(1)
set county($core.String value) => $_setString(0, value);
@$pb.TagNumber(1)
$core.bool hasCounty() => $_has(0);
@$pb.TagNumber(1)
void clearCounty() => $_clearField(1);
@$pb.TagNumber(2)
$core.String get region => $_getSZ(1);
@$pb.TagNumber(2)
set region($core.String value) => $_setString(1, value);
@$pb.TagNumber(2)
$core.bool hasRegion() => $_has(1);
@$pb.TagNumber(2)
void clearRegion() => $_clearField(2);
@$pb.TagNumber(3)
$core.String get city => $_getSZ(2);
@$pb.TagNumber(3)
set city($core.String value) => $_setString(2, value);
@$pb.TagNumber(3)
$core.bool hasCity() => $_has(2);
@$pb.TagNumber(3)
void clearCity() => $_clearField(3);
}
class Response_PreKey extends $pb.GeneratedMessage { class Response_PreKey extends $pb.GeneratedMessage {
factory Response_PreKey({ factory Response_PreKey({
$fixnum.Int64? id, $fixnum.Int64? id,
@ -1754,11 +1512,11 @@ enum Response_Ok_Ok {
uploadtoken, uploadtoken,
userdata, userdata,
authtoken, authtoken,
location, deprecated7,
authenticated, authenticated,
plans, plans,
planballance, planballance,
vouchers, deprecated11,
addaccountsinvites, addaccountsinvites,
downloadtokens, downloadtokens,
signedprekey, signedprekey,
@ -1774,11 +1532,11 @@ class Response_Ok extends $pb.GeneratedMessage {
Response_UploadToken? uploadtoken, Response_UploadToken? uploadtoken,
Response_UserData? userdata, Response_UserData? userdata,
$core.List<$core.int>? authtoken, $core.List<$core.int>? authtoken,
Response_Location? location, Response_Deprecated? deprecated7,
Response_Authenticated? authenticated, Response_Authenticated? authenticated,
Response_Plans? plans, Response_Plans? plans,
Response_PlanBallance? planballance, Response_PlanBallance? planballance,
Response_Vouchers? vouchers, Response_Deprecated? deprecated11,
Response_AddAccountsInvites? addaccountsinvites, Response_AddAccountsInvites? addaccountsinvites,
Response_DownloadTokens? downloadtokens, Response_DownloadTokens? downloadtokens,
Response_SignedPreKey? signedprekey, Response_SignedPreKey? signedprekey,
@ -1791,11 +1549,11 @@ class Response_Ok extends $pb.GeneratedMessage {
if (uploadtoken != null) result.uploadtoken = uploadtoken; if (uploadtoken != null) result.uploadtoken = uploadtoken;
if (userdata != null) result.userdata = userdata; if (userdata != null) result.userdata = userdata;
if (authtoken != null) result.authtoken = authtoken; if (authtoken != null) result.authtoken = authtoken;
if (location != null) result.location = location; if (deprecated7 != null) result.deprecated7 = deprecated7;
if (authenticated != null) result.authenticated = authenticated; if (authenticated != null) result.authenticated = authenticated;
if (plans != null) result.plans = plans; if (plans != null) result.plans = plans;
if (planballance != null) result.planballance = planballance; if (planballance != null) result.planballance = planballance;
if (vouchers != null) result.vouchers = vouchers; if (deprecated11 != null) result.deprecated11 = deprecated11;
if (addaccountsinvites != null) if (addaccountsinvites != null)
result.addaccountsinvites = addaccountsinvites; result.addaccountsinvites = addaccountsinvites;
if (downloadtokens != null) result.downloadtokens = downloadtokens; if (downloadtokens != null) result.downloadtokens = downloadtokens;
@ -1820,11 +1578,11 @@ class Response_Ok extends $pb.GeneratedMessage {
4: Response_Ok_Ok.uploadtoken, 4: Response_Ok_Ok.uploadtoken,
5: Response_Ok_Ok.userdata, 5: Response_Ok_Ok.userdata,
6: Response_Ok_Ok.authtoken, 6: Response_Ok_Ok.authtoken,
7: Response_Ok_Ok.location, 7: Response_Ok_Ok.deprecated7,
8: Response_Ok_Ok.authenticated, 8: Response_Ok_Ok.authenticated,
9: Response_Ok_Ok.plans, 9: Response_Ok_Ok.plans,
10: Response_Ok_Ok.planballance, 10: Response_Ok_Ok.planballance,
11: Response_Ok_Ok.vouchers, 11: Response_Ok_Ok.deprecated11,
12: Response_Ok_Ok.addaccountsinvites, 12: Response_Ok_Ok.addaccountsinvites,
13: Response_Ok_Ok.downloadtokens, 13: Response_Ok_Ok.downloadtokens,
14: Response_Ok_Ok.signedprekey, 14: Response_Ok_Ok.signedprekey,
@ -1847,16 +1605,16 @@ class Response_Ok extends $pb.GeneratedMessage {
subBuilder: Response_UserData.create) subBuilder: Response_UserData.create)
..a<$core.List<$core.int>>( ..a<$core.List<$core.int>>(
6, _omitFieldNames ? '' : 'authtoken', $pb.PbFieldType.OY) 6, _omitFieldNames ? '' : 'authtoken', $pb.PbFieldType.OY)
..aOM<Response_Location>(7, _omitFieldNames ? '' : 'location', ..aOM<Response_Deprecated>(7, _omitFieldNames ? '' : 'deprecated7',
subBuilder: Response_Location.create) protoName: 'deprecated_7', subBuilder: Response_Deprecated.create)
..aOM<Response_Authenticated>(8, _omitFieldNames ? '' : 'authenticated', ..aOM<Response_Authenticated>(8, _omitFieldNames ? '' : 'authenticated',
subBuilder: Response_Authenticated.create) subBuilder: Response_Authenticated.create)
..aOM<Response_Plans>(9, _omitFieldNames ? '' : 'plans', ..aOM<Response_Plans>(9, _omitFieldNames ? '' : 'plans',
subBuilder: Response_Plans.create) subBuilder: Response_Plans.create)
..aOM<Response_PlanBallance>(10, _omitFieldNames ? '' : 'planballance', ..aOM<Response_PlanBallance>(10, _omitFieldNames ? '' : 'planballance',
subBuilder: Response_PlanBallance.create) subBuilder: Response_PlanBallance.create)
..aOM<Response_Vouchers>(11, _omitFieldNames ? '' : 'vouchers', ..aOM<Response_Deprecated>(11, _omitFieldNames ? '' : 'deprecated11',
subBuilder: Response_Vouchers.create) protoName: 'deprecated_11', subBuilder: Response_Deprecated.create)
..aOM<Response_AddAccountsInvites>( ..aOM<Response_AddAccountsInvites>(
12, _omitFieldNames ? '' : 'addaccountsinvites', 12, _omitFieldNames ? '' : 'addaccountsinvites',
subBuilder: Response_AddAccountsInvites.create) subBuilder: Response_AddAccountsInvites.create)
@ -1979,15 +1737,15 @@ class Response_Ok extends $pb.GeneratedMessage {
void clearAuthtoken() => $_clearField(6); void clearAuthtoken() => $_clearField(6);
@$pb.TagNumber(7) @$pb.TagNumber(7)
Response_Location get location => $_getN(6); Response_Deprecated get deprecated7 => $_getN(6);
@$pb.TagNumber(7) @$pb.TagNumber(7)
set location(Response_Location value) => $_setField(7, value); set deprecated7(Response_Deprecated value) => $_setField(7, value);
@$pb.TagNumber(7) @$pb.TagNumber(7)
$core.bool hasLocation() => $_has(6); $core.bool hasDeprecated7() => $_has(6);
@$pb.TagNumber(7) @$pb.TagNumber(7)
void clearLocation() => $_clearField(7); void clearDeprecated7() => $_clearField(7);
@$pb.TagNumber(7) @$pb.TagNumber(7)
Response_Location ensureLocation() => $_ensure(6); Response_Deprecated ensureDeprecated7() => $_ensure(6);
@$pb.TagNumber(8) @$pb.TagNumber(8)
Response_Authenticated get authenticated => $_getN(7); Response_Authenticated get authenticated => $_getN(7);
@ -2023,15 +1781,15 @@ class Response_Ok extends $pb.GeneratedMessage {
Response_PlanBallance ensurePlanballance() => $_ensure(9); Response_PlanBallance ensurePlanballance() => $_ensure(9);
@$pb.TagNumber(11) @$pb.TagNumber(11)
Response_Vouchers get vouchers => $_getN(10); Response_Deprecated get deprecated11 => $_getN(10);
@$pb.TagNumber(11) @$pb.TagNumber(11)
set vouchers(Response_Vouchers value) => $_setField(11, value); set deprecated11(Response_Deprecated value) => $_setField(11, value);
@$pb.TagNumber(11) @$pb.TagNumber(11)
$core.bool hasVouchers() => $_has(10); $core.bool hasDeprecated11() => $_has(10);
@$pb.TagNumber(11) @$pb.TagNumber(11)
void clearVouchers() => $_clearField(11); void clearDeprecated11() => $_clearField(11);
@$pb.TagNumber(11) @$pb.TagNumber(11)
Response_Vouchers ensureVouchers() => $_ensure(10); Response_Deprecated ensureDeprecated11() => $_ensure(10);
@$pb.TagNumber(12) @$pb.TagNumber(12)
Response_AddAccountsInvites get addaccountsinvites => $_getN(11); Response_AddAccountsInvites get addaccountsinvites => $_getN(11);

View file

@ -9,48 +9,3 @@
// ignore_for_file: curly_braces_in_flow_control_structures // ignore_for_file: curly_braces_in_flow_control_structures
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes // ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_relative_imports // ignore_for_file: non_constant_identifier_names, prefer_relative_imports
import 'dart:core' as $core;
import 'package:protobuf/protobuf.dart' as $pb;
class Response_TransactionTypes extends $pb.ProtobufEnum {
static const Response_TransactionTypes Refund =
Response_TransactionTypes._(0, _omitEnumNames ? '' : 'Refund');
static const Response_TransactionTypes VoucherRedeemed =
Response_TransactionTypes._(1, _omitEnumNames ? '' : 'VoucherRedeemed');
static const Response_TransactionTypes VoucherCreated =
Response_TransactionTypes._(2, _omitEnumNames ? '' : 'VoucherCreated');
static const Response_TransactionTypes Cash =
Response_TransactionTypes._(3, _omitEnumNames ? '' : 'Cash');
static const Response_TransactionTypes PlanUpgrade =
Response_TransactionTypes._(4, _omitEnumNames ? '' : 'PlanUpgrade');
static const Response_TransactionTypes Unknown =
Response_TransactionTypes._(5, _omitEnumNames ? '' : 'Unknown');
static const Response_TransactionTypes ThanksForTesting =
Response_TransactionTypes._(6, _omitEnumNames ? '' : 'ThanksForTesting');
static const Response_TransactionTypes AutoRenewal =
Response_TransactionTypes._(7, _omitEnumNames ? '' : 'AutoRenewal');
static const $core.List<Response_TransactionTypes> values =
<Response_TransactionTypes>[
Refund,
VoucherRedeemed,
VoucherCreated,
Cash,
PlanUpgrade,
Unknown,
ThanksForTesting,
AutoRenewal,
];
static final $core.List<Response_TransactionTypes?> _byValue =
$pb.ProtobufEnum.$_initByValueList(values, 7);
static Response_TransactionTypes? valueOf($core.int value) =>
value < 0 || value >= _byValue.length ? null : _byValue[value];
const Response_TransactionTypes._(super.value, super.name);
}
const $core.bool _omitEnumNames =
$core.bool.fromEnvironment('protobuf.omit_enum_names');

View file

@ -166,12 +166,10 @@ const Response$json = {
Response_Plans$json, Response_Plans$json,
Response_AddAccountsInvite$json, Response_AddAccountsInvite$json,
Response_AddAccountsInvites$json, Response_AddAccountsInvites$json,
Response_Transaction$json,
Response_AdditionalAccount$json, Response_AdditionalAccount$json,
Response_Voucher$json, Response_Deprecated$json,
Response_Vouchers$json, Response_Transaction$json,
Response_PlanBallance$json, Response_PlanBallance$json,
Response_Location$json,
Response_PreKey$json, Response_PreKey$json,
Response_SignedPreKey$json, Response_SignedPreKey$json,
Response_UserData$json, Response_UserData$json,
@ -180,7 +178,6 @@ const Response$json = {
Response_ProofOfWork$json, Response_ProofOfWork$json,
Response_Ok$json Response_Ok$json
], ],
'4': [Response_TransactionTypes$json],
'8': [ '8': [
{'1': 'Response'}, {'1': 'Response'},
], ],
@ -285,29 +282,6 @@ const Response_AddAccountsInvites$json = {
], ],
}; };
@$core.Deprecated('Use responseDescriptor instead')
const Response_Transaction$json = {
'1': 'Transaction',
'2': [
{'1': 'deposit_cents', '3': 1, '4': 1, '5': 3, '10': 'depositCents'},
{
'1': 'transaction_type',
'3': 2,
'4': 1,
'5': 14,
'6': '.server_to_client.Response.TransactionTypes',
'10': 'transactionType'
},
{
'1': 'created_at_unix_timestamp',
'3': 3,
'4': 1,
'5': 3,
'10': 'createdAtUnixTimestamp'
},
],
};
@$core.Deprecated('Use responseDescriptor instead') @$core.Deprecated('Use responseDescriptor instead')
const Response_AdditionalAccount$json = { const Response_AdditionalAccount$json = {
'1': 'AdditionalAccount', '1': 'AdditionalAccount',
@ -318,36 +292,13 @@ const Response_AdditionalAccount$json = {
}; };
@$core.Deprecated('Use responseDescriptor instead') @$core.Deprecated('Use responseDescriptor instead')
const Response_Voucher$json = { const Response_Deprecated$json = {
'1': 'Voucher', '1': 'Deprecated',
'2': [
{'1': 'voucher_id', '3': 1, '4': 1, '5': 9, '10': 'voucherId'},
{'1': 'value_cents', '3': 2, '4': 1, '5': 3, '10': 'valueCents'},
{'1': 'redeemed', '3': 3, '4': 1, '5': 8, '10': 'redeemed'},
{'1': 'requested', '3': 4, '4': 1, '5': 8, '10': 'requested'},
{
'1': 'created_at_unix_timestamp',
'3': 5,
'4': 1,
'5': 3,
'10': 'createdAtUnixTimestamp'
},
],
}; };
@$core.Deprecated('Use responseDescriptor instead') @$core.Deprecated('Use responseDescriptor instead')
const Response_Vouchers$json = { const Response_Transaction$json = {
'1': 'Vouchers', '1': 'Transaction',
'2': [
{
'1': 'vouchers',
'3': 1,
'4': 3,
'5': 11,
'6': '.server_to_client.Response.Voucher',
'10': 'vouchers'
},
],
}; };
@$core.Deprecated('Use responseDescriptor instead') @$core.Deprecated('Use responseDescriptor instead')
@ -429,16 +380,6 @@ const Response_PlanBallance$json = {
], ],
}; };
@$core.Deprecated('Use responseDescriptor instead')
const Response_Location$json = {
'1': 'Location',
'2': [
{'1': 'county', '3': 1, '4': 1, '5': 9, '10': 'county'},
{'1': 'region', '3': 2, '4': 1, '5': 9, '10': 'region'},
{'1': 'city', '3': 3, '4': 1, '5': 9, '10': 'city'},
],
};
@$core.Deprecated('Use responseDescriptor instead') @$core.Deprecated('Use responseDescriptor instead')
const Response_PreKey$json = { const Response_PreKey$json = {
'1': 'PreKey', '1': 'PreKey',
@ -602,13 +543,13 @@ const Response_Ok$json = {
}, },
{'1': 'authtoken', '3': 6, '4': 1, '5': 12, '9': 0, '10': 'authtoken'}, {'1': 'authtoken', '3': 6, '4': 1, '5': 12, '9': 0, '10': 'authtoken'},
{ {
'1': 'location', '1': 'deprecated_7',
'3': 7, '3': 7,
'4': 1, '4': 1,
'5': 11, '5': 11,
'6': '.server_to_client.Response.Location', '6': '.server_to_client.Response.Deprecated',
'9': 0, '9': 0,
'10': 'location' '10': 'deprecated7'
}, },
{ {
'1': 'authenticated', '1': 'authenticated',
@ -638,13 +579,13 @@ const Response_Ok$json = {
'10': 'planballance' '10': 'planballance'
}, },
{ {
'1': 'vouchers', '1': 'deprecated_11',
'3': 11, '3': 11,
'4': 1, '4': 1,
'5': 11, '5': 11,
'6': '.server_to_client.Response.Vouchers', '6': '.server_to_client.Response.Deprecated',
'9': 0, '9': 0,
'10': 'vouchers' '10': 'deprecated11'
}, },
{ {
'1': 'addaccountsinvites', '1': 'addaccountsinvites',
@ -688,21 +629,6 @@ const Response_Ok$json = {
], ],
}; };
@$core.Deprecated('Use responseDescriptor instead')
const Response_TransactionTypes$json = {
'1': 'TransactionTypes',
'2': [
{'1': 'Refund', '2': 0},
{'1': 'VoucherRedeemed', '2': 1},
{'1': 'VoucherCreated', '2': 2},
{'1': 'Cash', '2': 3},
{'1': 'PlanUpgrade', '2': 4},
{'1': 'Unknown', '2': 5},
{'1': 'ThanksForTesting', '2': 6},
{'1': 'AutoRenewal', '2': 7},
],
};
/// Descriptor for `Response`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `Response`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List responseDescriptor = $convert.base64Decode( final $typed_data.Uint8List responseDescriptor = $convert.base64Decode(
'CghSZXNwb25zZRIvCgJvaxgBIAEoCzIdLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuT2tIAF' 'CghSZXNwb25zZRIvCgJvaxgBIAEoCzIdLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuT2tIAF'
@ -720,64 +646,53 @@ final $typed_data.Uint8List responseDescriptor = $convert.base64Decode(
'bnQuUmVzcG9uc2UuUGxhblIFcGxhbnMaTQoRQWRkQWNjb3VudHNJbnZpdGUSFwoHcGxhbl9pZB' 'bnQuUmVzcG9uc2UuUGxhblIFcGxhbnMaTQoRQWRkQWNjb3VudHNJbnZpdGUSFwoHcGxhbl9pZB'
'gBIAEoCVIGcGxhbklkEh8KC2ludml0ZV9jb2RlGAIgASgJUgppbnZpdGVDb2RlGlwKEkFkZEFj' 'gBIAEoCVIGcGxhbklkEh8KC2ludml0ZV9jb2RlGAIgASgJUgppbnZpdGVDb2RlGlwKEkFkZEFj'
'Y291bnRzSW52aXRlcxJGCgdpbnZpdGVzGAEgAygLMiwuc2VydmVyX3RvX2NsaWVudC5SZXNwb2' 'Y291bnRzSW52aXRlcxJGCgdpbnZpdGVzGAEgAygLMiwuc2VydmVyX3RvX2NsaWVudC5SZXNwb2'
'5zZS5BZGRBY2NvdW50c0ludml0ZVIHaW52aXRlcxrFAQoLVHJhbnNhY3Rpb24SIwoNZGVwb3Np' '5zZS5BZGRBY2NvdW50c0ludml0ZVIHaW52aXRlcxpFChFBZGRpdGlvbmFsQWNjb3VudBIXCgd1'
'dF9jZW50cxgBIAEoA1IMZGVwb3NpdENlbnRzElYKEHRyYW5zYWN0aW9uX3R5cGUYAiABKA4yKy' 'c2VyX2lkGAEgASgDUgZ1c2VySWQSFwoHcGxhbl9pZBgDIAEoCVIGcGxhbklkGgwKCkRlcHJlY2'
'5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlRyYW5zYWN0aW9uVHlwZXNSD3RyYW5zYWN0aW9u' 'F0ZWQaDQoLVHJhbnNhY3Rpb24alwUKDFBsYW5CYWxsYW5jZRJACh11c2VkX2RhaWx5X21lZGlh'
'VHlwZRI5ChljcmVhdGVkX2F0X3VuaXhfdGltZXN0YW1wGAMgASgDUhZjcmVhdGVkQXRVbml4VG' 'X3VwbG9hZF9saW1pdBgBIAEoA1IZdXNlZERhaWx5TWVkaWFVcGxvYWRMaW1pdBI+Chx1c2VkX3'
'ltZXN0YW1wGkUKEUFkZGl0aW9uYWxBY2NvdW50EhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBIX' 'VwbG9hZF9tZWRpYV9zaXplX2xpbWl0GAIgASgDUhh1c2VkVXBsb2FkTWVkaWFTaXplTGltaXQS'
'CgdwbGFuX2lkGAMgASgJUgZwbGFuSWQavgEKB1ZvdWNoZXISHQoKdm91Y2hlcl9pZBgBIAEoCV' 'MwoTcGF5bWVudF9wZXJpb2RfZGF5cxgDIAEoA0gAUhFwYXltZW50UGVyaW9kRGF5c4gBARJLCi'
'IJdm91Y2hlcklkEh8KC3ZhbHVlX2NlbnRzGAIgASgDUgp2YWx1ZUNlbnRzEhoKCHJlZGVlbWVk' 'BsYXN0X3BheW1lbnRfZG9uZV91bml4X3RpbWVzdGFtcBgEIAEoA0gBUhxsYXN0UGF5bWVudERv'
'GAMgASgIUghyZWRlZW1lZBIcCglyZXF1ZXN0ZWQYBCABKAhSCXJlcXVlc3RlZBI5ChljcmVhdG' 'bmVVbml4VGltZXN0YW1wiAEBEkoKDHRyYW5zYWN0aW9ucxgFIAMoCzImLnNlcnZlcl90b19jbG'
'VkX2F0X3VuaXhfdGltZXN0YW1wGAUgASgDUhZjcmVhdGVkQXRVbml4VGltZXN0YW1wGkoKCFZv' 'llbnQuUmVzcG9uc2UuVHJhbnNhY3Rpb25SDHRyYW5zYWN0aW9ucxJdChNhZGRpdGlvbmFsX2Fj'
'dWNoZXJzEj4KCHZvdWNoZXJzGAEgAygLMiIuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5Wb3' 'Y291bnRzGAYgAygLMiwuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5BZGRpdGlvbmFsQWNjb3'
'VjaGVyUgh2b3VjaGVycxqXBQoMUGxhbkJhbGxhbmNlEkAKHXVzZWRfZGFpbHlfbWVkaWFfdXBs' 'VudFISYWRkaXRpb25hbEFjY291bnRzEiYKDGF1dG9fcmVuZXdhbBgHIAEoCEgCUgthdXRvUmVu'
'b2FkX2xpbWl0GAEgASgDUhl1c2VkRGFpbHlNZWRpYVVwbG9hZExpbWl0Ej4KHHVzZWRfdXBsb2' 'ZXdhbIgBARJCChthZGRpdGlvbmFsX2FjY291bnRfb3duZXJfaWQYCCABKANIA1IYYWRkaXRpb2'
'FkX21lZGlhX3NpemVfbGltaXQYAiABKANSGHVzZWRVcGxvYWRNZWRpYVNpemVMaW1pdBIzChNw' '5hbEFjY291bnRPd25lcklkiAEBQhYKFF9wYXltZW50X3BlcmlvZF9kYXlzQiMKIV9sYXN0X3Bh'
'YXltZW50X3BlcmlvZF9kYXlzGAMgASgDSABSEXBheW1lbnRQZXJpb2REYXlziAEBEksKIGxhc3' 'eW1lbnRfZG9uZV91bml4X3RpbWVzdGFtcEIPCg1fYXV0b19yZW5ld2FsQh4KHF9hZGRpdGlvbm'
'RfcGF5bWVudF9kb25lX3VuaXhfdGltZXN0YW1wGAQgASgDSAFSHGxhc3RQYXltZW50RG9uZVVu' 'FsX2FjY291bnRfb3duZXJfaWQaMAoGUHJlS2V5Eg4KAmlkGAEgASgDUgJpZBIWCgZwcmVrZXkY'
'aXhUaW1lc3RhbXCIAQESSgoMdHJhbnNhY3Rpb25zGAUgAygLMiYuc2VydmVyX3RvX2NsaWVudC' 'AiABKAxSBnByZWtleRqVAQoMU2lnbmVkUHJlS2V5EigKEHNpZ25lZF9wcmVrZXlfaWQYASABKA'
'5SZXNwb25zZS5UcmFuc2FjdGlvblIMdHJhbnNhY3Rpb25zEl0KE2FkZGl0aW9uYWxfYWNjb3Vu' 'NSDnNpZ25lZFByZWtleUlkEiMKDXNpZ25lZF9wcmVrZXkYAiABKAxSDHNpZ25lZFByZWtleRI2'
'dHMYBiADKAsyLC5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLkFkZGl0aW9uYWxBY2NvdW50Uh' 'ChdzaWduZWRfcHJla2V5X3NpZ25hdHVyZRgDIAEoDFIVc2lnbmVkUHJla2V5U2lnbmF0dXJlGv'
'JhZGRpdGlvbmFsQWNjb3VudHMSJgoMYXV0b19yZW5ld2FsGAcgASgISAJSC2F1dG9SZW5ld2Fs' 'YDCghVc2VyRGF0YRIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQSOwoHcHJla2V5cxgCIAMoCzIh'
'iAEBEkIKG2FkZGl0aW9uYWxfYWNjb3VudF9vd25lcl9pZBgIIAEoA0gDUhhhZGRpdGlvbmFsQW' 'LnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuUHJlS2V5UgdwcmVrZXlzEh8KCHVzZXJuYW1lGA'
'Njb3VudE93bmVySWSIAQFCFgoUX3BheW1lbnRfcGVyaW9kX2RheXNCIwohX2xhc3RfcGF5bWVu' 'cgASgMSABSCHVzZXJuYW1liAEBEjMKE3B1YmxpY19pZGVudGl0eV9rZXkYAyABKAxIAVIRcHVi'
'dF9kb25lX3VuaXhfdGltZXN0YW1wQg8KDV9hdXRvX3JlbmV3YWxCHgocX2FkZGl0aW9uYWxfYW' 'bGljSWRlbnRpdHlLZXmIAQESKAoNc2lnbmVkX3ByZWtleRgEIAEoDEgCUgxzaWduZWRQcmVrZX'
'Njb3VudF9vd25lcl9pZBpOCghMb2NhdGlvbhIWCgZjb3VudHkYASABKAlSBmNvdW50eRIWCgZy' 'mIAQESOwoXc2lnbmVkX3ByZWtleV9zaWduYXR1cmUYBSABKAxIA1IVc2lnbmVkUHJla2V5U2ln'
'ZWdpb24YAiABKAlSBnJlZ2lvbhISCgRjaXR5GAMgASgJUgRjaXR5GjAKBlByZUtleRIOCgJpZB' 'bmF0dXJliAEBEi0KEHNpZ25lZF9wcmVrZXlfaWQYBiABKANIBFIOc2lnbmVkUHJla2V5SWSIAQ'
'gBIAEoA1ICaWQSFgoGcHJla2V5GAIgASgMUgZwcmVrZXkalQEKDFNpZ25lZFByZUtleRIoChBz' 'ESLAoPcmVnaXN0cmF0aW9uX2lkGAggASgDSAVSDnJlZ2lzdHJhdGlvbklkiAEBQgsKCV91c2Vy'
'aWduZWRfcHJla2V5X2lkGAEgASgDUg5zaWduZWRQcmVrZXlJZBIjCg1zaWduZWRfcHJla2V5GA' 'bmFtZUIWChRfcHVibGljX2lkZW50aXR5X2tleUIQCg5fc2lnbmVkX3ByZWtleUIaChhfc2lnbm'
'IgASgMUgxzaWduZWRQcmVrZXkSNgoXc2lnbmVkX3ByZWtleV9zaWduYXR1cmUYAyABKAxSFXNp' 'VkX3ByZWtleV9zaWduYXR1cmVCEwoRX3NpZ25lZF9wcmVrZXlfaWRCEgoQX3JlZ2lzdHJhdGlv'
'Z25lZFByZWtleVNpZ25hdHVyZRr2AwoIVXNlckRhdGESFwoHdXNlcl9pZBgBIAEoA1IGdXNlck' 'bl9pZBpZCgtVcGxvYWRUb2tlbhIhCgx1cGxvYWRfdG9rZW4YASABKAxSC3VwbG9hZFRva2VuEi'
'lkEjsKB3ByZWtleXMYAiADKAsyIS5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlByZUtleVIH' 'cKD2Rvd25sb2FkX3Rva2VucxgCIAMoDFIOZG93bmxvYWRUb2tlbnMaOQoORG93bmxvYWRUb2tl'
'cHJla2V5cxIfCgh1c2VybmFtZRgHIAEoDEgAUgh1c2VybmFtZYgBARIzChNwdWJsaWNfaWRlbn' 'bnMSJwoPZG93bmxvYWRfdG9rZW5zGAEgAygMUg5kb3dubG9hZFRva2VucxpFCgtQcm9vZk9mV2'
'RpdHlfa2V5GAMgASgMSAFSEXB1YmxpY0lkZW50aXR5S2V5iAEBEigKDXNpZ25lZF9wcmVrZXkY' '9yaxIWCgZwcmVmaXgYASABKAlSBnByZWZpeBIeCgpkaWZmaWN1bHR5GAIgASgDUgpkaWZmaWN1'
'BCABKAxIAlIMc2lnbmVkUHJla2V5iAEBEjsKF3NpZ25lZF9wcmVrZXlfc2lnbmF0dXJlGAUgAS' 'bHR5GtcHCgJPaxIUCgROb25lGAEgASgISABSBE5vbmUSGAoGdXNlcmlkGAIgASgDSABSBnVzZX'
'gMSANSFXNpZ25lZFByZWtleVNpZ25hdHVyZYgBARItChBzaWduZWRfcHJla2V5X2lkGAYgASgD' 'JpZBImCg1hdXRoY2hhbGxlbmdlGAMgASgMSABSDWF1dGhjaGFsbGVuZ2USSgoLdXBsb2FkdG9r'
'SARSDnNpZ25lZFByZWtleUlkiAEBEiwKD3JlZ2lzdHJhdGlvbl9pZBgIIAEoA0gFUg5yZWdpc3' 'ZW4YBCABKAsyJi5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlVwbG9hZFRva2VuSABSC3VwbG'
'RyYXRpb25JZIgBAUILCglfdXNlcm5hbWVCFgoUX3B1YmxpY19pZGVudGl0eV9rZXlCEAoOX3Np' '9hZHRva2VuEkEKCHVzZXJkYXRhGAUgASgLMiMuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5V'
'Z25lZF9wcmVrZXlCGgoYX3NpZ25lZF9wcmVrZXlfc2lnbmF0dXJlQhMKEV9zaWduZWRfcHJla2' 'c2VyRGF0YUgAUgh1c2VyZGF0YRIeCglhdXRodG9rZW4YBiABKAxIAFIJYXV0aHRva2VuEkoKDG'
'V5X2lkQhIKEF9yZWdpc3RyYXRpb25faWQaWQoLVXBsb2FkVG9rZW4SIQoMdXBsb2FkX3Rva2Vu' 'RlcHJlY2F0ZWRfNxgHIAEoCzIlLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuRGVwcmVjYXRl'
'GAEgASgMUgt1cGxvYWRUb2tlbhInCg9kb3dubG9hZF90b2tlbnMYAiADKAxSDmRvd25sb2FkVG' 'ZEgAUgtkZXByZWNhdGVkNxJQCg1hdXRoZW50aWNhdGVkGAggASgLMiguc2VydmVyX3RvX2NsaW'
'9rZW5zGjkKDkRvd25sb2FkVG9rZW5zEicKD2Rvd25sb2FkX3Rva2VucxgBIAMoDFIOZG93bmxv' 'VudC5SZXNwb25zZS5BdXRoZW50aWNhdGVkSABSDWF1dGhlbnRpY2F0ZWQSOAoFcGxhbnMYCSAB'
'YWRUb2tlbnMaRQoLUHJvb2ZPZldvcmsSFgoGcHJlZml4GAEgASgJUgZwcmVmaXgSHgoKZGlmZm' 'KAsyIC5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlBsYW5zSABSBXBsYW5zEk0KDHBsYW5iYW'
'ljdWx0eRgCIAEoA1IKZGlmZmljdWx0eRrDBwoCT2sSFAoETm9uZRgBIAEoCEgAUgROb25lEhgK' 'xsYW5jZRgKIAEoCzInLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuUGxhbkJhbGxhbmNlSABS'
'BnVzZXJpZBgCIAEoA0gAUgZ1c2VyaWQSJgoNYXV0aGNoYWxsZW5nZRgDIAEoDEgAUg1hdXRoY2' 'DHBsYW5iYWxsYW5jZRJMCg1kZXByZWNhdGVkXzExGAsgASgLMiUuc2VydmVyX3RvX2NsaWVudC'
'hhbGxlbmdlEkoKC3VwbG9hZHRva2VuGAQgASgLMiYuc2VydmVyX3RvX2NsaWVudC5SZXNwb25z' '5SZXNwb25zZS5EZXByZWNhdGVkSABSDGRlcHJlY2F0ZWQxMRJfChJhZGRhY2NvdW50c2ludml0'
'ZS5VcGxvYWRUb2tlbkgAUgt1cGxvYWR0b2tlbhJBCgh1c2VyZGF0YRgFIAEoCzIjLnNlcnZlcl' 'ZXMYDCABKAsyLS5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLkFkZEFjY291bnRzSW52aXRlc0'
'90b19jbGllbnQuUmVzcG9uc2UuVXNlckRhdGFIAFIIdXNlcmRhdGESHgoJYXV0aHRva2VuGAYg' 'gAUhJhZGRhY2NvdW50c2ludml0ZXMSUwoOZG93bmxvYWR0b2tlbnMYDSABKAsyKS5zZXJ2ZXJf'
'ASgMSABSCWF1dGh0b2tlbhJBCghsb2NhdGlvbhgHIAEoCzIjLnNlcnZlcl90b19jbGllbnQuUm' 'dG9fY2xpZW50LlJlc3BvbnNlLkRvd25sb2FkVG9rZW5zSABSDmRvd25sb2FkdG9rZW5zEk0KDH'
'VzcG9uc2UuTG9jYXRpb25IAFIIbG9jYXRpb24SUAoNYXV0aGVudGljYXRlZBgIIAEoCzIoLnNl' 'NpZ25lZHByZWtleRgOIAEoCzInLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuU2lnbmVkUHJl'
'cnZlcl90b19jbGllbnQuUmVzcG9uc2UuQXV0aGVudGljYXRlZEgAUg1hdXRoZW50aWNhdGVkEj' 'S2V5SABSDHNpZ25lZHByZWtleRJKCgtwcm9vZk9mV29yaxgPIAEoCzImLnNlcnZlcl90b19jbG'
'gKBXBsYW5zGAkgASgLMiAuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5QbGFuc0gAUgVwbGFu' 'llbnQuUmVzcG9uc2UuUHJvb2ZPZldvcmtIAFILcHJvb2ZPZldvcmtCBAoCT2tCCgoIUmVzcG9u'
'cxJNCgxwbGFuYmFsbGFuY2UYCiABKAsyJy5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlBsYW' 'c2U=');
'5CYWxsYW5jZUgAUgxwbGFuYmFsbGFuY2USQQoIdm91Y2hlcnMYCyABKAsyIy5zZXJ2ZXJfdG9f'
'Y2xpZW50LlJlc3BvbnNlLlZvdWNoZXJzSABSCHZvdWNoZXJzEl8KEmFkZGFjY291bnRzaW52aX'
'RlcxgMIAEoCzItLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuQWRkQWNjb3VudHNJbnZpdGVz'
'SABSEmFkZGFjY291bnRzaW52aXRlcxJTCg5kb3dubG9hZHRva2VucxgNIAEoCzIpLnNlcnZlcl'
'90b19jbGllbnQuUmVzcG9uc2UuRG93bmxvYWRUb2tlbnNIAFIOZG93bmxvYWR0b2tlbnMSTQoM'
'c2lnbmVkcHJla2V5GA4gASgLMicuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5TaWduZWRQcm'
'VLZXlIAFIMc2lnbmVkcHJla2V5EkoKC3Byb29mT2ZXb3JrGA8gASgLMiYuc2VydmVyX3RvX2Ns'
'aWVudC5SZXNwb25zZS5Qcm9vZk9mV29ya0gAUgtwcm9vZk9mV29ya0IECgJPayKWAQoQVHJhbn'
'NhY3Rpb25UeXBlcxIKCgZSZWZ1bmQQABITCg9Wb3VjaGVyUmVkZWVtZWQQARISCg5Wb3VjaGVy'
'Q3JlYXRlZBACEggKBENhc2gQAxIPCgtQbGFuVXBncmFkZRAEEgsKB1Vua25vd24QBRIUChBUaG'
'Fua3NGb3JUZXN0aW5nEAYSDwoLQXV0b1JlbmV3YWwQB0IKCghSZXNwb25zZQ==');

View file

@ -16,7 +16,6 @@ import 'package:twonly/src/visual/views/onboarding/recover.view.dart';
import 'package:twonly/src/visual/views/public_profile.view.dart'; import 'package:twonly/src/visual/views/public_profile.view.dart';
import 'package:twonly/src/visual/views/settings/account.view.dart'; import 'package:twonly/src/visual/views/settings/account.view.dart';
import 'package:twonly/src/visual/views/settings/appearance.view.dart'; import 'package:twonly/src/visual/views/settings/appearance.view.dart';
import 'package:twonly/src/visual/views/settings/backup/backup_server.view.dart';
import 'package:twonly/src/visual/views/settings/backup/backup_settings.view.dart'; import 'package:twonly/src/visual/views/settings/backup/backup_settings.view.dart';
import 'package:twonly/src/visual/views/settings/backup/backup_setup.view.dart'; import 'package:twonly/src/visual/views/settings/backup/backup_setup.view.dart';
import 'package:twonly/src/visual/views/settings/chat/chat_reactions.view.dart'; import 'package:twonly/src/visual/views/settings/chat/chat_reactions.view.dart';
@ -165,10 +164,6 @@ final routerProvider = GoRouter(
path: 'backup', path: 'backup',
builder: (context, state) => const BackupView(), builder: (context, state) => const BackupView(),
routes: [ routes: [
GoRoute(
path: 'server',
builder: (context, state) => const BackupServerView(),
),
GoRoute( GoRoute(
path: 'recovery', path: 'recovery',
builder: (context, state) => const BackupRecoveryView(), builder: (context, state) => const BackupRecoveryView(),

View file

@ -15,6 +15,7 @@ import 'package:flutter/foundation.dart';
import 'package:libsignal_protocol_dart/src/ecc/ed25519.dart'; import 'package:libsignal_protocol_dart/src/ecc/ed25519.dart';
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:twonly/core/bridge/wrapper/key_manager.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/secure_storage.keys.dart'; import 'package:twonly/src/constants/secure_storage.keys.dart';
@ -31,7 +32,7 @@ import 'package:twonly/src/services/api/messages.api.dart';
import 'package:twonly/src/services/api/server_messages.api.dart'; import 'package:twonly/src/services/api/server_messages.api.dart';
import 'package:twonly/src/services/api/utils.api.dart'; import 'package:twonly/src/services/api/utils.api.dart';
import 'package:twonly/src/services/flame.service.dart'; import 'package:twonly/src/services/flame.service.dart';
import 'package:twonly/src/services/group.services.dart'; import 'package:twonly/src/services/group.service.dart';
import 'package:twonly/src/services/notifications/fcm.notifications.dart'; import 'package:twonly/src/services/notifications/fcm.notifications.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/services/signal/identity.signal.dart'; import 'package:twonly/src/services/signal/identity.signal.dart';
@ -60,6 +61,8 @@ class ApiService {
// final String apiHost = kReleaseMode ? 'api.twonly.eu' : 'dev.twonly.eu'; // final String apiHost = kReleaseMode ? 'api.twonly.eu' : 'dev.twonly.eu';
final String apiSecure = kReleaseMode ? 's' : 's'; final String apiSecure = kReleaseMode ? 's' : 's';
String get apiEndpoint => 'http$apiSecure://$apiHost/api/';
final _planUpdateController = StreamController<SubscriptionPlan>.broadcast(); final _planUpdateController = StreamController<SubscriptionPlan>.broadcast();
Stream<SubscriptionPlan> get onPlanUpdated => _planUpdateController.stream; Stream<SubscriptionPlan> get onPlanUpdated => _planUpdateController.stream;
@ -92,6 +95,7 @@ class ApiService {
try { try {
final channel = IOWebSocketChannel.connect( final channel = IOWebSocketChannel.connect(
Uri.parse(apiUrl), Uri.parse(apiUrl),
pingInterval: const Duration(seconds: 30),
); );
_channel = channel; _channel = channel;
_channel!.stream.listen(_onData, onDone: _onDone, onError: _onError); _channel!.stream.listen(_onData, onDone: _onDone, onError: _onError);
@ -122,7 +126,7 @@ class ApiService {
twonlyDB.markUpdated(); twonlyDB.markUpdated();
unawaited(syncFlameCounters()); unawaited(syncFlameCounters());
unawaited(setupNotificationWithUsers()); unawaited(setupNotificationWithUsers());
unawaited(signalHandleNewServerConnection()); unawaited(SignalIdentityService.onAuthenticated());
resetResyncedUsers(); resetResyncedUsers();
resetUserDiscoveryRequestUpdates(); resetUserDiscoveryRequestUpdates();
unawaited(fetchGroupStatesForUnjoinedGroups()); unawaited(fetchGroupStatesForUnjoinedGroups());
@ -244,11 +248,11 @@ class ApiService {
try { try {
final msg = server.ServerToClient.fromBuffer(msgBuffer as Uint8List); final msg = server.ServerToClient.fromBuffer(msgBuffer as Uint8List);
if (msg.v0.hasResponse()) { if (msg.v0.hasResponse()) {
await removeFromRetransmissionBuffer(msg.v0.seq);
final completer = _pendingRequests.remove(msg.v0.seq); final completer = _pendingRequests.remove(msg.v0.seq);
if (completer != null && !completer.isCompleted) { if (completer != null && !completer.isCompleted) {
completer.complete(msg); completer.complete(msg);
} }
unawaited(removeFromRetransmissionBuffer(msg.v0.seq));
} else { } else {
unawaited(handleServerMessage(msg)); unawaited(handleServerMessage(msg));
} }
@ -414,6 +418,7 @@ class ApiService {
), ),
); );
} }
await twonlyDB.receiptsDao.deleteReceiptForUser(contactId);
} }
} }
return res; return res;
@ -450,6 +455,21 @@ class ApiService {
await onAuthenticated(); await onAuthenticated();
} else { } else {
unawaited(onAuthenticated()); unawaited(onAuthenticated());
try {
Log.info('Switching authentication to login token');
final loginToken = await RustKeyManager.getLoginToken();
final res = await _setLoginToken(loginToken);
if (res.isSuccess) {
Log.info('Switch was successfully.');
await UserService.update((u) => u.canUseLoginTokenForAuth = true);
await SecureStorage.instance.delete(
key: SecureStorageKeys.apiAuthToken,
);
}
} catch (e) {
Log.error(e);
}
} }
return true; return true;
} }
@ -466,16 +486,62 @@ class ApiService {
return false; return false;
} }
Future<bool> tryAuthenticateWithLoginToken() async {
try {
final loginToken = await RustKeyManager.getLoginToken();
final authenticate = Handshake_AuthenticateWithLoginToken()
..userId = Int64(userService.currentUser.userId)
..appVersion = (await PackageInfo.fromPlatform()).version
..deviceId = Int64(userService.currentUser.deviceId)
..inBackground = AppState.isInBackgroundTask
..secretLoginToken = loginToken.toList();
final handshake = Handshake()..authenticateWithLoginToken = authenticate;
final req = createClientToServerFromHandshake(handshake);
final result = await sendRequestSync(req, authenticated: false);
if (result.isSuccess) {
Log.info('websocket is authenticated');
isAuthenticated = true;
if (AppState.isInBackgroundTask) {
await onAuthenticated();
} else {
unawaited(onAuthenticated());
}
return true;
}
if (result.isError) {
if (result.error != ErrorCode.AuthTokenNotValid &&
result.error != ErrorCode.ForegroundSessionConnected) {
Log.error(
'got error while authenticating to the server: ${result.error}',
);
return false;
}
}
} catch (e) {
Log.error(e);
}
return false;
}
Future<void> authenticate() async { Future<void> authenticate() async {
return lockAuthentication.protect(() async { return lockAuthentication.protect(() async {
if (isAuthenticated) return; if (isAuthenticated) return;
if (await getSignalIdentity() == null) {
Log.error('Signal identity not found.'); if (!userService.isUserCreated) {
return; return;
} }
if (!userService.isUserCreated) return; if (!userService.isUserCreated) return;
if (userService.currentUser.canUseLoginTokenForAuth) {
await tryAuthenticateWithLoginToken();
return;
}
if (await tryAuthenticateWithToken()) { if (await tryAuthenticateWithToken()) {
return; return;
} }
@ -542,6 +608,8 @@ class ApiService {
final signedPreKey = (await signalStore.loadSignedPreKeys())[0]; final signedPreKey = (await signalStore.loadSignedPreKeys())[0];
final loginToken = await RustKeyManager.getLoginToken();
final register = Handshake_Register() final register = Handshake_Register()
..username = username ..username = username
..publicIdentityKey = (await signalStore.getIdentityKeyPair()) ..publicIdentityKey = (await signalStore.getIdentityKeyPair())
@ -552,6 +620,7 @@ class ApiService {
..signedPrekeySignature = signedPreKey.signature ..signedPrekeySignature = signedPreKey.signature
..signedPrekeyId = Int64(signedPreKey.id) ..signedPrekeyId = Int64(signedPreKey.id)
..langCode = ui.PlatformDispatcher.instance.locale.languageCode ..langCode = ui.PlatformDispatcher.instance.locale.languageCode
..loginToken = loginToken
..proofOfWork = Int64(proofOfWorkResult) ..proofOfWork = Int64(proofOfWorkResult)
..isIos = Platform.isIOS; ..isIos = Platform.isIOS;
@ -617,13 +686,28 @@ class ApiService {
return sendRequestSync(req, ensureRetransmission: true); return sendRequestSync(req, ensureRetransmission: true);
} }
Future<Result> getCurrentLocation() async { Future<Result> _setLoginToken(List<int> token) async {
final get = ApplicationData_GetLocation(); final get = ApplicationData_SetLoginToken()..loginToken = token;
final appData = ApplicationData()..getLocation = get; final appData = ApplicationData()..setLoginToken = get;
final req = createClientToServerFromApplicationData(appData); final req = createClientToServerFromApplicationData(appData);
return sendRequestSync(req); return sendRequestSync(req);
} }
Future<int?> getUserIdFromUsername(String username) async {
final appData = Handshake(
getUseridByUsername: Handshake_GetUserIdByUsername(username: username),
);
final req = createClientToServerFromHandshake(appData);
final res = await sendRequestSync(req);
if (res.isSuccess) {
final ok = res.value as server.Response_Ok;
if (ok.hasUserid()) {
return ok.userid.toInt();
}
}
return null;
}
Future<Response_UserData?> getUserData(String username) async { Future<Response_UserData?> getUserData(String username) async {
final get = ApplicationData_GetUserByUsername()..username = username; final get = ApplicationData_GetUserByUsername()..username = username;
final appData = ApplicationData()..getUserByUsername = get; final appData = ApplicationData()..getUserByUsername = get;
@ -652,27 +736,6 @@ class ApiService {
return null; return null;
} }
Future<Response_Vouchers?> getVoucherList() async {
final get = ApplicationData_GetVouchers();
final appData = ApplicationData()..getVouchers = get;
final req = createClientToServerFromApplicationData(appData);
final res = await sendRequestSync(req);
if (res.isSuccess) {
final ok = res.value as server.Response_Ok;
if (ok.hasVouchers()) {
return ok.vouchers;
}
}
return null;
}
Future<Result> updatePlanOptions(bool autoRenewal) async {
final get = ApplicationData_UpdatePlanOptions()..autoRenewal = autoRenewal;
final appData = ApplicationData()..updatePlanOptions = get;
final req = createClientToServerFromApplicationData(appData);
return sendRequestSync(req);
}
Future<Result> removeAdditionalUser(Int64 userId) async { Future<Result> removeAdditionalUser(Int64 userId) async {
final get = ApplicationData_RemoveAdditionalUser()..userId = userId; final get = ApplicationData_RemoveAdditionalUser()..userId = userId;
final appData = ApplicationData()..removeAdditionalUser = get; final appData = ApplicationData()..removeAdditionalUser = get;
@ -687,34 +750,6 @@ class ApiService {
return sendRequestSync(req, contactId: userId.toInt()); return sendRequestSync(req, contactId: userId.toInt());
} }
Future<Result> buyVoucher(int valueInCents) async {
final get = ApplicationData_CreateVoucher()..valueCents = valueInCents;
final appData = ApplicationData()..createVoucher = get;
final req = createClientToServerFromApplicationData(appData);
return sendRequestSync(req);
}
Future<Result> switchToPayedPlan(
String planId,
bool payMonthly,
bool autoRenewal,
) async {
final get = ApplicationData_SwitchToPayedPlan()
..planId = planId
..payMonthly = payMonthly
..autoRenewal = autoRenewal;
final appData = ApplicationData()..switchtoPayedPlan = get;
final req = createClientToServerFromApplicationData(appData);
return sendRequestSync(req);
}
Future<Result> redeemVoucher(String voucher) async {
final get = ApplicationData_RedeemVoucher()..voucher = voucher;
final appData = ApplicationData()..redeemVoucher = get;
final req = createClientToServerFromApplicationData(appData);
return sendRequestSync(req);
}
Future<Result> reportUser(int userId, String reason) async { Future<Result> reportUser(int userId, String reason) async {
final get = ApplicationData_ReportUser() final get = ApplicationData_ReportUser()
..reportedUserId = Int64(userId) ..reportedUserId = Int64(userId)
@ -731,13 +766,6 @@ class ApiService {
return sendRequestSync(req); return sendRequestSync(req);
} }
Future<Result> redeemUserInviteCode(String inviteCode) async {
final get = ApplicationData_RedeemAdditionalCode()..inviteCode = inviteCode;
final appData = ApplicationData()..redeemAdditionalCode = get;
final req = createClientToServerFromApplicationData(appData);
return sendRequestSync(req);
}
Future<Result> updateFCMToken(String googleFcm) async { Future<Result> updateFCMToken(String googleFcm) async {
final get = ApplicationData_UpdateGoogleFcmToken()..googleFcm = googleFcm; final get = ApplicationData_UpdateGoogleFcmToken()..googleFcm = googleFcm;
final appData = ApplicationData()..updateGoogleFcmToken = get; final appData = ApplicationData()..updateGoogleFcmToken = get;

View file

@ -60,6 +60,15 @@ Future<bool> handleNewContactRequest(int fromUserId) async {
} }
Future<void> handleContactAccept(int fromUserId) async { Future<void> handleContactAccept(int fromUserId) async {
final contact = await twonlyDB.contactsDao
.getContactByUserId(fromUserId)
.getSingleOrNull();
if (contact == null) return;
if (contact.requested || contact.deletedByUser) {
Log.error('User has never send an request. So ignore the Accept.');
return;
}
await twonlyDB.contactsDao.updateContact( await twonlyDB.contactsDao.updateContact(
fromUserId, fromUserId,
const ContactsCompanion( const ContactsCompanion(
@ -68,10 +77,6 @@ Future<void> handleContactAccept(int fromUserId) async {
deletedByUser: Value(false), deletedByUser: Value(false),
), ),
); );
final contact = await twonlyDB.contactsDao
.getContactByUserId(fromUserId)
.getSingleOrNull();
if (contact != null) {
await twonlyDB.groupsDao.createNewDirectChat( await twonlyDB.groupsDao.createNewDirectChat(
fromUserId, fromUserId,
GroupsCompanion( GroupsCompanion(
@ -79,7 +84,6 @@ Future<void> handleContactAccept(int fromUserId) async {
), ),
); );
} }
}
Future<bool> handleContactRequest( Future<bool> handleContactRequest(
int fromUserId, int fromUserId,
@ -143,8 +147,8 @@ Future<void> handleContactUpdate(
groupId: Value(group.groupId), groupId: Value(group.groupId),
type: const Value(GroupActionType.updatedContactUsername), type: const Value(GroupActionType.updatedContactUsername),
contactId: Value(fromUserId), contactId: Value(fromUserId),
oldGroupName: Value('@${contact.username}'), oldGroupName: Value(contact.username),
newGroupName: Value('@${contactUpdate.username}'), newGroupName: Value(contactUpdate.username),
), ),
); );
} }
@ -157,7 +161,7 @@ Future<void> handleContactUpdate(
groupId: Value(group.groupId), groupId: Value(group.groupId),
type: const Value(GroupActionType.updatedContactDisplayName), type: const Value(GroupActionType.updatedContactDisplayName),
contactId: Value(fromUserId), contactId: Value(fromUserId),
oldGroupName: Value(contact.displayName ?? ''), oldGroupName: Value(contact.displayName ?? contact.username),
newGroupName: Value(contactUpdate.displayName), newGroupName: Value(contactUpdate.displayName),
), ),
); );

View file

@ -8,7 +8,7 @@ import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/messages.api.dart'; import 'package:twonly/src/services/api/messages.api.dart';
import 'package:twonly/src/services/api/utils.api.dart'; import 'package:twonly/src/services/api/utils.api.dart';
import 'package:twonly/src/services/group.services.dart'; import 'package:twonly/src/services/group.service.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
Future<void> handleGroupCreate( Future<void> handleGroupCreate(

View file

@ -267,13 +267,13 @@ Future<void> requestMediaReupload(String mediaId) async {
final messages = await twonlyDB.messagesDao.getMessagesByMediaId(mediaId); final messages = await twonlyDB.messagesDao.getMessagesByMediaId(mediaId);
for (final message in messages) { for (final message in messages) {
if (message.openedAt != null) continue; if (message.openedAt != null || message.senderId == null) continue;
await sendCipherText( await sendCipherText(
messages.first.senderId!, message.senderId!,
EncryptedContent( EncryptedContent(
mediaUpdate: EncryptedContent_MediaUpdate( mediaUpdate: EncryptedContent_MediaUpdate(
type: EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR, type: EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR,
targetMessageId: messages.first.messageId, targetMessageId: message.messageId,
), ),
), ),
); );
@ -356,8 +356,6 @@ Future<void> handleEncryptedFile(String mediaId) async {
Log.info('Decryption of $mediaId was successful'); Log.info('Decryption of $mediaId was successful');
mediaService.encryptedPath.deleteSync(); mediaService.encryptedPath.deleteSync();
unawaited(apiService.downloadDone(mediaService.mediaFile.downloadToken!));
}, },
); );
} }

View file

@ -8,7 +8,7 @@ import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/api/mediafiles/download.api.dart'; import 'package:twonly/src/services/api/mediafiles/download.api.dart';
import 'package:twonly/src/services/api/mediafiles/upload.api.dart'; import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
import 'package:twonly/src/services/backup/create.backup.dart'; import 'package:twonly/src/services/backup.service.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
@ -22,8 +22,11 @@ Future<void> initFileDownloader() async {
if (update.task.taskId.contains('download_')) { if (update.task.taskId.contains('download_')) {
await handleDownloadStatusUpdate(update); await handleDownloadStatusUpdate(update);
} }
if (update.task.taskId.contains('backup')) { if (update.task.taskId.contains('backup_')) {
await handleBackupStatusUpdate(update); await BackupService.handleBackupStatusUpdate(
update.task.taskId,
update,
);
} }
case TaskProgressUpdate(): case TaskProgressUpdate():
Log.info( Log.info(

View file

@ -1,6 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'package:background_downloader/background_downloader.dart'; import 'package:background_downloader/background_downloader.dart';
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:connectivity_plus/connectivity_plus.dart';
@ -12,7 +10,6 @@ import 'package:http/http.dart' as http;
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/secure_storage.keys.dart';
import 'package:twonly/src/database/tables/mediafiles.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/messages.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
@ -21,12 +18,12 @@ import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/mediafiles/media_background.api.dart'; import 'package:twonly/src/services/api/mediafiles/media_background.api.dart';
import 'package:twonly/src/services/api/messages.api.dart'; import 'package:twonly/src/services/api/messages.api.dart';
import 'package:twonly/src/services/api/utils.api.dart';
import 'package:twonly/src/services/flame.service.dart'; import 'package:twonly/src/services/flame.service.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/exclusive_access.utils.dart'; import 'package:twonly/src/utils/exclusive_access.utils.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/secure_storage.dart';
import 'package:workmanager/workmanager.dart' hide TaskStatus; import 'package:workmanager/workmanager.dart' hide TaskStatus;
final lockRetransmission = Mutex(); final lockRetransmission = Mutex();
@ -620,22 +617,17 @@ Future<void> _uploadUploadRequest(MediaFileService media) async {
return null; return null;
} }
final apiAuthTokenRaw = await SecureStorage.instance.read(
key: SecureStorageKeys.apiAuthToken,
);
if (apiAuthTokenRaw == null) {
Log.error('api auth token not defined.');
return null;
}
final apiAuthToken = uint8ListToHex(base64Decode(apiAuthTokenRaw));
final apiUrl = final apiUrl =
'http${apiService.apiSecure}://${apiService.apiHost}/api/upload'; 'http${apiService.apiSecure}://${apiService.apiHost}/api/upload';
// try {
Log.info('Starting upload from ${media.mediaFile.mediaId}'); Log.info('Starting upload from ${media.mediaFile.mediaId}');
final headers = await getAuthenticationHeader();
if (headers == null) {
Log.error('Auth headers are empty. Returning');
return;
}
final task = UploadTask.fromFile( final task = UploadTask.fromFile(
taskId: 'upload_${media.mediaFile.mediaId}', taskId: 'upload_${media.mediaFile.mediaId}',
displayName: media.mediaFile.type.name, displayName: media.mediaFile.type.name,
@ -643,9 +635,7 @@ Future<void> _uploadUploadRequest(MediaFileService media) async {
url: apiUrl, url: apiUrl,
priority: 0, priority: 0,
retries: 10, retries: 10,
headers: { headers: headers,
'x-twonly-auth-token': apiAuthToken,
},
); );
final connectivityResult = await Connectivity().checkConnectivity(); final connectivityResult = await Connectivity().checkConnectivity();

View file

@ -67,8 +67,9 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
Receipt? receipt, Receipt? receipt,
bool onlyReturnEncryptedData = false, bool onlyReturnEncryptedData = false,
bool blocking = true, bool blocking = true,
bool useLock = true,
}) async { }) async {
if (apiService.appIsOutdated) return null;
try { try {
if (receiptId == null && receipt == null) return null; if (receiptId == null && receipt == null) return null;
if (receipt == null) { if (receipt == null) {
@ -133,7 +134,6 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
final cipherText = await signalEncryptMessage( final cipherText = await signalEncryptMessage(
receipt.contactId, receipt.contactId,
Uint8List.fromList(message.encryptedContent), Uint8List.fromList(message.encryptedContent),
useLock: useLock,
); );
if (cipherText == null) { if (cipherText == null) {
Log.error('Could not encrypt the message. Aborting and trying again.'); Log.error('Could not encrypt the message. Aborting and trying again.');
@ -338,7 +338,6 @@ Future<(Uint8List, Uint8List?)?> sendCipherText(
bool blocking = true, bool blocking = true,
String? messageId, String? messageId,
bool onlySendIfNoReceiptsAreOpen = false, bool onlySendIfNoReceiptsAreOpen = false,
bool useLock = true,
}) async { }) async {
if (onlySendIfNoReceiptsAreOpen) { if (onlySendIfNoReceiptsAreOpen) {
final openReceipts = await twonlyDB.receiptsDao.getReceiptCountForContact( final openReceipts = await twonlyDB.receiptsDao.getReceiptCountForContact(
@ -400,7 +399,6 @@ Future<(Uint8List, Uint8List?)?> sendCipherText(
receipt: receipt, receipt: receipt,
onlyReturnEncryptedData: onlyReturnEncryptedData, onlyReturnEncryptedData: onlyReturnEncryptedData,
blocking: blocking, blocking: blocking,
useLock: useLock,
); );
if (!blocking) { if (!blocking) {
return null; return null;

View file

@ -4,7 +4,6 @@ import 'dart:io';
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:hashlib/random.dart'; import 'package:hashlib/random.dart';
import 'package:mutex/mutex.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart';
@ -28,7 +27,7 @@ import 'package:twonly/src/services/api/client2client/reaction.c2c.dart';
import 'package:twonly/src/services/api/client2client/text_message.c2c.dart'; import 'package:twonly/src/services/api/client2client/text_message.c2c.dart';
import 'package:twonly/src/services/api/client2client/user_discovery.c2c.dart'; import 'package:twonly/src/services/api/client2client/user_discovery.c2c.dart';
import 'package:twonly/src/services/api/messages.api.dart'; import 'package:twonly/src/services/api/messages.api.dart';
import 'package:twonly/src/services/group.services.dart'; import 'package:twonly/src/services/group.service.dart';
import 'package:twonly/src/services/key_verification.service.dart'; import 'package:twonly/src/services/key_verification.service.dart';
import 'package:twonly/src/services/notifications/background.notifications.dart'; import 'package:twonly/src/services/notifications/background.notifications.dart';
import 'package:twonly/src/services/signal/encryption.signal.dart'; import 'package:twonly/src/services/signal/encryption.signal.dart';
@ -36,10 +35,7 @@ import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
final lockHandleServerMessage = Mutex();
Future<void> handleServerMessage(server.ServerToClient msg) async { Future<void> handleServerMessage(server.ServerToClient msg) async {
return lockHandleServerMessage.protect(() async {
Log.info('Processing a message from the server.'); Log.info('Processing a message from the server.');
/// Returns means, that the server can delete the message from the server. /// Returns means, that the server can delete the message from the server.
@ -77,7 +73,6 @@ Future<void> handleServerMessage(server.ServerToClient msg) async {
await apiService.sendResponse(ClientToServer()..v0 = v0); await apiService.sendResponse(ClientToServer()..v0 = v0);
AppState.gotMessageFromServer = true; AppState.gotMessageFromServer = true;
Log.info('Message from server proccessed.'); Log.info('Message from server proccessed.');
});
} }
DateTime lastPushKeyRequest = clock.now().subtract(const Duration(hours: 1)); DateTime lastPushKeyRequest = clock.now().subtract(const Duration(hours: 1));

View file

@ -1,6 +1,10 @@
import 'dart:convert';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
import 'package:twonly/core/bridge/wrapper/key_manager.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/secure_storage.keys.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart' import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart'
@ -14,6 +18,9 @@ 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/api/messages.api.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/services/signal/session.signal.dart'; import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/secure_storage.dart';
class Result<T, E> { class Result<T, E> {
Result.error(this.error) : value = null; Result.error(this.error) : value = null;
@ -106,3 +113,36 @@ Future<bool> importSignalContactAndCreateRequest(
return true; return true;
} }
Future<Map<String, String>?> getAuthenticationHeader() async {
var headers = <String, String>{};
if (userService.currentUser.canUseLoginTokenForAuth) {
final loginToken = await RustKeyManager.getLoginToken();
headers = {
'x-twonly-user-id': userService.currentUser.userId
.toRadixString(16)
.padLeft(16, '0')
.toUpperCase(),
'x-twonly-login-token': uint8ListToHex(loginToken),
};
} else {
final apiAuthTokenRaw = await SecureStorage.instance.read(
key: SecureStorageKeys.apiAuthToken,
);
if (apiAuthTokenRaw == null) {
Log.error('api auth token not defined.');
return null;
}
final apiAuthToken = uint8ListToHex(base64Decode(apiAuthTokenRaw));
headers = {
'x-twonly-auth-token': apiAuthToken,
};
}
return headers;
}

View file

@ -34,12 +34,15 @@ Future<void> initializeBackgroundTaskManager() async {
void callbackDispatcher() { void callbackDispatcher() {
Workmanager().executeTask((task, inputData) async { Workmanager().executeTask((task, inputData) async {
SentryWidgetsFlutterBinding.ensureInitialized(); SentryWidgetsFlutterBinding.ensureInitialized();
await AppEnvironment.init();
switch (task) { switch (task) {
case 'eu.twonly.periodic_task': case 'eu.twonly.periodic_task':
// if (await initBackgroundExecution()) { // if (await initBackgroundExecution()) {
// await handlePeriodicTask(); // await handlePeriodicTask();
// } // }
break;
case 'eu.twonly.processing_task': case 'eu.twonly.processing_task':
case _ when task.startsWith('progressing_finish_uploads_'):
if (await initBackgroundExecution()) { if (await initBackgroundExecution()) {
await handleProcessingTask(); await handleProcessingTask();
} }
@ -58,7 +61,6 @@ Future<bool> initBackgroundExecution() async {
return false; return false;
} }
await AppEnvironment.init();
AppState.isInBackgroundTask = true; AppState.isInBackgroundTask = true;
if (await StartupGuard.isAppStarting()) { if (await StartupGuard.isAppStarting()) {
@ -130,6 +132,7 @@ Future<void> handlePeriodicTask({int lastExecutionInSecondsLimit = 120}) async {
return; return;
} }
try {
while (!AppState.gotMessageFromServer) { while (!AppState.gotMessageFromServer) {
if (stopwatch.elapsed.inSeconds >= 15) { if (stopwatch.elapsed.inSeconds >= 15) {
Log.info('No new message from the server after 15 seconds.'); Log.info('No new message from the server after 15 seconds.');
@ -145,9 +148,10 @@ Future<void> handlePeriodicTask({int lastExecutionInSecondsLimit = 120}) async {
await finishStartedPreprocessing(); await finishStartedPreprocessing();
await Future.delayed(const Duration(milliseconds: 2000)); await Future.delayed(const Duration(milliseconds: 2000));
} finally {
await apiService.close(() {}); await apiService.close(() {});
stopwatch.stop(); stopwatch.stop();
}
Log.info('eu.twonly.periodic_task finished after ${stopwatch.elapsed}.'); Log.info('eu.twonly.periodic_task finished after ${stopwatch.elapsed}.');
return; return;

View file

@ -0,0 +1,363 @@
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:background_downloader/background_downloader.dart';
import 'package:clock/clock.dart' as clock;
import 'package:http/http.dart' as http;
import 'package:mutex/mutex.dart';
import 'package:twonly/core/bridge/wrapper/backup.dart';
import 'package:twonly/core/bridge/wrapper/key_manager.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/keyvalue.keys.dart';
import 'package:twonly/src/model/json/backup.model.dart';
import 'package:twonly/src/services/api/utils.api.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/keyvalue.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
class BackupService {
static final Mutex _protected = Mutex();
static String _getIdentityBackupUrl(String backupId) =>
'${apiService.apiEndpoint}/backup/identity/$backupId';
static String _getArchiveBackupUrl(String backupDownloadToken, int? userId) =>
'${apiService.apiEndpoint}/backup/archive/${userId == null ? '' : '${userId.toRadixString(16).padLeft(16, '0').toUpperCase()}/'}$backupDownloadToken';
static final _backupUpdateController = StreamController<void>.broadcast();
static Stream<void> get onBackupUpdated => _backupUpdateController.stream;
static Future<CurrentBackupStatus> getData() async {
return CurrentBackupStatus.fromJson(
(await KeyValueStore.get(KeyValueKeys.currentBackupState)) ??
CurrentBackupStatus().toJson(),
);
}
static Future<void> updateBackupPassword(String password) async {
// Set or reset the backup data...
await KeyValueStore.put(
KeyValueKeys.currentBackupState,
CurrentBackupStatus().toJson(),
);
_backupUpdateController.add(null);
await RustBackupIdentity.setBackupPasswordKeys(
password: password,
// Using the userId is this will never change in a users lifecycle
userId: userService.currentUser.userId,
);
await UserService.update((u) => u.isBackupEnabled = true);
unawaited(makeBackup(force: true));
}
static Future<void> handleBackupStatusUpdate(
String taskId,
TaskStatusUpdate update,
) async {
var status = LastBackupUploadState.success;
if (update.status == TaskStatus.failed ||
update.status == TaskStatus.canceled) {
status = LastBackupUploadState.failed;
} else if (update.status != TaskStatus.complete) {
Log.info('Backup is in state: ${update.status}');
return;
}
await _protected.protect(() async {
final backup = await getData();
if (taskId == 'backup_identity') {
backup
..identityLastSuccessFull = clock.clock.now()
..identityState = status;
} else {
backup
..archiveLastSuccessFull = clock.clock.now()
..archiveState = status;
}
await KeyValueStore.put(
KeyValueKeys.currentBackupState,
backup.toJson(),
);
_backupUpdateController.add(null);
});
}
static Future<void> makeBackup({bool force = false}) async {
await _protected.protect(() async {
final backup = await getData();
final lastDay = clock.clock.now().subtract(const Duration(days: 1));
final lastWeek = clock.clock.now().subtract(const Duration(days: 7));
if (force ||
backup.identityLastSuccessFull == null ||
(backup.identityState != LastBackupUploadState.pending &&
backup.identityLastSuccessFull!.isBefore(lastWeek) ||
backup.identityLastSuccessFull!.isBefore(
lastWeek.subtract(const Duration(days: 1)),
))) {
final backupId = await RustBackupIdentity.getBackupId();
if (backupId == null) {
Log.error('No backup password was set by the user.');
backup.identityState = LastBackupUploadState.failed;
} else {
Log.info('Performing a identity backup.');
final encryptedBackup =
await RustBackupIdentity.getIdentityBackupBytes();
final backupTempFile = File(
'${AppEnvironment.cacheDir}/identity_backup.bin',
)..writeAsBytesSync(encryptedBackup);
Log.info(
'Identity backup has a size of ${backupTempFile.statSync().size}.',
);
final task = UploadTask.fromFile(
taskId: 'backup_identity',
httpRequestMethod: 'PUT',
file: backupTempFile,
url: _getIdentityBackupUrl(backupId),
post: 'binary',
retries: 2,
headers: {
'Content-Type': 'application/octet-stream',
},
);
if (await FileDownloader().enqueue(task)) {
Log.info('Starting upload from backup identity.');
backup
..identityState = LastBackupUploadState.pending
..identityLastSuccessFull = clock.clock.now()
..identitySize = encryptedBackup.length;
await KeyValueStore.put(
KeyValueKeys.currentBackupState,
backup.toJson(),
);
_backupUpdateController.add(null);
} else {
Log.error('Error starting upload task for backup identity.');
}
}
}
if (force ||
backup.archiveLastSuccessFull == null ||
(backup.archiveState != LastBackupUploadState.pending &&
backup.archiveLastSuccessFull!.isBefore(lastDay) ||
backup.archiveLastSuccessFull!.isBefore(
lastDay.subtract(const Duration(days: 1)),
))) {
Log.info('Creating a archive backup.');
late final String backupArchive;
late final String backupDownloadToken;
try {
(backupDownloadToken, backupArchive) =
await RustBackupArchive.createBackupArchive();
} catch (e) {
Log.error(e);
return;
}
Log.info(
'Archive backup has a size of ${File(backupArchive).statSync().size}.',
);
final headers = await getAuthenticationHeader();
if (headers == null) {
Log.error('Auth headers are empty. Returning');
return;
}
final task = UploadTask.fromFile(
taskId: 'backup_archive',
file: File(backupArchive),
url: _getArchiveBackupUrl(backupDownloadToken, null),
priority: 0,
retries: 10,
headers: headers,
);
if (await FileDownloader().enqueue(task)) {
Log.info('Uploading backup archive.');
backup
..archiveState = LastBackupUploadState.pending
..archiveLastSuccessFull = clock.clock.now()
..archiveSize = File(backupArchive).statSync().size;
await KeyValueStore.put(
KeyValueKeys.currentBackupState,
backup.toJson(),
);
_backupUpdateController.add(null);
} else {
Log.error('Error starting upload task for backup archive.');
}
}
});
}
static Future<BackupRecovery?> getBackupRecoveryData() async {
final stateJson = await KeyValueStore.get(KeyValueKeys.backupRecoveryState);
if (stateJson == null) return null;
return BackupRecovery.fromJson(stateJson);
}
static Future<RecoveryError?> _nextBackupStage() async {
return _protected.protect(() async {
final recoveryData = await getBackupRecoveryData();
if (recoveryData == null) return null;
if (recoveryData.state == BackupRecoveryState.identityBackupStarted) {
// First start to download the identity to restore the KeyManager
final backupKeys = await RustBackupIdentity.getBackupPasswordKeys(
userId: recoveryData.userId,
password: recoveryData.password,
);
final backupId = uint8ListToHex(backupKeys.backupId);
final backupServerUrl = _getIdentityBackupUrl(backupId);
final (encryptedBytes, error) = await _downloadBackup(backupServerUrl);
if (error != null || encryptedBytes == null) {
Log.error(error);
return error;
}
Log.info('Restored identity.');
try {
await RustBackupIdentity.restoreIdentityBackup(
keys: backupKeys,
encryptedBytes: encryptedBytes,
);
recoveryData.state = BackupRecoveryState.archiveBackupStarted;
await KeyValueStore.put(
KeyValueKeys.backupRecoveryState,
recoveryData.toJson(),
);
_backupUpdateController.add(null);
} catch (e) {
Log.error(e);
return RecoveryError.unkownError;
}
}
if (recoveryData.state == BackupRecoveryState.archiveBackupStarted) {
// The KeyManager was restored sucessfully, restore the archive now.
try {
final downloadToken =
await RustBackupArchive.getBackupDownloadToken();
if (downloadToken == null) {
// identity was not restored correctly try this again.
recoveryData.state = BackupRecoveryState.identityBackupStarted;
await KeyValueStore.put(
KeyValueKeys.backupRecoveryState,
recoveryData.toJson(),
);
return RecoveryError.tryAgainLater;
}
final backupServerUrl = _getArchiveBackupUrl(
downloadToken,
recoveryData.userId,
);
final backupArchive = await _downloadBackup(backupServerUrl);
if (backupArchive.$2 != null || backupArchive.$1 == null) {
return backupArchive.$2;
}
final archiveFile = File('${AppEnvironment.cacheDir}/archive.bin')
..writeAsBytesSync(backupArchive.$1!);
await RustBackupArchive.restoreBackupArchive(
filePath: archiveFile.path,
);
await UserService.update((u) {
u.deviceId += 1;
});
await KeyValueStore.delete(
KeyValueKeys.backupRecoveryState,
);
} catch (e) {
Log.error(e);
return RecoveryError.unkownError;
}
}
return null;
});
}
static Future<RecoveryError?> tryToReinstallTheArchive() async {
final userId = await RustKeyManager.getUserId();
if (userId == null) return null;
final state = BackupRecovery(
username: '',
userId: userId,
password: '',
)..state = BackupRecoveryState.archiveBackupStarted;
await KeyValueStore.put(KeyValueKeys.backupRecoveryState, state.toJson());
return _nextBackupStage();
}
static Future<RecoveryError?> startFullBackupRecovery(
String username,
String password,
) async {
final userId = await apiService.getUserIdFromUsername(username);
if (userId == null) {
return RecoveryError.usernameNotValid;
}
final state = BackupRecovery(
username: username,
userId: userId,
password: password,
);
await deleteLocalUserData();
await KeyValueStore.put(KeyValueKeys.backupRecoveryState, state.toJson());
return _nextBackupStage();
}
static Future<(Uint8List?, RecoveryError?)> _downloadBackup(
String backupServerUrl,
) async {
late http.Response response;
try {
response = await http.get(
Uri.parse(backupServerUrl),
headers: {
HttpHeaders.acceptHeader: 'application/octet-stream',
},
);
} catch (e) {
Log.error('Error fetching backup: $e');
return (null, RecoveryError.noInternet);
}
Log.info('Backup downlaod status: ${response.statusCode}');
switch (response.statusCode) {
case 200:
return (response.bodyBytes, null);
case 404:
return (null, RecoveryError.passwordInvalid);
default:
return (null, RecoveryError.tryAgainLater);
}
}
}
enum RecoveryError {
usernameNotValid,
passwordInvalid,
tryAgainLater,
noInternet,
unkownError,
}

View file

@ -1,89 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'package:drift/drift.dart';
import 'package:hashlib/hashlib.dart';
import 'package:http/http.dart' as http;
import 'package:twonly/locator.dart';
import 'package:twonly/src/model/json/userdata.model.dart';
import 'package:twonly/src/services/backup/create.backup.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
Future<void> enableTwonlySafe(String password) async {
final (backupId, encryptionKey) = await getMasterKey(
password,
userService.currentUser.username,
);
await UserService.update((user) {
user.twonlySafeBackup = TwonlySafeBackup(
encryptionKey: encryptionKey,
backupId: backupId,
);
});
unawaited(performTwonlySafeBackup(force: true));
}
Future<void> removeTwonlySafeFromServer() async {
final serverUrl = getTwonlySafeBackupUrl();
if (serverUrl == null) {
Log.error('Could not remove twonly safe as serverUrl is null');
return;
}
try {
final response = await http.delete(
Uri.parse(serverUrl),
headers: {
'Content-Type': 'application/json', // Set the content type if needed
// Add any other headers if required
},
);
Log.info('Download deleted with: ${response.statusCode}');
} catch (e) {
Log.error('Could not connect upload the backup.');
}
}
Future<(Uint8List, Uint8List)> getMasterKey(
String password,
String username,
) async {
final List<int> passwordBytes = utf8.encode(password);
final List<int> saltBytes = utf8.encode(username);
// Values are derived from the Threema Whitepaper
// https://threema.com/assets/documents/cryptography_whitepaper.pdf
final scrypt = Scrypt(
cost: 65536,
salt: saltBytes,
);
final key = scrypt.convert(passwordBytes).bytes;
return (key.sublist(0, 32), key.sublist(32, 64));
}
String? getTwonlySafeBackupUrl() {
if (userService.currentUser.twonlySafeBackup == null) return null;
return getTwonlySafeBackupUrlFromServer(
userService.currentUser.twonlySafeBackup!.backupId,
userService.currentUser.backupServer,
);
}
String? getTwonlySafeBackupUrlFromServer(
List<int> backupId,
BackupServer? backupServer,
) {
var backupServerUrl = 'https://safe.twonly.eu/';
if (backupServer != null) {
backupServerUrl = backupServer.serverUrl;
}
final backupIdHex = uint8ListToHex(backupId).toLowerCase();
return '${backupServerUrl}backups/$backupIdHex';
}

View file

@ -1,238 +0,0 @@
// ignore_for_file: parameter_assignments
import 'dart:convert';
import 'dart:io';
import 'package:background_downloader/background_downloader.dart';
import 'package:clock/clock.dart';
import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart';
import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart';
import 'package:path/path.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/secure_storage.keys.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/json/userdata.model.dart';
import 'package:twonly/src/model/protobuf/client/generated/backup.pb.dart';
import 'package:twonly/src/services/backup/common.backup.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/secure_storage.dart';
Future<void> performTwonlySafeBackup({bool force = false}) async {
if (userService.currentUser.twonlySafeBackup == null) {
return;
}
if (userService.currentUser.twonlySafeBackup!.backupUploadState ==
LastBackupUploadState.pending) {
Log.warn('Backup upload is already pending.');
return;
}
final lastUpdateTime =
userService.currentUser.twonlySafeBackup!.lastBackupDone;
if (!force && lastUpdateTime != null) {
if (lastUpdateTime.isAfter(clock.now().subtract(const Duration(days: 1)))) {
return;
}
}
Log.info('Starting new twonly Backup!');
final backupDir = Directory(
join(AppEnvironment.supportDir, 'backup_twonly_safe/'),
);
await backupDir.create(recursive: true);
final backupDatabaseFile = File(join(backupDir.path, 'twonly.backup.sqlite'));
final backupDatabaseFileCleaned = File(
join(backupDir.path, 'twonly.backup.cleaned.sqlite'),
);
// copy database
final originalDatabase = File(
join(AppEnvironment.supportDir, 'twonly.sqlite'),
);
await originalDatabase.copy(backupDatabaseFile.path);
driftRuntimeOptions.dontWarnAboutMultipleDatabases = true;
final backupDB = TwonlyDB(
driftDatabase(
name: 'twonly.backup',
native: DriftNativeOptions(
databaseDirectory: () async {
return backupDir;
},
),
),
);
await backupDB.deleteDataForTwonlySafe();
await backupDB.customStatement('VACUUM INTO ?', [
backupDatabaseFileCleaned.path,
]);
await backupDB.printTableSizes();
await backupDB.close();
// ignore: inference_failure_on_collection_literal
final secureStorageBackup = {};
secureStorageBackup[SecureStorageKeys.signalIdentity] = await SecureStorage
.instance
.read(
key: SecureStorageKeys.signalIdentity,
);
secureStorageBackup[SecureStorageKeys.signalSignedPreKey] =
await SecureStorage.instance.read(
key: SecureStorageKeys.signalSignedPreKey,
);
final userBackup = await UserService.getUser();
if (userBackup == null) return;
// FILTER settings which should not be in the backup
userBackup
..twonlySafeBackup = null
..lastImageSend = null
..todaysImageCounter = null
..lastPlanBallance = ''
..additionalUserInvites = ''
..signalLastSignedPreKeyUpdated = null;
secureStorageBackup[SecureStorageKeys.userData] = jsonEncode(userBackup);
// Compress and convert backup data
final twonlyDatabaseBytes = await backupDatabaseFileCleaned.readAsBytes();
await backupDatabaseFile.delete();
await backupDatabaseFileCleaned.delete();
Log.info('twonlyDatabaseLength = ${twonlyDatabaseBytes.lengthInBytes}');
Log.info('secureStorageLength = ${jsonEncode(secureStorageBackup).length}');
final backupProto = TwonlySafeBackupContent(
secureStorageJson: jsonEncode(secureStorageBackup),
twonlyDatabase: twonlyDatabaseBytes,
);
final backupBytes = gzip.encode(backupProto.writeToBuffer());
final backupHash = uint8ListToHex((await Sha256().hash(backupBytes)).bytes);
if (userService.currentUser.twonlySafeBackup!.lastBackupDone == null ||
userService.currentUser.twonlySafeBackup!.lastBackupDone!.isAfter(
clock.now().subtract(const Duration(days: 90)),
)) {
force = true;
}
final lastHash = await SecureStorage.instance.read(
key: SecureStorageKeys.twonlySafeLastBackupHash,
);
if (lastHash != null && !force) {
if (backupHash == lastHash) {
Log.info('Since last backup nothing has changed.');
return;
}
}
await SecureStorage.instance.write(
key: SecureStorageKeys.twonlySafeLastBackupHash,
value: backupHash,
);
// Encrypt backup data
final chacha20 = FlutterChacha20.poly1305Aead();
final nonce = chacha20.newNonce();
final secretBox = await chacha20.encrypt(
backupBytes,
secretKey: SecretKey(
userService.currentUser.twonlySafeBackup!.encryptionKey,
),
nonce: nonce,
);
final encryptedBackupBytes = TwonlySafeBackupEncrypted(
mac: secretBox.mac.bytes,
nonce: nonce,
cipherText: secretBox.cipherText,
).writeToBuffer();
Log.info('Backup files created.');
final encryptedBackupBytesFile = File(
join(backupDir.path, 'twonly_safe.backup'),
);
await encryptedBackupBytesFile.writeAsBytes(encryptedBackupBytes);
Log.info(
'Create twonly Backup with a size of ${encryptedBackupBytes.length} bytes.',
);
if (userService.currentUser.backupServer != null) {
if (encryptedBackupBytes.length >
userService.currentUser.backupServer!.maxBackupBytes) {
Log.error('Backup is to big for the alternative backup server.');
await UserService.update((user) {
user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.failed;
});
return;
}
}
final task = UploadTask.fromFile(
taskId: 'backup',
file: encryptedBackupBytesFile,
httpRequestMethod: 'PUT',
url: getTwonlySafeBackupUrl()!,
post: 'binary',
retries: 2,
headers: {
'Content-Type': 'application/octet-stream',
},
);
if (await FileDownloader().enqueue(task)) {
Log.info('Starting upload from twonly Backup.');
await UserService.update((user) {
user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.pending;
user.twonlySafeBackup!.lastBackupDone = clock.now();
user.twonlySafeBackup!.lastBackupSize = encryptedBackupBytes.length;
});
} else {
Log.error('Error starting UploadTask for twonly Backup.');
}
}
Future<void> handleBackupStatusUpdate(TaskStatusUpdate update) async {
if (update.status == TaskStatus.failed ||
update.status == TaskStatus.canceled) {
await UserService.update((user) {
if (user.twonlySafeBackup != null) {
user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.failed;
}
});
} else if (update.status == TaskStatus.complete) {
Log.info(
'twonly Backup uploaded with status code ${update.responseStatusCode}',
);
await UserService.update((user) {
if (user.twonlySafeBackup != null) {
user.twonlySafeBackup!.backupUploadState =
LastBackupUploadState.success;
}
});
} else {
Log.info('Backup is in state: ${update.status}');
return;
}
}

View file

@ -1,118 +0,0 @@
// ignore_for_file: avoid_dynamic_calls
import 'dart:convert';
import 'dart:io';
import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart';
import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:drift/drift.dart';
import 'package:http/http.dart' as http;
import 'package:path/path.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/secure_storage.keys.dart';
import 'package:twonly/src/model/json/userdata.model.dart';
import 'package:twonly/src/model/protobuf/client/generated/backup.pb.dart';
import 'package:twonly/src/services/backup/common.backup.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/secure_storage.dart';
Future<void> recoverBackup(
String username,
String password,
BackupServer? server,
) async {
final (backupId, encryptionKey) = await getMasterKey(password, username);
final backupServerUrl = getTwonlySafeBackupUrlFromServer(backupId, server);
if (backupServerUrl == null) {
Log.error('Could not create backup url');
throw Exception('Could not create backup server url');
}
late Uint8List backupData;
late http.Response response;
try {
response = await http.get(
Uri.parse(backupServerUrl),
headers: {
HttpHeaders.acceptHeader: 'application/octet-stream',
},
);
} catch (e) {
Log.error('Error fetching backup: $e');
throw Exception('Backup server could not be reached. ($e)');
}
switch (response.statusCode) {
case 200:
backupData = response.bodyBytes;
case 400:
throw Exception('Bad Request: Validation failed.');
case 404:
throw Exception('No backup was found.');
case 429:
throw Exception('Too Many Requests: Rate limit reached.');
default:
throw Exception('Unexpected error: ${response.statusCode}');
}
return handleBackupData(encryptionKey, backupData);
}
Future<void> handleBackupData(
Uint8List encryptionKey,
Uint8List backupData,
) async {
final encryptedBackup = TwonlySafeBackupEncrypted.fromBuffer(
backupData,
);
final secretBox = SecretBox(
encryptedBackup.cipherText,
nonce: encryptedBackup.nonce,
mac: Mac(encryptedBackup.mac),
);
final compressedBytes = await FlutterChacha20.poly1305Aead().decrypt(
secretBox,
secretKey: SecretKeyData(encryptionKey),
);
final plaintextBytes = gzip.decode(compressedBytes);
final backupContent = TwonlySafeBackupContent.fromBuffer(
plaintextBytes,
);
final originalDatabase = File(
join(AppEnvironment.supportDir, 'twonly.sqlite'),
);
// in case there was only a secure storage error, do not replace the original database
if (!originalDatabase.existsSync()) {
await originalDatabase.writeAsBytes(backupContent.twonlyDatabase);
}
const storage = SecureStorage.instance;
final secureStorage = jsonDecode(backupContent.secureStorageJson);
await storage.write(
key: SecureStorageKeys.signalIdentity,
value: secureStorage[SecureStorageKeys.signalIdentity] as String,
);
await storage.write(
key: SecureStorageKeys.signalSignedPreKey,
value: secureStorage[SecureStorageKeys.signalSignedPreKey] as String,
);
await storage.write(
key: SecureStorageKeys.userData,
value: secureStorage[SecureStorageKeys.userData] as String,
);
await UserService.update((u) {
u.deviceId += 1;
});
}

View file

@ -96,7 +96,6 @@ Future<void> incFlameCounter(
final group = await twonlyDB.groupsDao.getGroup(groupId); final group = await twonlyDB.groupsDao.getGroup(groupId);
if (group == null) return; if (group == null) return;
if (group.isDirectChat) {
final contacts = await twonlyDB.groupsDao.getGroupContact( final contacts = await twonlyDB.groupsDao.getGroupContact(
group.groupId, group.groupId,
); );
@ -113,7 +112,6 @@ Future<void> incFlameCounter(
), ),
); );
} }
}
final totalMediaCounter = group.totalMediaCounter + 1; final totalMediaCounter = group.totalMediaCounter + 1;
var flameCounter = group.flameCounter; var flameCounter = group.flameCounter;

View file

@ -31,7 +31,7 @@ Future<void> createThumbnailsForVideo(
'It took ${stopwatch.elapsedMilliseconds}ms to create the thumbnail.', 'It took ${stopwatch.elapsedMilliseconds}ms to create the thumbnail.',
); );
} else { } else {
Log.error( Log.warn(
'Thumbnail creation failed for the video with exit code.', 'Thumbnail creation failed for the video with exit code.',
); );
} }

View file

@ -5,7 +5,7 @@ import 'dart:math';
import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'; import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart';
import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:path_provider/path_provider.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/constants/secure_storage.keys.dart'; import 'package:twonly/src/constants/secure_storage.keys.dart';
import 'package:twonly/src/localization/generated/app_localizations.dart'; import 'package:twonly/src/localization/generated/app_localizations.dart';
@ -266,8 +266,7 @@ Future<void> showLocalPushNotificationWithoutUserId(
} }
Future<String?> getAvatarIcon(int contactId) async { Future<String?> getAvatarIcon(int contactId) async {
final directory = await getApplicationCacheDirectory(); final avatarsDirectory = Directory('${AppEnvironment.cacheDir}/avatars');
final avatarsDirectory = Directory('${directory.path}/avatars');
final filePath = '${avatarsDirectory.path}/$contactId.png'; final filePath = '${avatarsDirectory.path}/$contactId.png';
final file = File(filePath); final file = File(filePath);
if (file.existsSync()) { if (file.existsSync()) {

View file

@ -6,13 +6,12 @@ import 'dart:io' show Platform;
import 'package:firebase_app_installations/firebase_app_installations.dart'; import 'package:firebase_app_installations/firebase_app_installations.dart';
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/locator.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/background/callback_dispatcher.background.dart';
import 'package:twonly/src/services/notifications/background.notifications.dart'; import 'package:twonly/src/services/notifications/background.notifications.dart';
import 'package:twonly/src/services/notifications/setup.notifications.dart';
import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
@ -21,11 +20,8 @@ import '../../../firebase_options.dart';
// see more here: https://firebase.google.com/docs/cloud-messaging/flutter/receive?hl=de // see more here: https://firebase.google.com/docs/cloud-messaging/flutter/receive?hl=de
Future<void> checkForTokenUpdates() async { Future<void> checkForTokenUpdates() async {
const storage = FlutterSecureStorage();
final storedToken = await storage.read(key: SecureStorageKeys.googleFcm);
try { try {
if (!userService.isUserCreated) return;
if (Platform.isIOS) { if (Platform.isIOS) {
var apnsToken = await FirebaseMessaging.instance.getAPNSToken(); var apnsToken = await FirebaseMessaging.instance.getAPNSToken();
for (var i = 0; i < 20; i++) { for (var i = 0; i < 20; i++) {
@ -47,23 +43,22 @@ Future<void> checkForTokenUpdates() async {
Log.info('Loaded FCM token.'); Log.info('Loaded FCM token.');
if (storedToken == null || fcmToken != storedToken) { if (userService.currentUser.fcmToken == null ||
Log.info('Got new FCM TOKEN.'); fcmToken != userService.currentUser.fcmToken) {
await storage.write(key: SecureStorageKeys.googleFcm, value: fcmToken); Log.info('Got new FCM token.');
await UserService.update((u) { await UserService.update((u) {
u.updateFCMToken = true; u
..updateFCMToken = true
..fcmToken = fcmToken;
}); });
} }
FirebaseMessaging.instance.onTokenRefresh FirebaseMessaging.instance.onTokenRefresh
.listen((fcmToken) async { .listen((fcmToken) async {
Log.info('Got new FCM TOKEN.');
await storage.write(
key: SecureStorageKeys.googleFcm,
value: fcmToken,
);
await UserService.update((u) { await UserService.update((u) {
u.updateFCMToken = true; u
..updateFCMToken = true
..fcmToken = fcmToken;
}); });
}) })
.onError((err) { .onError((err) {
@ -75,11 +70,16 @@ Future<void> checkForTokenUpdates() async {
} }
Future<void> initFCMAfterAuthenticated({bool force = false}) async { Future<void> initFCMAfterAuthenticated({bool force = false}) async {
final fcmToken = userService.currentUser.fcmToken;
if (userService.currentUser.updateFCMToken || force) { if (userService.currentUser.updateFCMToken || force) {
const storage = FlutterSecureStorage(); if (fcmToken == null) {
final storedToken = await storage.read(key: SecureStorageKeys.googleFcm); Log.error('FCM token could not be updated as it is empty');
if (storedToken != null) { await checkForTokenUpdates();
final res = await apiService.updateFCMToken(storedToken); return;
}
final res = await apiService.updateFCMToken(
fcmToken,
);
if (res.isSuccess) { if (res.isSuccess) {
Log.info('Uploaded new FCM token!'); Log.info('Uploaded new FCM token!');
await UserService.update((u) { await UserService.update((u) {
@ -88,9 +88,6 @@ Future<void> initFCMAfterAuthenticated({bool force = false}) async {
} else { } else {
Log.error('Could not update FCM token!'); Log.error('Could not update FCM token!');
} }
} else {
Log.error('Could not send FCM update to server as token is empty.');
}
} }
} }
@ -99,7 +96,7 @@ Future<void> resetFCMTokens() async {
Log.info('Firebase Installation successfully deleted.'); Log.info('Firebase Installation successfully deleted.');
await FirebaseMessaging.instance.deleteToken(); await FirebaseMessaging.instance.deleteToken();
Log.info('Old FCM deleted.'); Log.info('Old FCM deleted.');
await const FlutterSecureStorage().delete(key: SecureStorageKeys.googleFcm); await UserService.update((u) => u.fcmToken = null);
await checkForTokenUpdates(); await checkForTokenUpdates();
await initFCMAfterAuthenticated(force: true); await initFCMAfterAuthenticated(force: true);
} }
@ -119,7 +116,9 @@ Future<void> initFCMService() async {
@pragma('vm:entry-point') @pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async { Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
SentryWidgetsFlutterBinding.ensureInitialized(); SentryWidgetsFlutterBinding.ensureInitialized();
await AppEnvironment.init();
final isInitialized = await initBackgroundExecution(); final isInitialized = await initBackgroundExecution();
await setupPushNotification();
Log.info('Handling a background message: ${message.messageId}'); Log.info('Handling a background message: ${message.messageId}');
await handleRemoteMessage(message); await handleRemoteMessage(message);

View file

@ -12,16 +12,12 @@ import 'package:twonly/src/utils/log.dart';
Future<CiphertextMessage?> signalEncryptMessage( Future<CiphertextMessage?> signalEncryptMessage(
int target, int target,
Uint8List plaintextContent, { Uint8List plaintextContent,
bool useLock = true, ) async {
}) async {
if (useLock) {
return lockingSignalProtocol.protect<CiphertextMessage?>(() async { return lockingSignalProtocol.protect<CiphertextMessage?>(() async {
return _signalEncryptMessage(target, plaintextContent); return _signalEncryptMessage(target, plaintextContent);
}); });
} }
return _signalEncryptMessage(target, plaintextContent);
}
Future<CiphertextMessage?> _signalEncryptMessage( Future<CiphertextMessage?> _signalEncryptMessage(
int target, int target,
@ -44,7 +40,9 @@ signalDecryptMessage(
Uint8List encryptedContentRaw, Uint8List encryptedContentRaw,
int type, int type,
) async { ) async {
return lockingSignalProtocol.protect(() async { // Hold the lock only for the cryptographic operation, not for network I/O
final (decryptedContent, errorType, needsResync) = await lockingSignalProtocol
.protect(() async {
try { try {
final session = SessionCipher.fromStore( final session = SessionCipher.fromStore(
(await getSignalStore())!, (await getSignalStore())!,
@ -64,28 +62,51 @@ signalDecryptMessage(
); );
default: default:
Log.error('Unknown Message Decryption Type: $type'); Log.error('Unknown Message Decryption Type: $type');
return (null, PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN); return (
null,
PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN,
false,
);
} }
return (EncryptedContent.fromBuffer(plaintext), null); return (EncryptedContent.fromBuffer(plaintext), null, false);
} on InvalidKeyIdException catch (e) { } on InvalidKeyIdException catch (e) {
Log.warn(e); Log.warn(e);
return ( return (
null, null,
PlaintextContent_DecryptionErrorMessage_Type.PREKEY_UNKNOWN, PlaintextContent_DecryptionErrorMessage_Type.PREKEY_UNKNOWN,
false,
); );
} on DuplicateMessageException catch (e) { } on DuplicateMessageException catch (e) {
Log.info(e.toString()); Log.info(e.toString());
return (null, null); return (null, null, false);
} on InvalidMessageException catch (e) { } on InvalidMessageException catch (e) {
Log.warn(e); Log.warn(e);
if (!resyncedUsers.contains(fromUserId)) { return (
if (await handleSessionResync(fromUserId, useLock: false)) { null,
// This flag prevents from resyncing the session the client received multiple new PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN,
// messages from the server he could not decrypt true,
);
} catch (e) {
Log.error(e);
return (
null,
PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN,
false,
);
}
});
// Handle session resync OUTSIDE the lock to avoid holding it during
// network round-trips (which can block for up to 60 seconds)
if (needsResync && !resyncedUsers.contains(fromUserId)) {
if (await handleSessionResync(fromUserId)) {
// This flag prevents from resyncing the session the client received
// multiple new messages from the server he could not decrypt
resyncedUsers.add(fromUserId); resyncedUsers.add(fromUserId);
// This message contains a new PreKeyBundle establishing a new signal session // This message contains a new PreKeyBundle establishing a new signal
// session
await sendCipherText( await sendCipherText(
fromUserId, fromUserId,
EncryptedContent( EncryptedContent(
@ -93,14 +114,9 @@ signalDecryptMessage(
type: EncryptedContent_ErrorMessages_Type.SESSION_OUT_OF_SYNC, type: EncryptedContent_ErrorMessages_Type.SESSION_OUT_OF_SYNC,
), ),
), ),
useLock: false,
); );
} }
} }
return (null, PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN);
} catch (e) { return (decryptedContent, errorType);
Log.error(e);
return (null, PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN);
}
});
} }

View file

@ -1,30 +1,21 @@
import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/core/bridge/wrapper/key_manager.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/secure_storage.keys.dart';
import 'package:twonly/src/database/signal/signal_protocol_store.dart';
import 'package:twonly/src/model/json/signal_identity.model.dart'; import 'package:twonly/src/model/json/signal_identity.model.dart';
import 'package:twonly/src/services/signal/consts.signal.dart'; import 'package:twonly/src/services/signal/consts.signal.dart';
import 'package:twonly/src/services/signal/protocol_state.signal.dart'; import 'package:twonly/src/services/signal/protocol_state.signal.dart';
import 'package:twonly/src/services/signal/utils.signal.dart'; import 'package:twonly/src/services/signal/utils.signal.dart';
import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/secure_storage.dart';
Future<IdentityKeyPair?> getSignalIdentityKeyPair() async { class SignalIdentityService {
final signalIdentity = await getSignalIdentity(); static Future<void> onAuthenticated() async {
if (signalIdentity == null) return null;
return IdentityKeyPair.fromSerialized(signalIdentity.identityKeyPairU8List);
}
// This function runs after the clients authenticated with the server.
// It then checks if it should update a new session key
Future<void> signalHandleNewServerConnection() async {
if (userService.currentUser.signalLastSignedPreKeyUpdated != null) { if (userService.currentUser.signalLastSignedPreKeyUpdated != null) {
final fortyEightHoursAgo = clock.now().subtract(const Duration(hours: 48)); final fortyEightHoursAgo = clock.now().subtract(
const Duration(hours: 48),
);
final isYoungerThan48Hours = final isYoungerThan48Hours =
(userService.currentUser.signalLastSignedPreKeyUpdated!).isAfter( (userService.currentUser.signalLastSignedPreKeyUpdated!).isAfter(
fortyEightHoursAgo, fortyEightHoursAgo,
@ -56,6 +47,7 @@ Future<void> signalHandleNewServerConnection() async {
Log.info('updated signed pre key'); Log.info('updated signed pre key');
} }
} }
}
Future<List<PreKeyRecord>> signalGetPreKeys() async { Future<List<PreKeyRecord>> signalGetPreKeys() async {
return lockingSignalProtocol.protect(() async { return lockingSignalProtocol.protect(() async {
@ -75,64 +67,45 @@ Future<List<PreKeyRecord>> signalGetPreKeys() async {
Future<SignalIdentity?> getSignalIdentity() async { Future<SignalIdentity?> getSignalIdentity() async {
try { try {
var signalIdentityJson = await SecureStorage.instance.read( final identity = await RustKeyManager.getSignalIdentity();
key: SecureStorageKeys.signalIdentity, return SignalIdentity(
identityKeyPairU8List: identity.$1,
registrationId: identity.$2,
); );
if (signalIdentityJson == null) {
return null;
}
final decoded = jsonDecode(signalIdentityJson);
signalIdentityJson = null;
return SignalIdentity.fromJson(decoded as Map<String, dynamic>);
} catch (e) { } catch (e) {
Log.error('could not load signal identity: $e'); Log.error('could not load signal identity: $e');
return null; return null;
} }
} }
Future<IdentityKeyPair?> getSignalIdentityKeyPair() async {
final signalIdentity = await getSignalIdentity();
if (signalIdentity == null) return null;
return IdentityKeyPair.fromSerialized(signalIdentity.identityKeyPairU8List);
}
Future<Uint8List> getUserPublicKey() async { Future<Uint8List> getUserPublicKey() async {
Log.info('getUserPublicKey: getting identity');
final signalIdentity = (await getSignalIdentity())!; final signalIdentity = (await getSignalIdentity())!;
Log.info('getUserPublicKey: getting signal store');
final signalStore = await getSignalStoreFromIdentity(signalIdentity); final signalStore = await getSignalStoreFromIdentity(signalIdentity);
Log.info('getUserPublicKey: getting key pair');
final keyPair = await signalStore.getIdentityKeyPair(); final keyPair = await signalStore.getIdentityKeyPair();
Log.info('getUserPublicKey: serializing public key');
return keyPair.getPublicKey().serialize(); return keyPair.getPublicKey().serialize();
} }
Future<void> createIfNotExistsSignalIdentity() async { Future<void> createIfNotExistsSignalIdentity() async {
final signalIdentity = await SecureStorage.instance.read( // check if identity already exists
key: SecureStorageKeys.signalIdentity, if (await getSignalIdentity() != null) return;
);
if (signalIdentity != null) {
return;
}
final identityKeyPair = generateIdentityKeyPair(); final identityKeyPair = generateIdentityKeyPair();
final registrationId = generateRegistrationId(true); final registrationId = generateRegistrationId(true);
final signalStore = SignalSignalProtocolStore(
identityKeyPair,
registrationId,
);
final signedPreKey = generateSignedPreKey(identityKeyPair, defaultDeviceId); final signedPreKey = generateSignedPreKey(identityKeyPair, defaultDeviceId);
final signedPreKeyStore = <int, Uint8List>{};
signedPreKeyStore[signedPreKey.id] = signedPreKey.serialize();
await signalStore.signedPreKeyStore.storeSignedPreKey( await RustKeyManager.importSignalIdentity(
signedPreKey.id, identityKeyPairStructure: identityKeyPair.serialize(),
signedPreKey,
);
final storedSignalIdentity = SignalIdentity(
identityKeyPairU8List: identityKeyPair.serialize(),
registrationId: registrationId, registrationId: registrationId,
); signedPreKeyStore: signedPreKeyStore,
await SecureStorage.instance.write(
key: SecureStorageKeys.signalIdentity,
value: jsonEncode(storedSignalIdentity),
); );
} }

View file

@ -8,17 +8,11 @@ import 'package:twonly/src/services/signal/protocol_state.signal.dart';
import 'package:twonly/src/services/signal/utils.signal.dart'; import 'package:twonly/src/services/signal/utils.signal.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
Future<bool> processSignalUserData( Future<bool> processSignalUserData(Response_UserData userData) async {
Response_UserData userData, {
bool useLock = true,
}) async {
if (useLock) {
return lockingSignalProtocol.protect(() async { return lockingSignalProtocol.protect(() async {
return _processSignalUserData(userData); return _processSignalUserData(userData);
}); });
} }
return _processSignalUserData(userData);
}
Future<bool> _processSignalUserData(Response_UserData userData) async { Future<bool> _processSignalUserData(Response_UserData userData) async {
final SignalProtocolStore? signalStore = await getSignalStore(); final SignalProtocolStore? signalStore = await getSignalStore();
@ -106,14 +100,11 @@ Future<Uint8List?> getPublicKeyFromContact(int contactId) async {
} }
} }
Future<bool> handleSessionResync( Future<bool> handleSessionResync(int fromUserId) async {
int fromUserId, {
bool useLock = true,
}) async {
final userData = await apiService.getUserById(fromUserId); final userData = await apiService.getUserById(fromUserId);
if (userData != null) { if (userData != null) {
Log.info('Got new session data from the server to re-sync the session'); Log.info('Got new session data from the server to re-sync the session');
return processSignalUserData(userData, useLock: useLock); return processSignalUserData(userData);
} }
Log.info('Could not download userdata from the server.'); Log.info('Could not download userdata from the server.');
return false; return false;

View file

@ -2,9 +2,11 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
import 'package:twonly/core/bridge/wrapper/key_manager.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/secure_storage.keys.dart'; import 'package:twonly/src/constants/secure_storage.keys.dart';
import 'package:twonly/src/model/json/userdata.model.dart'; import 'package:twonly/src/model/json/userdata.model.dart';
import 'package:twonly/src/utils/keyvalue.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/secure_storage.dart'; import 'package:twonly/src/utils/secure_storage.dart';
@ -26,21 +28,49 @@ class UserService {
static Future<UserData?> getUser() async { static Future<UserData?> getUser() async {
try { try {
// 1. Try to load from KeyValueStore (user.json)
final userDataMap = await KeyValueStore.get('user');
if (userDataMap != null) {
final userData = UserData.fromJson(userDataMap);
await RustKeyManager.setUserId(userId: userData.userId);
return userData;
}
// 2. If not found, try to load from SecureStorage (Migration path)
final userDataJson = await SecureStorage.instance.read( final userDataJson = await SecureStorage.instance.read(
key: SecureStorageKeys.userData, key: SecureStorageKeys.userData,
); );
if (userDataJson == null) {
return null; if (userDataJson != null) {
} final userData = UserData.fromJson(
return UserData.fromJson(
jsonDecode(userDataJson) as Map<String, dynamic>, jsonDecode(userDataJson) as Map<String, dynamic>,
); );
// 3. Run migration
await _migrateFromSecureStorage(userData);
return userData;
}
return null;
} catch (e) { } catch (e) {
Log.error('could not load user: $e'); Log.error('could not load user: $e');
rethrow; // Rethrow instead of returning null to distinguish error from missing user rethrow;
} }
} }
static Future<void> _migrateFromSecureStorage(UserData userData) async {
// Currently empty migration logic as requested, but we MUST store the data
await KeyValueStore.put('user', userData.toJson());
try {
await RustKeyManager.setUserId(userId: userData.userId);
} catch (e) {
Log.error('Could not set userId in RustKeyManager during migration: $e');
}
// Optional: Log migration
Log.info('Migrated user data from SecureStorage to KeyValueStore');
}
static Future<void> update( static Future<void> update(
void Function(UserData userData) updateUser, void Function(UserData userData) updateUser,
) async { ) async {
@ -53,10 +83,7 @@ class UserService {
user.defaultShowTime = null; user.defaultShowTime = null;
} }
updateUser(user); updateUser(user);
await SecureStorage.instance.write( await KeyValueStore.put('user', user.toJson());
key: SecureStorageKeys.userData,
value: jsonEncode(user),
);
userService.currentUser = user; userService.currentUser = user;
} catch (e) { } catch (e) {
Log.error('Could not update the user: $e'); Log.error('Could not update the user: $e');
@ -66,6 +93,16 @@ class UserService {
userService.triggerUserUpdate(); userService.triggerUserUpdate();
} }
static Future<void> save(UserData user) async {
await KeyValueStore.put('user', user.toJson());
try {
await RustKeyManager.setUserId(userId: user.userId);
} catch (e) {
Log.error('Could not set userId in RustKeyManager during save: $e');
}
await userService.tryInit();
}
void triggerUserUpdate() { void triggerUserUpdate() {
_userDataUpdateController.add(null); _userDataUpdateController.add(null);
} }

View file

@ -1,8 +1,11 @@
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:twonly/core/bridge/wrapper/user_discovery.dart'; import 'package:twonly/core/bridge/wrapper/user_discovery.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/user_discovery/types.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/user_discovery/types.pb.dart';
@ -177,7 +180,7 @@ class UserDiscoveryService {
} }
} }
static Future<void> removeDeletedContacts() async { static Future<void> _removeDeletedContacts() async {
final subquery = twonlyDB.selectOnly(twonlyDB.contacts) final subquery = twonlyDB.selectOnly(twonlyDB.contacts)
..addColumns([twonlyDB.contacts.userId]) ..addColumns([twonlyDB.contacts.userId])
..where(twonlyDB.contacts.accountDeleted.equals(true)); ..where(twonlyDB.contacts.accountDeleted.equals(true));
@ -216,4 +219,35 @@ class UserDiscoveryService {
u.isUserDiscoveryEnabled = false; u.isUserDiscoveryEnabled = false;
}); });
} }
static Future<void> verifyInitializationOnStartup() async {
await _removeDeletedContacts();
final configExists = File(
'${AppEnvironment.supportDir}/user_discovery_config.json',
).existsSync();
final hasShares = await (twonlyDB.select(
twonlyDB.userDiscoveryShares,
)..limit(1)).get().then((list) => list.isNotEmpty);
if (userService.currentUser.isUserDiscoveryEnabled &&
(userService.currentUser.userDiscoveryInitializationError ||
!configExists ||
!hasShares)) {
unawaited(() async {
try {
Log.info(
'Retrying UserDiscovery initialization on startup (configExists: $configExists, hasShares: $hasShares)',
);
await initializeOrUpdate(
threshold: userService.currentUser.userDiscoveryThreshold,
sharePromotion: userService.currentUser.userDiscoverySharePromotion,
);
} catch (e) {
Log.error(
'Failed to retry UserDiscovery initialization on startup: $e',
);
}
}());
}
}
} }

View file

@ -7,7 +7,11 @@ import 'package:twonly/src/utils/exclusive_access.utils.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
class KeyValueStore { class KeyValueStore {
static final Mutex _mutex = Mutex(); static final Map<String, Mutex> _mutexes = {};
static Mutex _getMutex(String key) {
return _mutexes.putIfAbsent(key, Mutex.new);
}
static Future<File> _getFilePath(String key) async { static Future<File> _getFilePath(String key) async {
return File('${AppEnvironment.supportDir}/keyvalue/$key.json'); return File('${AppEnvironment.supportDir}/keyvalue/$key.json');
@ -16,7 +20,7 @@ class KeyValueStore {
static Future<T> _exclusive<T>(String key, Future<T> Function() action) { static Future<T> _exclusive<T>(String key, Future<T> Function() action) {
return exclusiveAccess( return exclusiveAccess(
lockName: 'keyvalue-$key', lockName: 'keyvalue-$key',
mutex: _mutex, mutex: _getMutex(key),
action: action, action: action,
); );
} }
@ -32,8 +36,8 @@ class KeyValueStore {
} }
}); });
static Future<Map<String, dynamic>?> get(String key) => static Future<Map<String, dynamic>?> get(String key) async {
_exclusive(key, () async { return _exclusive(key, () async {
final file = await _getFilePath(key); final file = await _getFilePath(key);
try { try {
if (file.existsSync()) { if (file.existsSync()) {
@ -48,9 +52,10 @@ class KeyValueStore {
return null; return null;
} }
}); });
}
static Future<void> put(String key, Map<String, dynamic> value) => static Future<void> put(String key, Map<String, dynamic> value) async {
_exclusive(key, () async { return _exclusive(key, () async {
try { try {
final file = await _getFilePath(key); final file = await _getFilePath(key);
await file.parent.create(recursive: true); await file.parent.create(recursive: true);
@ -60,3 +65,4 @@ class KeyValueStore {
} }
}); });
} }
}

View file

@ -29,7 +29,10 @@ class Log {
static String filterLogMessage(String msg) { static String filterLogMessage(String msg) {
if (msg.contains('SqliteException')) { if (msg.contains('SqliteException')) {
// Do not log data which would be inserted into the DB. // Do not log data which would be inserted into the DB.
return msg.substring(0, msg.indexOf('parameters: ')); final paramIndex = msg.indexOf('parameters: ');
if (paramIndex != -1) {
return msg.substring(0, paramIndex);
}
} }
return msg; return msg;
} }

View file

@ -197,12 +197,12 @@ String formatDateTime(BuildContext context, DateTime? dateTime) {
} }
} }
String formatBytes(int bytes, {int decimalPlaces = 2}) { String formatBytes(int bytes) {
if (bytes <= 0) return '0 Bytes'; if (bytes <= 0) return '0 Bytes';
const units = <String>['Bytes', 'KB', 'MB', 'GB', 'TB']; const units = <String>['Bytes', 'KB', 'MB', 'GB', 'TB'];
final unitIndex = (log(bytes) / log(1000)).floor(); final unitIndex = (log(bytes) / log(1000)).floor();
final formattedSize = bytes / pow(1000, unitIndex); final formattedSize = bytes / pow(1000, unitIndex);
return '${formattedSize.toStringAsFixed(decimalPlaces)} ${units[unitIndex]}'; return '${formattedSize.ceil()} ${units[unitIndex]}';
} }
bool isUUIDNewer(String uuid1, String uuid2) { bool isUUIDNewer(String uuid1, String uuid2) {

View file

@ -9,6 +9,9 @@ RegExp emojiRegex() => RegExp(
); );
bool isOneEmoji(String character) { bool isOneEmoji(String character) {
if (EmojiAnimationComp.animatedIcons.containsKey(character)) {
return true;
}
final matches = emojiRegex().allMatches(character); final matches = emojiRegex().allMatches(character);
if (matches.length == 1) { if (matches.length == 1) {
final match = matches.first; final match = matches.first;
@ -82,6 +85,7 @@ class EmojiAnimationComp extends StatelessWidget {
'😴': 'sleep.lottie', '😴': 'sleep.lottie',
'🤒': 'thermometer-face.lottie', '🤒': 'thermometer-face.lottie',
'🤕': 'bandage-face.lottie', '🤕': 'bandage-face.lottie',
'🫪': 'distorted_face.json',
'🤥': 'liar.lottie', '🤥': 'liar.lottie',
'😇': 'halo.lottie', '😇': 'halo.lottie',
'🤠': 'cowboy.lottie', '🤠': 'cowboy.lottie',

View file

@ -9,7 +9,7 @@ import 'package:twonly/src/database/tables/groups.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/messages.api.dart'; import 'package:twonly/src/services/api/messages.api.dart';
import 'package:twonly/src/services/group.services.dart'; import 'package:twonly/src/services/group.service.dart';
import 'package:twonly/src/utils/misc.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/better_list_title.element.dart';
import 'package:twonly/src/visual/views/groups/group.view.dart'; import 'package:twonly/src/visual/views/groups/group.view.dart';

View file

@ -0,0 +1,258 @@
import 'dart:async';
import 'package:flutter/material.dart';
enum SnackbarLevel {
info,
success,
warning,
error,
}
void showSnackbar(
BuildContext context,
String message, {
SnackbarLevel level = SnackbarLevel.error,
}) {
Color backgroundColor;
IconData iconData;
switch (level) {
case SnackbarLevel.info:
backgroundColor = Colors.blue.shade700;
iconData = Icons.info_outline;
case SnackbarLevel.success:
backgroundColor = Colors.green.shade700;
iconData = Icons.check_circle_outline;
case SnackbarLevel.warning:
backgroundColor = Colors.orange.shade800;
iconData = Icons.warning_amber_rounded;
case SnackbarLevel.error:
backgroundColor = Colors.red.shade700;
iconData = Icons.error_outline;
}
AnimationController? localAnimationController;
_showOverlay(
context: context,
animationDuration: const Duration(milliseconds: 1000),
reverseAnimationDuration: const Duration(milliseconds: 350),
displayDuration: const Duration(milliseconds: 3000),
onAnimationControllerInit: (controller) =>
localAnimationController = controller,
child: _SnackbarWidget(
message: message,
backgroundColor: backgroundColor,
icon: Icon(iconData, color: Colors.white, size: 28),
onCloseClick: () {
localAnimationController?.reverse();
},
),
);
}
OverlayEntry? _previousEntry;
void _showOverlay({
required BuildContext context,
required Widget child,
required Duration animationDuration,
required Duration reverseAnimationDuration,
required Duration displayDuration,
required void Function(AnimationController) onAnimationControllerInit,
}) {
final overlayState = Overlay.maybeOf(context);
if (overlayState == null) return;
late OverlayEntry overlayEntry;
overlayEntry = OverlayEntry(
builder: (_) => _AnimatedSnackbar(
animationDuration: animationDuration,
reverseAnimationDuration: reverseAnimationDuration,
displayDuration: displayDuration,
onAnimationControllerInit: onAnimationControllerInit,
onDismissed: () {
if (overlayEntry.mounted) {
overlayEntry.remove();
}
if (_previousEntry == overlayEntry) {
_previousEntry = null;
}
},
child: child,
),
);
if (_previousEntry != null && _previousEntry!.mounted) {
_previousEntry?.remove();
}
overlayState.insert(overlayEntry);
_previousEntry = overlayEntry;
}
class _SnackbarWidget extends StatelessWidget {
const _SnackbarWidget({
required this.message,
required this.backgroundColor,
required this.icon,
required this.onCloseClick,
});
final String message;
final Color backgroundColor;
final Icon icon;
final VoidCallback onCloseClick;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
clipBehavior: Clip.hardEdge,
constraints: const BoxConstraints(minHeight: 70),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: const BorderRadius.all(Radius.circular(12)),
boxShadow: const [
BoxShadow(
color: Colors.black26,
spreadRadius: 1,
blurRadius: 30,
),
],
),
width: double.infinity,
child: Row(
children: [
const SizedBox(width: 16),
icon,
const SizedBox(width: 12),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Text(
message,
style: theme.textTheme.bodyMedium?.merge(
const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16,
color: Colors.white,
),
),
textAlign: TextAlign.start,
),
),
),
GestureDetector(
onTap: onCloseClick,
behavior: HitTestBehavior.opaque,
child: const Padding(
padding: EdgeInsets.all(16),
child: Icon(Icons.close, color: Colors.white70, size: 20),
),
),
],
),
);
}
}
class _AnimatedSnackbar extends StatefulWidget {
const _AnimatedSnackbar({
required this.child,
required this.onDismissed,
required this.animationDuration,
required this.reverseAnimationDuration,
required this.displayDuration,
required this.onAnimationControllerInit,
});
final Widget child;
final VoidCallback onDismissed;
final Duration animationDuration;
final Duration reverseAnimationDuration;
final Duration displayDuration;
final void Function(AnimationController) onAnimationControllerInit;
@override
State<_AnimatedSnackbar> createState() => _AnimatedSnackbarState();
}
class _AnimatedSnackbarState extends State<_AnimatedSnackbar>
with SingleTickerProviderStateMixin {
late final AnimationController _animationController;
late final Animation<Offset> _offsetAnimation;
Timer? _timer;
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: widget.animationDuration,
reverseDuration: widget.reverseAnimationDuration,
);
_animationController.addStatusListener(_handleAnimationStatus);
widget.onAnimationControllerInit(_animationController);
_offsetAnimation =
Tween<Offset>(
begin: const Offset(0, -1),
end: Offset.zero,
).animate(
CurvedAnimation(
parent: _animationController,
curve: Curves.elasticOut,
reverseCurve: Curves.linearToEaseOut,
),
);
_animationController.forward();
}
void _handleAnimationStatus(AnimationStatus status) {
if (status == AnimationStatus.completed) {
_timer = Timer(widget.displayDuration, () {
if (mounted) {
_animationController.reverse();
}
});
} else if (status == AnimationStatus.dismissed) {
_timer?.cancel();
widget.onDismissed();
}
}
@override
void dispose() {
_animationController.dispose();
_timer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Positioned(
top: 16,
left: 16,
right: 16,
child: SlideTransition(
position: _offsetAnimation,
child: SafeArea(
child: Dismissible(
key: UniqueKey(),
direction: DismissDirection.up,
dismissThresholds: const {DismissDirection.up: 0.2},
confirmDismiss: (_) async {
if (mounted) {
await _animationController.reverse();
}
return false;
},
child: widget.child,
),
),
),
);
}
}

View file

@ -0,0 +1,292 @@
import 'dart:async';
import 'dart:collection';
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/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
import 'package:twonly/src/visual/components/emoji_picker.bottom.dart';
import 'package:twonly/src/visual/components/flame_counter.comp.dart';
import 'package:twonly/src/visual/components/snackbar.dart';
import 'package:twonly/src/visual/decorations/input_text.decoration.dart';
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layer_data.dart';
class AddNewShortcutView extends StatefulWidget {
const AddNewShortcutView({this.shortcut, super.key});
final Shortcut? shortcut;
@override
State<AddNewShortcutView> createState() => _StartNewChatView();
}
class _StartNewChatView extends State<AddNewShortcutView> {
List<Group> _groups = [];
List<Group> _allGroups = [];
final TextEditingController _searchGroupName = TextEditingController();
late StreamSubscription<List<Group>> _groupSub;
final HashSet<String> _selectedGroups = HashSet();
String? shortcutEmoji;
@override
void initState() {
super.initState();
if (widget.shortcut != null) {
shortcutEmoji = widget.shortcut!.emoji;
twonlyDB.shortcutsDao.getShortcutMembers(widget.shortcut!.id).then((
members,
) {
if (mounted) {
setState(() {
for (final m in members) {
_selectedGroups.add(m.groupId);
}
});
}
});
}
final stream = twonlyDB.groupsDao.watchGroupsForChatList();
_groupSub = stream.listen((update) async {
update.sort(
(a, b) => a.groupName.compareTo(b.groupName),
);
setState(() {
_allGroups = update;
});
await filterUsers();
});
}
@override
void dispose() {
unawaited(_groupSub.cancel());
super.dispose();
}
Future<void> filterUsers() async {
if (_searchGroupName.value.text.isEmpty) {
setState(() {
_groups = _allGroups;
});
return;
}
final usersFiltered = _allGroups
.where(
(group) => group.groupName.toLowerCase().contains(
_searchGroupName.value.text.toLowerCase(),
),
)
.toList();
setState(() {
_groups = usersFiltered;
});
}
void toggleSelectedGroup(String groupId) {
if (!_selectedGroups.contains(groupId)) {
if (_selectedGroups.length > 256) {
showSnackbar(context, context.lang.groupSizeLimitError(256));
return;
}
_selectedGroups.add(groupId);
} else {
_selectedGroups.remove(groupId);
}
setState(() {});
}
Future<void> submitChanges() async {
try {
if (widget.shortcut != null) {
await twonlyDB.shortcutsDao.updateShortcut(
widget.shortcut!.id,
shortcutEmoji!,
);
await twonlyDB.shortcutsDao.deleteShortcutMembers(widget.shortcut!.id);
await twonlyDB.shortcutsDao.addShortcutMembers(
widget.shortcut!.id,
_selectedGroups.toList(),
);
} else {
await twonlyDB.shortcutsDao.createShortcut(
shortcutEmoji!,
);
final shortcutId = (await twonlyDB.shortcutsDao.getShortcutByEmoji(
shortcutEmoji!,
))!.id;
await twonlyDB.shortcutsDao.deleteShortcutMembers(shortcutId);
await twonlyDB.shortcutsDao.addShortcutMembers(
shortcutId,
_selectedGroups.toList(),
);
}
if (mounted) Navigator.pop(context);
} catch (e) {
Log.error(e);
if (mounted) {
showSnackbar(context, context.lang.errorEmojiUsedOrInvalid);
}
}
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: Scaffold(
appBar: AppBar(
title: Text(
widget.shortcut == null
? context.lang.createShortcut
: context.lang.editShortcut,
),
actions: [
if (widget.shortcut != null)
IconButton(
icon: const FaIcon(
FontAwesomeIcons.trashCan,
size: 18,
color: Colors.red,
),
onPressed: () async {
final confirm = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(context.lang.deleteShortcut),
content: Text(context.lang.deleteShortcutBody),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: Text(context.lang.cancel),
),
FilledButton(
onPressed: () => Navigator.pop(context, true),
child: Text(context.lang.delete),
),
],
),
);
if (confirm == true) {
await twonlyDB.shortcutsDao.deleteShortcut(
widget.shortcut!.id,
);
if (context.mounted) Navigator.pop(context);
}
},
),
TextButton(
onPressed: () async {
// ignore: inference_failure_on_function_invocation
final result = await showModalBottomSheet(
context: context,
backgroundColor: Colors.black,
builder: (context) => const EmojiPickerBottom(),
);
if (result is EmojiLayerData) {
setState(() {
shortcutEmoji = result.text;
});
}
},
child: Text(
shortcutEmoji ?? context.lang.selectEmoji,
style: TextStyle(
fontSize: shortcutEmoji == null ? 14 : 22,
),
),
),
const SizedBox(width: 8),
],
),
floatingActionButton: FilledButton.icon(
onPressed: (_selectedGroups.isEmpty || shortcutEmoji == null)
? null
: submitChanges,
label: Text(
widget.shortcut == null
? context.lang.createShortcut
: context.lang.updateShortcut,
),
icon: const FaIcon(FontAwesomeIcons.check),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.only(
bottom: 40,
left: 10,
top: 20,
right: 10,
),
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: TextField(
onChanged: (_) async {
await filterUsers();
},
controller: _searchGroupName,
decoration: getInputDecoration(
context,
context.lang.shareImageSearchAllContacts,
),
),
),
const SizedBox(height: 10),
Expanded(
child: ListView.builder(
restorationId: 'new_message_users_list',
itemCount: _groups.length,
itemBuilder: (context, i) {
final group = _groups[i];
return ListTile(
key: ValueKey(group.groupId),
title: Row(
children: [
Text(substringBy(group.groupName, 12)),
FlameCounterWidget(
groupId: group.groupId,
prefix: true,
),
],
),
leading: AvatarIcon(
group: group,
fontSize: 15,
),
trailing: Checkbox(
value: _selectedGroups.contains(group.groupId),
side: WidgetStateBorderSide.resolveWith(
(states) {
if (states.contains(WidgetState.selected)) {
return const BorderSide(width: 0);
}
return BorderSide(
color: Theme.of(context).colorScheme.outline,
);
},
),
onChanged: (value) {
toggleSelectedGroup(group.groupId);
},
),
onTap: () {
toggleSelectedGroup(group.groupId);
},
);
},
),
),
],
),
),
),
),
);
}
}

View file

@ -5,6 +5,7 @@ import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/qr.utils.dart'; import 'package:twonly/src/utils/qr.utils.dart';
import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
import 'package:twonly/src/visual/components/snackbar.dart';
import 'package:twonly/src/visual/views/camera/camera_preview_components/main_camera_controller.dart'; import 'package:twonly/src/visual/views/camera/camera_preview_components/main_camera_controller.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
@ -46,16 +47,19 @@ class CameraScannedOverlay extends StatelessWidget {
onTap: () async { onTap: () async {
c.isLoading = true; c.isLoading = true;
mainController.setState(); mainController.setState();
showSnackbar(
context,
context.lang.requestedUserToastText(c.profile.username),
level: SnackbarLevel.success,
);
if (await addNewContactFromPublicProfile(c.profile) && if (await addNewContactFromPublicProfile(c.profile) &&
context.mounted) { context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( // showSnackbar(
SnackBar( // context,
content: Text( // context.lang.requestedUserToastText(c.profile.username),
context.lang.requestedUserToastText(c.profile.username), // level: SnackbarLevel.success,
), // );
duration: const Duration(seconds: 8),
),
);
} }
}, },
child: Container( child: Container(

View file

@ -19,6 +19,7 @@ import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/snackbar.dart';
import 'package:twonly/src/visual/helpers/media_view_sizing.helper.dart'; import 'package:twonly/src/visual/helpers/media_view_sizing.helper.dart';
import 'package:twonly/src/visual/helpers/screenshot.helper.dart'; import 'package:twonly/src/visual/helpers/screenshot.helper.dart';
import 'package:twonly/src/visual/loader/three_rotating_dots.loader.dart'; import 'package:twonly/src/visual/loader/three_rotating_dots.loader.dart';
@ -254,14 +255,12 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
await File(picture.path).delete(); await File(picture.path).delete();
return imageBytes; return imageBytes;
} catch (e) { } catch (e) {
if (context.mounted) { if (mounted) {
// ignore: use_build_context_synchronously showSnackbar(
ScaffoldMessenger.of(context).showSnackBar( context,
SnackBar( 'Error loading picture: $e',
content: Text('Error loading picture: $e'),
duration: const Duration(seconds: 3),
),
); );
Log.error(e);
} }
return null; return null;
} }
@ -284,6 +283,8 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
await Future.delayed(const Duration(milliseconds: 1000)); await Future.delayed(const Duration(milliseconds: 1000));
} }
if (!mounted) return;
await mc.cameraController?.pausePreview(); await mc.cameraController?.pausePreview();
if (!mounted) { if (!mounted) {
return; return;
@ -342,6 +343,9 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
await _deInitVolumeControl(); await _deInitVolumeControl();
if (!mounted) return true; if (!mounted) return true;
// Cache active camera ID since ShareImageEditorView closes the camera and resets state parameters.
final initialCameraId = mc.selectedCameraDetails.cameraId;
final shouldReturn = final shouldReturn =
await Navigator.push( await Navigator.push(
context, context,
@ -382,7 +386,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
return true; return true;
} }
await mc.selectCamera( await mc.selectCamera(
mc.selectedCameraDetails.cameraId, initialCameraId,
false, false,
); );
return false; return false;
@ -606,17 +610,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
void _showCameraException(dynamic e) { void _showCameraException(dynamic e) {
Log.error('$e'); Log.error('$e');
try { if (mounted) showSnackbar(context, 'Error: $e');
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error: $e'),
duration: const Duration(seconds: 3),
),
);
}
// ignore: empty_catches
} catch (e) {}
} }
@override @override

View file

@ -3,6 +3,7 @@ import 'dart:io';
import 'package:camera/camera.dart'; import 'package:camera/camera.dart';
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:drift/drift.dart' show Value;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -17,6 +18,7 @@ import 'package:twonly/src/model/protobuf/client/generated/qr.pb.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/qr.utils.dart'; import 'package:twonly/src/utils/qr.utils.dart';
import 'package:twonly/src/visual/components/snackbar.dart';
import 'package:twonly/src/visual/helpers/screenshot.helper.dart'; import 'package:twonly/src/visual/helpers/screenshot.helper.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/camera_preview_controller_view.dart';
import 'package:twonly/src/visual/views/camera/camera_preview_components/face_filters.dart'; import 'package:twonly/src/visual/views/camera/camera_preview_components/face_filters.dart';
@ -83,6 +85,8 @@ class MainCameraController {
FaceFilterType _currentFilterType = FaceFilterType.none; FaceFilterType _currentFilterType = FaceFilterType.none;
FaceFilterType get currentFilterType => _currentFilterType; FaceFilterType get currentFilterType => _currentFilterType;
Future<void>? _pendingDisposal;
Future<void> closeCamera() async { Future<void> closeCamera() async {
contactsVerified = {}; contactsVerified = {};
scannedNewProfiles = {}; scannedNewProfiles = {};
@ -94,14 +98,18 @@ class MainCameraController {
final cameraControllerTemp = cameraController; final cameraControllerTemp = cameraController;
cameraController = null; cameraController = null;
// prevents: CameraException(Disposed CameraController, buildPreview() was called on a disposed CameraController.) // prevents: CameraException(Disposed CameraController, buildPreview() was called on a disposed CameraController.)
Future.delayed(const Duration(milliseconds: 100), () async { _pendingDisposal = Future.delayed(
const Duration(milliseconds: 100),
() async {
await cameraControllerTemp?.dispose(); await cameraControllerTemp?.dispose();
}); },
);
initCameraStarted = false; initCameraStarted = false;
selectedCameraDetails = SelectedCameraDetails(); selectedCameraDetails = SelectedCameraDetails();
} }
Future<void> selectCamera(int sCameraId, bool init) async { Future<void> selectCamera(int sCameraId, bool init) async {
await _pendingDisposal;
initCameraStarted = true; initCameraStarted = true;
if (AppEnvironment.cameras.isEmpty) { if (AppEnvironment.cameras.isEmpty) {
@ -136,7 +144,13 @@ class MainCameraController {
? ImageFormatGroup.nv21 ? ImageFormatGroup.nv21
: ImageFormatGroup.bgra8888, : ImageFormatGroup.bgra8888,
); );
try {
await cameraController?.initialize(); await cameraController?.initialize();
} catch (e) {
Log.error(e);
cameraController = null; // ensure uninitialized controller is not reused
return;
}
await cameraController?.startImageStream(_processCameraImage); await cameraController?.startImageStream(_processCameraImage);
await cameraController?.setZoomLevel(selectedCameraDetails.scaleFactor); await cameraController?.setZoomLevel(selectedCameraDetails.scaleFactor);
if (userService.currentUser.videoStabilizationEnabled && !kDebugMode) { if (userService.currentUser.videoStabilizationEnabled && !kDebugMode) {
@ -216,7 +230,7 @@ class MainCameraController {
(e.code == 'setFocusPointFailed' || e.code == 'setFocusModeFailed')) { (e.code == 'setFocusPointFailed' || e.code == 'setFocusModeFailed')) {
Log.info('Focus point or mode not supported on this device'); Log.info('Focus point or mode not supported on this device');
} else { } else {
Log.error(e); Log.warn(e);
} }
} }
@ -356,7 +370,14 @@ class MainCameraController {
if (res == null) continue; if (res == null) continue;
final (profile, contact, verificationOk) = res; final (profile, contact, verificationOk) = res;
if (contact == null) { if (contact?.blocked ?? false) {
await twonlyDB.contactsDao.updateContact(
contact!.userId,
const ContactsCompanion(blocked: Value(false)),
);
}
if (contact == null || contact.deletedByUser) {
if (scannedNewProfiles[profile.userId.toInt()] == null) { if (scannedNewProfiles[profile.userId.toInt()] == null) {
await HapticFeedback.heavyImpact(); await HapticFeedback.heavyImpact();
scannedNewProfiles[profile.userId.toInt()] = ScannedNewProfile( scannedNewProfiles[profile.userId.toInt()] = ScannedNewProfile(
@ -373,18 +394,14 @@ class MainCameraController {
); );
await HapticFeedback.heavyImpact(); await HapticFeedback.heavyImpact();
if (verificationOk) { final context = cameraPreviewKey.currentContext;
AppGlobalKeys.scaffoldMessengerKey.currentState?.showSnackBar( if (verificationOk && context != null && context.mounted) {
SnackBar( showSnackbar(
content: Text( context,
AppGlobalKeys.scaffoldMessengerKey.currentContext?.lang context.lang.verifiedPublicKey(
.verifiedPublicKey(
getContactDisplayName(contact), getContactDisplayName(contact),
) ??
'',
),
duration: const Duration(seconds: 6),
), ),
level: SnackbarLevel.success,
); );
} }
} }

View file

@ -77,10 +77,12 @@ class SaveToGalleryButtonState extends State<SaveToGalleryButton> {
await newService.storeMediaFile(); await newService.storeMediaFile();
} }
if (mounted) {
setState(() { setState(() {
_imageSaved = true; _imageSaved = true;
_imageSaving = false; _imageSaving = false;
}); });
}
}, },
child: Row( child: Row(
children: [ children: [

View file

@ -20,6 +20,7 @@ import 'package:twonly/src/visual/decorations/input_text.decoration.dart';
import 'package:twonly/src/visual/elements/headline.element.dart'; import 'package:twonly/src/visual/elements/headline.element.dart';
import 'package:twonly/src/visual/helpers/screenshot.helper.dart'; import 'package:twonly/src/visual/helpers/screenshot.helper.dart';
import 'package:twonly/src/visual/views/camera/share_image_contact_selection_components/best_friends_selector.dart'; import 'package:twonly/src/visual/views/camera/share_image_contact_selection_components/best_friends_selector.dart';
import 'package:twonly/src/visual/views/camera/share_image_contact_selection_components/shortcut_row.comp.dart';
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/background.layer.dart'; import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/background.layer.dart';
class ShareImageView extends StatefulWidget { class ShareImageView extends StatefulWidget {
@ -194,6 +195,11 @@ class _ShareImageView extends State<ShareImageView> {
), ),
), ),
), ),
const SizedBox(height: 10),
ShortcutRowComp(
selectedGroupIds: widget.selectedGroupIds,
updateSelectedGroupIds: updateSelectedGroupIds,
),
if (_pinnedContacts.isNotEmpty) const SizedBox(height: 10), if (_pinnedContacts.isNotEmpty) const SizedBox(height: 10),
BestFriendsSelector( BestFriendsSelector(
groups: _pinnedContacts, groups: _pinnedContacts,

View file

@ -0,0 +1,115 @@
import 'dart:async';
import 'dart:collection';
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';
import 'package:twonly/src/visual/views/camera/add_new_shortcut.view.dart';
class ShortcutRowComp extends StatefulWidget {
const ShortcutRowComp({
required this.selectedGroupIds,
required this.updateSelectedGroupIds,
super.key,
});
final HashSet<String> selectedGroupIds;
final void Function(String, bool) updateSelectedGroupIds;
@override
State<ShortcutRowComp> createState() => _ShortcutRowCompState();
}
class _ShortcutRowCompState extends State<ShortcutRowComp> {
List<Shortcut> _shortcuts = [];
late StreamSubscription<List<Shortcut>> shortcutSub;
@override
void initState() {
super.initState();
unawaited(initAsync());
}
Future<void> initAsync() async {
shortcutSub = twonlyDB.shortcutsDao.watchAllShortcuts().listen((shortcuts) {
if (_shortcuts.isEmpty) {
shortcuts.sort((a, b) => b.usageCounter.compareTo(a.usageCounter));
_shortcuts = shortcuts;
} else {
final map = {for (final s in shortcuts) s.id: s};
final updated = <Shortcut>[];
for (final old in _shortcuts) {
if (map.containsKey(old.id)) {
updated.add(map.remove(old.id)!);
}
}
updated.addAll(map.values);
_shortcuts = updated;
}
if (mounted) setState(() {});
});
}
@override
void dispose() {
unawaited(shortcutSub.cancel());
super.dispose();
}
Future<void> _openCreateDialog() async {
await context.navPush(const AddNewShortcutView());
}
Future<void> _applyShortcut(Shortcut shortcut) async {
await twonlyDB.shortcutsDao.incrementUsage(shortcut.id);
final members = await twonlyDB.shortcutsDao.getShortcutMembers(shortcut.id);
for (final groupId in widget.selectedGroupIds.toList()) {
widget.updateSelectedGroupIds(groupId, false);
}
for (final m in members) {
widget.updateSelectedGroupIds(m.groupId, true);
}
}
@override
Widget build(BuildContext context) {
return SizedBox(
height: 40,
child: ListView(
scrollDirection: Axis.horizontal,
children: [
Row(
children: [
ActionChip(
padding: EdgeInsets.zero,
onPressed: _openCreateDialog,
label: _shortcuts.isEmpty
? Text(
context.lang.createShortcut,
style: const TextStyle(fontSize: 9),
)
: const Icon(Icons.add_reaction_outlined, size: 20),
shape: const StadiumBorder(),
),
for (final shortcut in _shortcuts)
GestureDetector(
onLongPress: () {
context.navPush(AddNewShortcutView(shortcut: shortcut));
},
child: ActionChip(
padding: EdgeInsets.zero,
onPressed: () => _applyShortcut(shortcut),
label: Text(
shortcut.emoji,
style: const TextStyle(fontSize: 18),
),
shape: const StadiumBorder(),
),
),
],
),
],
),
);
}
}

View file

@ -5,7 +5,7 @@ import 'package:flutter/material.dart';
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layer_data.dart'; import 'package:twonly/src/visual/views/camera/share_image_editor_components/layer_data.dart';
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/filters/datetime_filter.dart'; import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/filters/datetime_filter.dart';
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/filters/image_filter.dart'; import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/filters/image_filter.dart';
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/filters/location_filter.dart'; import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/filters/stickers.dart';
/// Main layer /// Main layer
class FilterLayer extends StatefulWidget { class FilterLayer extends StatefulWidget {
@ -75,7 +75,6 @@ class _FilterLayerState extends State<FilterLayer> {
List<Widget> pages = [ List<Widget> pages = [
const FilterSkeleton(), const FilterSkeleton(),
const DateTimeFilter(), const DateTimeFilter(),
// const LocationFilter(),
const FilterSkeleton(), const FilterSkeleton(),
]; ];

View file

@ -1,164 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:clock/clock.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:path_provider/path_provider.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/filter.layer.dart';
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/filters/datetime_filter.dart';
class LocationFilter extends StatefulWidget {
const LocationFilter({super.key});
@override
State<LocationFilter> createState() => _LocationFilterState();
}
class _LocationFilterState extends State<LocationFilter> {
String? _imageUrl;
Response_Location? location;
@override
void initState() {
super.initState();
unawaited(initAsync());
}
Future<void> initAsync() async {
final res = await apiService.getCurrentLocation();
if (res.isSuccess) {
// ignore: avoid_dynamic_calls
location = res.value.location as Response_Location?;
await _searchForImage();
if (mounted) setState(() {});
}
}
Future<void> _searchForImage() async {
if (location == null) return;
final imageIndex = await getStickerIndex();
// Normalize the city and country for search
final normalizedCity = location!.city.toLowerCase().replaceAll(' ', '_');
final normalizedCountry = location!.county.toLowerCase();
// Search for the city first
for (final item in imageIndex) {
if (item.imageSrc.contains('/cities/$normalizedCountry/')) {
// Check if the item matches the normalized city
if (item.imageSrc.contains('$normalizedCity.')) {
if (item.imageSrc.startsWith('/api/')) {
_imageUrl = 'https://twonly.eu/${item.imageSrc}';
if (mounted) setState(() {});
}
return;
}
}
}
// If city not found, search for the country
if (_imageUrl == null) {
for (final item in imageIndex) {
if (item.imageSrc.contains('/countries/') &&
item.imageSrc.contains(normalizedCountry)) {
if (item.imageSrc.startsWith('/api/')) {
_imageUrl = 'https://twonly.eu/${item.imageSrc}';
if (mounted) setState(() {});
}
break;
}
}
}
}
@override
Widget build(BuildContext context) {
if (_imageUrl != null) {
return FilterSkeleton(
child: Positioned(
bottom: 0,
left: 40,
right: 40,
child: Center(
child: CachedNetworkImage(
imageUrl: _imageUrl!,
),
),
),
);
}
if (location != null) {
if (location!.county != '-') {
return FilterSkeleton(
child: Positioned(
bottom: 50,
left: 40,
child: Column(
children: [
FilterText(location!.city),
FilterText(location!.county),
],
),
),
);
}
}
return const DateTimeFilter(color: Colors.black);
}
}
class Sticker {
Sticker({required this.imageSrc, required this.source});
factory Sticker.fromJson(Map<String, dynamic> json) {
return Sticker(
imageSrc: json['imageSrc'] as String,
source: json['source'] as String? ?? '',
);
}
final String imageSrc;
final String source;
}
Future<List<Sticker>> getStickerIndex() async {
final directory = await getApplicationCacheDirectory();
final indexFile = File('${directory.path}/stickers.json');
var res = <Sticker>[];
if (indexFile.existsSync() && kReleaseMode) {
final lastModified = indexFile.lastModifiedSync();
final difference = clock.now().difference(lastModified);
final content = await indexFile.readAsString();
final jsonList = json.decode(content) as List;
res = jsonList
.map((json) => Sticker.fromJson(json as Map<String, dynamic>))
.toList();
if (difference.inHours < 2) {
return res;
}
}
try {
final response = await http.get(
Uri.parse('https://twonly.eu/api/sticker/stickers.json'),
);
if (response.statusCode == 200) {
await indexFile.writeAsString(response.body);
final jsonList = json.decode(response.body) as List;
return jsonList
.map((json) => Sticker.fromJson(json as Map<String, dynamic>))
.toList();
} else {
return res;
}
} catch (e) {
Log.error('$e');
return res;
}
}

View file

@ -0,0 +1,56 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:clock/clock.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:twonly/globals.dart';
import 'package:twonly/src/utils/log.dart';
class Sticker {
Sticker({required this.imageSrc, required this.source});
factory Sticker.fromJson(Map<String, dynamic> json) {
return Sticker(
imageSrc: json['imageSrc'] as String,
source: json['source'] as String? ?? '',
);
}
final String imageSrc;
final String source;
}
Future<List<Sticker>> getStickerIndex() async {
final indexFile = File('${AppEnvironment.cacheDir}/stickers.json');
var res = <Sticker>[];
if (indexFile.existsSync() && kReleaseMode) {
final lastModified = indexFile.lastModifiedSync();
final difference = clock.now().difference(lastModified);
final content = await indexFile.readAsString();
final jsonList = json.decode(content) as List;
res = jsonList
.map((json) => Sticker.fromJson(json as Map<String, dynamic>))
.toList();
if (difference.inHours < 2) {
return res;
}
}
try {
final response = await http.get(
Uri.parse('https://twonly.eu/api/sticker/stickers.json'),
);
if (response.statusCode == 200) {
await indexFile.writeAsString(response.body);
final jsonList = json.decode(response.body) as List;
return jsonList
.map((json) => Sticker.fromJson(json as Map<String, dynamic>))
.toList();
} else {
return res;
}
} catch (e) {
Log.error('$e');
return res;
}
}

View file

@ -21,6 +21,7 @@ import 'package:twonly/src/visual/themes/light.dart';
import 'package:twonly/src/visual/views/chats/chat_list_components/feedback_btn.comp.dart'; import 'package:twonly/src/visual/views/chats/chat_list_components/feedback_btn.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/group_list_item.comp.dart';
import 'package:twonly/src/visual/views/onboarding/setup/components/finish_setup.comp.dart'; import 'package:twonly/src/visual/views/onboarding/setup/components/finish_setup.comp.dart';
import 'package:twonly/src/visual/views/settings/backup/components/missing_backup_setup.comp.dart';
class ChatListView extends StatefulWidget { class ChatListView extends StatefulWidget {
const ChatListView({super.key}); const ChatListView({super.key});
@ -215,6 +216,7 @@ class _ChatListViewState extends State<ChatListView> {
child: Column( child: Column(
children: [ children: [
const FinishSetupComp(), const FinishSetupComp(),
const MissingBackupComp(),
if (_groupsNotPinned.isEmpty && if (_groupsNotPinned.isEmpty &&
_groupsPinned.isEmpty && _groupsPinned.isEmpty &&
_groupsArchived.isEmpty) _groupsArchived.isEmpty)

View file

@ -125,44 +125,6 @@ class _ChatGroupActionState extends State<ChatGroupAction> {
} }
} }
// switch (widget.action.type) {
// case GroupActionType.updatedGroupName:
// text = (contact == null)
// ? 'You have changed the group name to "${widget.action.newGroupName}".'
// : '$maker has changed the group name to "${widget.action.newGroupName}".';
// icon = FontAwesomeIcons.pencil;
// case GroupActionType.createdGroup:
// icon = FontAwesomeIcons.penToSquare;
// text = (contact == null)
// ? 'You have created the group.'
// : '$maker has created the group.';
// case GroupActionType.removedMember:
// icon = FontAwesomeIcons.userMinus;
// text = (contact == null)
// ? 'You have removed $affected from the group.'
// : '$maker has removed $affected from the group.';
// case GroupActionType.addMember:
// icon = FontAwesomeIcons.userPlus;
// text = (contact == null)
// ? 'You have added $affected to the group.'
// : '$maker has added $affected to the group.';
// case GroupActionType.promoteToAdmin:
// icon = FontAwesomeIcons.key;
// text = (contact == null)
// ? 'You made $affected an admin.'
// : '$maker made $affected an admin.';
// case GroupActionType.demoteToMember:
// icon = FontAwesomeIcons.key;
// text = (contact == null)
// ? 'You revoked $affectedR admin rights.'
// : '$maker revoked $affectedR admin rights.';
// case GroupActionType.leftGroup:
// icon = FontAwesomeIcons.userMinus;
// text = (contact == null)
// ? 'You have left the group.'
// : '$maker has left the group.';
// }
return Padding( return Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
child: Center( child: Center(

View file

@ -7,8 +7,8 @@ import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
@ -131,8 +131,7 @@ class _MessageInputState extends State<MessageInput> {
_currentDuration = 0; _currentDuration = 0;
}); });
await HapticFeedback.heavyImpact(); await HapticFeedback.heavyImpact();
final audioTmpPath = final audioTmpPath = '${AppEnvironment.cacheDir}/recording.m4a';
'${(await getApplicationCacheDirectory()).path}/recording.m4a';
unawaited( unawaited(
recorderController.record( recorderController.record(
path: audioTmpPath, path: audioTmpPath,

View file

@ -61,7 +61,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
Message? currentMessage; Message? currentMessage;
DateTime? canBeSeenUntil; DateTime? canBeSeenUntil;
double progress = 0; final ValueNotifier<double> progress = ValueNotifier(0);
bool showSendTextMessageInput = false; bool showSendTextMessageInput = false;
final GlobalKey mediaWidgetKey = GlobalKey(); final GlobalKey mediaWidgetKey = GlobalKey();
@ -100,6 +100,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
progressTimer?.cancel(); progressTimer?.cancel();
_subscription?.cancel(); _subscription?.cancel();
downloadStateListener?.cancel(); downloadStateListener?.cancel();
progress.dispose();
ScreenProtector.preventScreenshotOff(); ScreenProtector.preventScreenshotOff();
@ -226,7 +227,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
canBeSeenUntil = null; canBeSeenUntil = null;
imageSaving = false; imageSaving = false;
imageSaved = false; imageSaved = false;
progress = 0; progress.value = 0;
showSendTextMessageInput = false; showSendTextMessageInput = false;
}); });
@ -351,6 +352,11 @@ class _MediaViewerViewState extends State<MediaViewerView> {
return nextMediaOrExit(); return nextMediaOrExit();
} }
// The server can now delete the encrypted bytes, as the users has sucessfully opened it.
unawaited(
apiService.downloadDone(currentMediaLocal.mediaFile.downloadToken!),
);
var timerRequired = false; var timerRequired = false;
if (currentMediaLocal.mediaFile.type == MediaType.video) { if (currentMediaLocal.mediaFile.type == MediaType.video) {
@ -388,9 +394,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
final duration = ctrl.value.duration.inSeconds; final duration = ctrl.value.duration.inSeconds;
if (duration > 0) { if (duration > 0) {
setState(() { progress.value = 1 - ctrl.value.position.inSeconds / duration;
progress = 1 - ctrl.value.position.inSeconds / duration;
});
} }
if (currentMediaLocal.mediaFile.displayLimitInMilliseconds != if (currentMediaLocal.mediaFile.displayLimitInMilliseconds !=
@ -450,9 +454,8 @@ class _MediaViewerViewState extends State<MediaViewerView> {
} }
final difference = canBeSeenUntil!.difference(clock.now()); final difference = canBeSeenUntil!.difference(clock.now());
// Calculate the progress as a value between 0.0 and 1.0 // Calculate the progress as a value between 0.0 and 1.0
progress = progress.value =
difference.inMilliseconds / (mediaFile.displayLimitInMilliseconds!); difference.inMilliseconds / (mediaFile.displayLimitInMilliseconds!);
setState(() {});
}); });
} }
} }
@ -647,7 +650,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
children: [ children: [
if (_showDownloadingLoader) _loader(), if (_showDownloadingLoader) _loader(),
if ((currentMedia != null || videoController != null) && if ((currentMedia != null || videoController != null) &&
(canBeSeenUntil == null || progress >= 0)) (canBeSeenUntil == null || progress.value >= 0))
GestureDetector( GestureDetector(
onTap: onTap, onTap: onTap,
onDoubleTap: (videoController == null) ? null : onTap, onDoubleTap: (videoController == null) ? null : onTap,
@ -717,7 +720,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
if (currentMedia != null && if (currentMedia != null &&
currentMedia?.mediaFile.downloadState != DownloadState.ready) currentMedia?.mediaFile.downloadState != DownloadState.ready)
Positioned.fill(child: _loader()), Positioned.fill(child: _loader()),
if (canBeSeenUntil != null || progress >= 0) if (canBeSeenUntil != null || progress.value >= 0)
Positioned( Positioned(
right: 20, right: 20,
top: 27, top: 27,
@ -726,9 +729,14 @@ class _MediaViewerViewState extends State<MediaViewerView> {
SizedBox( SizedBox(
width: 20, width: 20,
height: 20, height: 20,
child: CircularProgressIndicator( child: ValueListenableBuilder<double>(
value: progress, valueListenable: progress,
builder: (context, value, child) {
return CircularProgressIndicator(
value: value,
strokeWidth: 2, strokeWidth: 2,
);
},
), ),
), ),
], ],

View file

@ -10,8 +10,10 @@ import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart
as server; as server;
import 'package:twonly/src/model/protobuf/client/generated/qr.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/qr.pb.dart';
import 'package:twonly/src/services/api/utils.api.dart'; import 'package:twonly/src/services/api/utils.api.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/qr.utils.dart'; import 'package:twonly/src/utils/qr.utils.dart';
import 'package:twonly/src/visual/components/snackbar.dart';
class AddContactViaQrLinkView extends StatefulWidget { class AddContactViaQrLinkView extends StatefulWidget {
const AddContactViaQrLinkView({ const AddContactViaQrLinkView({
@ -69,11 +71,8 @@ class _AddContactViaQrLinkViewState extends State<AddContactViaQrLinkView> {
context.pop(); context.pop();
} }
} catch (e) { } catch (e) {
if (mounted) { if (mounted) showSnackbar(context, 'Error: $e');
ScaffoldMessenger.of(context).showSnackBar( Log.error(e);
SnackBar(content: Text('Error: $e')),
);
}
} finally { } finally {
if (mounted) { if (mounted) {
setState(() { setState(() {

View file

@ -16,6 +16,7 @@ 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/avatar_icon.comp.dart';
import 'package:twonly/src/visual/components/flame_counter.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/select_chat_deletion_time.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/components/verification_badge.comp.dart';
import 'package:twonly/src/visual/elements/better_list_title.element.dart'; import 'package:twonly/src/visual/elements/better_list_title.element.dart';
import 'package:twonly/src/visual/views/contact/contact_components/restore_flame.comp.dart'; import 'package:twonly/src/visual/views/contact/contact_components/restore_flame.comp.dart';
@ -102,12 +103,7 @@ class _ContactViewState extends State<ContactView> {
if (!mounted) return; if (!mounted) return;
if (!delete) { if (!delete) {
ScaffoldMessenger.of(context).showSnackBar( showSnackbar(context, context.lang.deleteUserErrorMessage);
SnackBar(
content: Text(context.lang.deleteUserErrorMessage),
duration: const Duration(seconds: 8),
),
);
return; return;
} }
@ -157,11 +153,10 @@ class _ContactViewState extends State<ContactView> {
final res = await apiService.reportUser(contact.userId, reason); final res = await apiService.reportUser(contact.userId, reason);
if (!mounted) return; if (!mounted) return;
if (res.isSuccess) { if (res.isSuccess) {
ScaffoldMessenger.of(context).showSnackBar( showSnackbar(
SnackBar( context,
content: Text(context.lang.userGotReported), context.lang.userGotReported,
duration: const Duration(seconds: 3), level: SnackbarLevel.info,
),
); );
} else { } else {
showNetworkIssue(context); showNetworkIssue(context);

View file

@ -7,12 +7,13 @@ import 'package:twonly/locator.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/tables/groups.table.dart'; import 'package:twonly/src/database/tables/groups.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/group.services.dart'; import 'package:twonly/src/services/group.service.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/alert.dialog.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/avatar_icon.comp.dart';
import 'package:twonly/src/visual/components/flame_counter.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/select_chat_deletion_time.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/components/verification_badge.comp.dart';
import 'package:twonly/src/visual/elements/better_list_title.element.dart'; import 'package:twonly/src/visual/elements/better_list_title.element.dart';
import 'package:twonly/src/visual/views/contact/contact.view.dart'; import 'package:twonly/src/visual/views/contact/contact.view.dart';
@ -343,10 +344,8 @@ Future<String?> showGroupNameChangeDialog(
} }
void showNetworkIssue(BuildContext context) { void showNetworkIssue(BuildContext context) {
ScaffoldMessenger.of(context).showSnackBar( showSnackbar(
SnackBar( context,
content: Text(context.lang.groupNetworkIssue), context.lang.groupNetworkIssue,
duration: const Duration(seconds: 3),
),
); );
} }

View file

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/group.services.dart'; import 'package:twonly/src/services/group.service.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
import 'package:twonly/src/visual/components/flame_counter.comp.dart'; import 'package:twonly/src/visual/components/flame_counter.comp.dart';

View file

@ -11,6 +11,7 @@ import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
import 'package:twonly/src/visual/components/flame_counter.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/context_menu/user.context_menu.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/decorations/input_text.decoration.dart';
import 'package:twonly/src/visual/views/groups/group_create_select_group_name.view.dart'; import 'package:twonly/src/visual/views/groups/group_create_select_group_name.view.dart';
@ -88,12 +89,7 @@ class _StartNewChatView extends State<GroupCreateSelectMembersView> {
if (alreadyInGroup.contains(userId)) return; if (alreadyInGroup.contains(userId)) return;
if (!selectedUsers.contains(userId)) { if (!selectedUsers.contains(userId)) {
if (selectedUsers.length + alreadyInGroup.length > 256) { if (selectedUsers.length + alreadyInGroup.length > 256) {
ScaffoldMessenger.of(context).showSnackBar( showSnackbar(context, context.lang.groupSizeLimitError(256));
SnackBar(
content: Text(context.lang.groupSizeLimitError(256)),
duration: const Duration(seconds: 3),
),
);
return; return;
} }
selectedUsers.add(userId); selectedUsers.add(userId);

View file

@ -9,9 +9,10 @@ import 'package:twonly/src/database/tables/groups.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/messages.api.dart'; import 'package:twonly/src/services/api/messages.api.dart';
import 'package:twonly/src/services/group.services.dart'; import 'package:twonly/src/services/group.service.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/alert.dialog.dart'; import 'package:twonly/src/visual/components/alert.dialog.dart';
import 'package:twonly/src/visual/components/snackbar.dart';
import 'package:twonly/src/visual/context_menu/context_menu.helper.dart'; import 'package:twonly/src/visual/context_menu/context_menu.helper.dart';
import 'package:twonly/src/visual/views/groups/group.view.dart'; import 'package:twonly/src/visual/views/groups/group.view.dart';
@ -107,11 +108,10 @@ class GroupMemberContextMenu extends StatelessWidget {
), ),
); );
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( showSnackbar(
SnackBar( context,
content: Text(context.lang.contactRequestSend), context.lang.contactRequestSend,
duration: const Duration(seconds: 3), level: SnackbarLevel.success,
),
); );
} }
} }

View file

@ -1,7 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'package:app_links/app_links.dart'; import 'package:app_links/app_links.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_sharing_intent/model/sharing_file.dart'; import 'package:flutter_sharing_intent/model/sharing_file.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
@ -39,8 +41,11 @@ class HomeViewState extends State<HomeView> {
final MainCameraController _mainCameraController = MainCameraController(); final MainCameraController _mainCameraController = MainCameraController();
final PageController _homeViewPageController = PageController(initialPage: 1); final PageController _homeViewPageController = PageController(initialPage: 1);
late StreamSubscription<List<SharedFile>> _intentStreamSub; StreamSubscription<List<SharedFile>>? _intentStreamSub;
late StreamSubscription<Uri> _deepLinkSub; StreamSubscription<Uri>? _deepLinkSub;
StreamSubscription<RemoteMessage>? _onMessageOpenedAppSub;
StreamSubscription<int>? _homeViewPageIndexSub;
StreamSubscription<NotificationResponse>? _selectNotificationSub;
static final streamHomeViewPageIndex = StreamController<int>.broadcast(); static final streamHomeViewPageIndex = StreamController<int>.broadcast();
@ -51,14 +56,16 @@ class HomeViewState extends State<HomeView> {
if (mounted) setState(() {}); if (mounted) setState(() {});
}; };
streamHomeViewPageIndex.stream.listen((index) { _homeViewPageIndexSub = streamHomeViewPageIndex.stream.listen((index) {
_homeViewPageController.jumpToPage(index); _homeViewPageController.jumpToPage(index);
setState(() { setState(() {
_activePageIdx = index; _activePageIdx = index;
}); });
}); });
selectNotificationStream.stream.listen((response) async { _selectNotificationSub = selectNotificationStream.stream.listen((
response,
) async {
if (response.payload != null && if (response.payload != null &&
response.payload!.startsWith(Routes.chats) && response.payload!.startsWith(Routes.chats) &&
response.payload! != Routes.chats) { response.payload! != Routes.chats) {
@ -67,6 +74,13 @@ class HomeViewState extends State<HomeView> {
streamHomeViewPageIndex.add(0); streamHomeViewPageIndex.add(0);
}); });
_onMessageOpenedAppSub = FirebaseMessaging.onMessageOpenedApp.listen((
message,
) {
Log.info('Opened app from iOS/Remote push notification tap.');
streamHomeViewPageIndex.add(0);
});
unawaited(_mainCameraController.selectCamera(0, true)); unawaited(_mainCameraController.selectCamera(0, true));
unawaited(_initAsync()); unawaited(_initAsync());
@ -99,10 +113,23 @@ class HomeViewState extends State<HomeView> {
final notificationAppLaunchDetails = await flutterLocalNotificationsPlugin final notificationAppLaunchDetails = await flutterLocalNotificationsPlugin
.getNotificationAppLaunchDetails(); .getNotificationAppLaunchDetails();
RemoteMessage? initialRemoteMessage;
try {
initialRemoteMessage = await FirebaseMessaging.instance
.getInitialMessage();
} catch (e) {
Log.error('Could not get initial Firebase message: $e');
}
if (widget.initialPage == 0 || if (widget.initialPage == 0 ||
initialRemoteMessage != null ||
(notificationAppLaunchDetails != null && (notificationAppLaunchDetails != null &&
notificationAppLaunchDetails.didNotificationLaunchApp)) { notificationAppLaunchDetails.didNotificationLaunchApp)) {
if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? false) { if (initialRemoteMessage != null) {
Log.info('App launched from iOS/Remote push notification tap.');
streamHomeViewPageIndex.add(0);
} else if (notificationAppLaunchDetails?.didNotificationLaunchApp ??
false) {
final payload = final payload =
notificationAppLaunchDetails?.notificationResponse?.payload; notificationAppLaunchDetails?.notificationResponse?.payload;
if (payload != null && if (payload != null &&
@ -134,12 +161,13 @@ class HomeViewState extends State<HomeView> {
@override @override
void dispose() { void dispose() {
selectNotificationStream.close(); _onMessageOpenedAppSub?.cancel();
streamHomeViewPageIndex.close(); _homeViewPageIndexSub?.cancel();
_selectNotificationSub?.cancel();
_disableCameraTimer?.cancel(); _disableCameraTimer?.cancel();
_mainCameraController.closeCamera(); _mainCameraController.closeCamera();
_intentStreamSub.cancel(); _intentStreamSub?.cancel();
_deepLinkSub.cancel(); _deepLinkSub?.cancel();
super.dispose(); super.dispose();
} }

View file

@ -1,13 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:restart_app/restart_app.dart'; import 'package:restart_app/restart_app.dart';
import 'package:twonly/src/model/json/userdata.model.dart'; import 'package:twonly/src/services/backup.service.dart';
import 'package:twonly/src/services/backup/restore.backup.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/alert.dialog.dart'; import 'package:twonly/src/visual/components/alert.dialog.dart';
import 'package:twonly/src/visual/components/snackbar.dart';
import 'package:twonly/src/visual/decorations/input_text.decoration.dart'; import 'package:twonly/src/visual/decorations/input_text.decoration.dart';
import 'package:twonly/src/visual/views/settings/backup/backup_server.view.dart';
class BackupRecoveryView extends StatefulWidget { class BackupRecoveryView extends StatefulWidget {
const BackupRecoveryView({super.key}); const BackupRecoveryView({super.key});
@ -19,7 +17,6 @@ class BackupRecoveryView extends StatefulWidget {
class _BackupRecoveryViewState extends State<BackupRecoveryView> { class _BackupRecoveryViewState extends State<BackupRecoveryView> {
bool obscureText = true; bool obscureText = true;
bool isLoading = false; bool isLoading = false;
BackupServer? backupServer;
final TextEditingController usernameCtrl = TextEditingController(); final TextEditingController usernameCtrl = TextEditingController();
final TextEditingController passwordCtrl = TextEditingController(); final TextEditingController passwordCtrl = TextEditingController();
@ -28,30 +25,37 @@ class _BackupRecoveryViewState extends State<BackupRecoveryView> {
isLoading = true; isLoading = true;
}); });
try { final error = await BackupService.startFullBackupRecovery(
await recoverBackup(
usernameCtrl.text, usernameCtrl.text,
passwordCtrl.text, passwordCtrl.text,
backupServer,
); );
if (!mounted) return;
if (error != null) {
String errorMessage;
switch (error) {
case RecoveryError.noInternet:
errorMessage = context.lang.recoverErrorNoInternet;
case RecoveryError.usernameNotValid:
errorMessage = context.lang.recoverErrorUsernameNotValid;
case RecoveryError.passwordInvalid:
errorMessage = context.lang.recoverErrorPasswordInvalid;
case RecoveryError.tryAgainLater:
errorMessage = context.lang.recoverErrorTryAgainLater;
case RecoveryError.unkownError:
errorMessage = context.lang.recoverErrorUnknown;
}
setState(() {
isLoading = false;
});
return showSnackbar(context, errorMessage);
}
await Restart.restartApp( await Restart.restartApp(
notificationTitle: 'Backup successfully recovered.', notificationTitle: context.lang.recoverSuccessTitle,
notificationBody: 'Click here to open the app again', notificationBody: context.lang.recoverSuccessBody,
forceKill: true, forceKill: true,
); );
} catch (e) {
// in case something was already written from the backup...
Log.error('$e');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('$e'),
duration: const Duration(seconds: 3),
),
);
}
}
setState(() { setState(() {
isLoading = false; isLoading = false;
@ -135,20 +139,6 @@ class _BackupRecoveryViewState extends State<BackupRecoveryView> {
), ),
], ],
), ),
const SizedBox(height: 30),
Center(
child: OutlinedButton(
onPressed: () async {
backupServer =
await context.navPush(
const BackupServerView(),
)
as BackupServer?;
setState(() {});
},
child: Text(context.lang.backupExpertSettings),
),
),
const SizedBox(height: 10), const SizedBox(height: 10),
Center( Center(
child: FilledButton.icon( child: FilledButton.icon(

View file

@ -1,22 +1,19 @@
// ignore_for_file: avoid_dynamic_calls // ignore_for_file: avoid_dynamic_calls
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/constants/secure_storage.keys.dart';
import 'package:twonly/src/model/json/userdata.model.dart'; import 'package:twonly/src/model/json/userdata.model.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/services/signal/identity.signal.dart'; import 'package:twonly/src/services/signal/identity.signal.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/pow.dart'; import 'package:twonly/src/utils/pow.dart';
import 'package:twonly/src/utils/secure_storage.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/visual/components/alert.dialog.dart'; import 'package:twonly/src/visual/components/alert.dialog.dart';
import 'package:twonly/src/visual/views/groups/group.view.dart'; import 'package:twonly/src/visual/views/groups/group.view.dart';
@ -141,12 +138,7 @@ class _RegisterViewState extends State<RegisterView> {
currentSetupPage: SetupPages.profile.name, currentSetupPage: SetupPages.profile.name,
)..appVersion = AppState.latestAppVersionId; )..appVersion = AppState.latestAppVersionId;
await SecureStorage.instance.write( await UserService.save(userData);
key: SecureStorageKeys.userData,
value: jsonEncode(userData),
);
await userService.tryInit();
await apiService.authenticate(); await apiService.authenticate();
widget.callbackOnSuccess(); widget.callbackOnSuccess();

View file

@ -1,9 +1,7 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart'; import 'package:twonly/src/services/backup.service.dart';
import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/services/backup/common.backup.dart';
import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/alert.dialog.dart'; import 'package:twonly/src/visual/components/alert.dialog.dart';
@ -19,16 +17,16 @@ class BackupSetupPage extends StatefulWidget {
} }
class _BackupSetupPageState extends State<BackupSetupPage> { class _BackupSetupPageState extends State<BackupSetupPage> {
bool isLoading = false; bool _isLoading = false;
final TextEditingController passwordCtrl = TextEditingController(); final TextEditingController _passwordCtrl = TextEditingController();
final TextEditingController repeatedPasswordCtrl = TextEditingController(); final TextEditingController _repeatedPasswordCtrl = TextEditingController();
Future<bool> onPressedEnableTwonlySafe() async { Future<bool> onPressedEnableTwonlySafe() async {
setState(() { setState(() {
isLoading = true; _isLoading = true;
}); });
if (!await isSecurePassword(passwordCtrl.text)) { if (!await isSecurePassword(_passwordCtrl.text)) {
if (!mounted) return true; if (!mounted) return true;
final ignore = await showAlertDialog( final ignore = await showAlertDialog(
context, context,
@ -40,14 +38,14 @@ class _BackupSetupPageState extends State<BackupSetupPage> {
if (!mounted) return true; if (!mounted) return true;
if (ignore) { if (ignore) {
setState(() { setState(() {
isLoading = false; _isLoading = false;
}); });
return true; return true;
} }
} }
await Future.delayed(const Duration(milliseconds: 100)); await Future.delayed(const Duration(milliseconds: 100));
await enableTwonlySafe(passwordCtrl.text); await BackupService.updateBackupPassword(_passwordCtrl.text);
await UserService.update((user) { await UserService.update((user) {
user.currentSetupPage = SetupPages.backup.next()?.name; user.currentSetupPage = SetupPages.backup.next()?.name;
@ -55,25 +53,25 @@ class _BackupSetupPageState extends State<BackupSetupPage> {
if (!mounted) return true; if (!mounted) return true;
setState(() { setState(() {
isLoading = false; _isLoading = false;
}); });
return false; return false;
} }
@override @override
void dispose() { void dispose() {
passwordCtrl.dispose(); _passwordCtrl.dispose();
repeatedPasswordCtrl.dispose(); _repeatedPasswordCtrl.dispose();
super.dispose(); super.dispose();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isPasswordValid = passwordCtrl.text.length >= 10; final isPasswordValid = _passwordCtrl.text.length >= 10;
final isRepeatedPasswordValid = final isRepeatedPasswordValid =
passwordCtrl.text == repeatedPasswordCtrl.text; _passwordCtrl.text == _repeatedPasswordCtrl.text;
final canSubmit = final canSubmit =
!isLoading && !_isLoading &&
(isPasswordValid && isRepeatedPasswordValid || !kReleaseMode); (isPasswordValid && isRepeatedPasswordValid || !kReleaseMode);
return Column( return Column(
@ -95,24 +93,24 @@ class _BackupSetupPageState extends State<BackupSetupPage> {
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
BackupPasswordTextField( BackupPasswordTextField(
controller: passwordCtrl, controller: _passwordCtrl,
labelText: context.lang.password, labelText: context.lang.password,
onChanged: (_) => setState(() {}), onChanged: (_) => setState(() {}),
), ),
PasswordRequirementText( PasswordRequirementText(
text: context.lang.backupPasswordRequirement, text: context.lang.backupPasswordRequirement,
showError: passwordCtrl.text.isNotEmpty && !isPasswordValid, showError: _passwordCtrl.text.isNotEmpty && !isPasswordValid,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
BackupPasswordTextField( BackupPasswordTextField(
controller: repeatedPasswordCtrl, controller: _repeatedPasswordCtrl,
labelText: context.lang.passwordRepeated, labelText: context.lang.passwordRepeated,
onChanged: (_) => setState(() {}), onChanged: (_) => setState(() {}),
), ),
PasswordRequirementText( PasswordRequirementText(
text: context.lang.passwordRepeatedNotEqual, text: context.lang.passwordRepeatedNotEqual,
showError: showError:
repeatedPasswordCtrl.text.isNotEmpty && !isRepeatedPasswordValid, _repeatedPasswordCtrl.text.isNotEmpty && !isRepeatedPasswordValid,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
Row( Row(
@ -131,16 +129,9 @@ class _BackupSetupPageState extends State<BackupSetupPage> {
), ),
], ],
), ),
const SizedBox(height: 20),
Center(
child: TextButton(
onPressed: () => context.push(Routes.settingsBackupServer),
child: Text(context.lang.backupExpertSettings),
),
),
const SizedBox(height: 40), const SizedBox(height: 40),
NextButtonComp( NextButtonComp(
isLoading: isLoading, isLoading: _isLoading,
canSubmit: canSubmit, canSubmit: canSubmit,
onPressed: onPressedEnableTwonlySafe, onPressed: onPressedEnableTwonlySafe,
), ),

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