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
## 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
- 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_UP
import io.flutter.embedding.engine.FlutterEngine
import android.content.Context
import io.crates.keyring.Keyring
class MainActivity : FlutterFragmentActivity() {
@ -24,6 +26,8 @@ class MainActivity : FlutterFragmentActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
Keyring.initializeNdkContext(applicationContext)
MediaStoreChannel.configure(flutterEngine, applicationContext)
VideoCompressionChannel.configure(flutterEngine, applicationContext)
}

View file

@ -3,10 +3,12 @@ package eu.twonly
import io.flutter.app.FlutterApplication
import dev.fluttercommunity.workmanager.WorkmanagerDebug
import dev.fluttercommunity.workmanager.LoggingDebugHandler
import io.crates.keyring.Keyring
class MyApplication : FlutterApplication() {
override fun onCreate() {
super.onCreate()
Keyring.initializeNdkContext(this)
// This enables the internal plugin logging to Logcat
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/register.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';
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 recoveryPossible;
@override
State<App> createState() => _AppState();
}
@ -77,7 +83,6 @@ class _AppState extends State<App> with WidgetsBindingObserver {
if (widget.storageError) {
return MaterialApp(
scaffoldMessengerKey: AppGlobalKeys.scaffoldMessengerKey,
localizationsDelegates: localizationsDelegates,
debugShowCheckedModeBanner: false,
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(
routerConfig: routerProvider,
scaffoldMessengerKey: AppGlobalKeys.scaffoldMessengerKey,
localizationsDelegates: localizationsDelegates,
debugShowCheckedModeBanner: false,
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 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);
class AnnouncedUser {
@ -36,6 +36,27 @@ class AnnouncedUser {
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 {
final int promotionId;
final PlatformInt64 publicId;
@ -74,24 +95,3 @@ class OtherPromotion {
announcementShare == other.announcementShare &&
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/callbacks.dart';
import 'bridge/wrapper/backup.dart';
import 'bridge/wrapper/key_manager.dart';
import 'bridge/wrapper/user_discovery.dart';
import 'dart:async';
import 'dart:convert';
import 'dart:ffi' as ffi;
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';
abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@ -98,6 +102,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
Object dco_decode_DartOpaque(dynamic raw);
@protected
Map<PlatformInt64, Uint8List> dco_decode_Map_i_64_list_prim_u_8_strict_None(
dynamic raw,
);
@protected
RustStreamSink<String> dco_decode_StreamSink_String_Sse(dynamic raw);
@ -107,17 +116,23 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
AnnouncedUser dco_decode_announced_user(dynamic raw);
@protected
BackupPasswordKeys dco_decode_backup_password_keys(dynamic raw);
@protected
bool dco_decode_bool(dynamic raw);
@protected
AnnouncedUser dco_decode_box_autoadd_announced_user(dynamic raw);
@protected
BackupPasswordKeys dco_decode_box_autoadd_backup_password_keys(dynamic raw);
@protected
PlatformInt64 dco_decode_box_autoadd_i_64(dynamic raw);
@protected
TwonlyConfig dco_decode_box_autoadd_twonly_config(dynamic raw);
InitConfig dco_decode_box_autoadd_init_config(dynamic raw);
@protected
FlutterUserDiscovery dco_decode_flutter_user_discovery(dynamic raw);
@ -125,6 +140,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
PlatformInt64 dco_decode_i_64(dynamic raw);
@protected
InitConfig dco_decode_init_config(dynamic raw);
@protected
PlatformInt64 dco_decode_isize(dynamic raw);
@ -140,6 +158,13 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
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
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);
@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
int dco_decode_u_32(dynamic raw);
@ -167,6 +211,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
int dco_decode_u_8(dynamic raw);
@protected
U8Array32 dco_decode_u_8_array_32(dynamic raw);
@protected
void dco_decode_unit(dynamic raw);
@ -179,6 +226,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
Object sse_decode_DartOpaque(SseDeserializer deserializer);
@protected
Map<PlatformInt64, Uint8List> sse_decode_Map_i_64_list_prim_u_8_strict_None(
SseDeserializer deserializer,
);
@protected
RustStreamSink<String> sse_decode_StreamSink_String_Sse(
SseDeserializer deserializer,
@ -190,6 +242,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
AnnouncedUser sse_decode_announced_user(SseDeserializer deserializer);
@protected
BackupPasswordKeys sse_decode_backup_password_keys(
SseDeserializer deserializer,
);
@protected
bool sse_decode_bool(SseDeserializer deserializer);
@ -198,13 +255,16 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
BackupPasswordKeys sse_decode_box_autoadd_backup_password_keys(
SseDeserializer deserializer,
);
@protected
PlatformInt64 sse_decode_box_autoadd_i_64(SseDeserializer deserializer);
@protected
TwonlyConfig sse_decode_box_autoadd_twonly_config(
SseDeserializer deserializer,
);
InitConfig sse_decode_box_autoadd_init_config(SseDeserializer deserializer);
@protected
FlutterUserDiscovery sse_decode_flutter_user_discovery(
@ -214,6 +274,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
PlatformInt64 sse_decode_i_64(SseDeserializer deserializer);
@protected
InitConfig sse_decode_init_config(SseDeserializer deserializer);
@protected
PlatformInt64 sse_decode_isize(SseDeserializer deserializer);
@ -233,6 +296,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
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
AnnouncedUser? sse_decode_opt_box_autoadd_announced_user(
SseDeserializer deserializer,
@ -258,7 +330,32 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
OtherPromotion sse_decode_other_promotion(SseDeserializer deserializer);
@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
int sse_decode_u_32(SseDeserializer deserializer);
@ -266,6 +363,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
int sse_decode_u_8(SseDeserializer deserializer);
@protected
U8Array32 sse_decode_u_8_array_32(SseDeserializer deserializer);
@protected
void sse_decode_unit(SseDeserializer deserializer);
@ -366,6 +466,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
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
void sse_encode_StreamSink_String_Sse(
RustStreamSink<String> self,
@ -378,6 +484,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_announced_user(AnnouncedUser self, SseSerializer serializer);
@protected
void sse_encode_backup_password_keys(
BackupPasswordKeys self,
SseSerializer serializer,
);
@protected
void sse_encode_bool(bool self, SseSerializer serializer);
@ -387,6 +499,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_backup_password_keys(
BackupPasswordKeys self,
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_i_64(
PlatformInt64 self,
@ -394,8 +512,8 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
);
@protected
void sse_encode_box_autoadd_twonly_config(
TwonlyConfig self,
void sse_encode_box_autoadd_init_config(
InitConfig self,
SseSerializer serializer,
);
@ -408,6 +526,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_i_64(PlatformInt64 self, SseSerializer serializer);
@protected
void sse_encode_init_config(InitConfig self, SseSerializer serializer);
@protected
void sse_encode_isize(PlatformInt64 self, SseSerializer serializer);
@ -432,6 +553,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
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
void sse_encode_opt_box_autoadd_announced_user(
AnnouncedUser? self,
@ -469,7 +599,40 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
);
@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
void sse_encode_u_32(int self, SseSerializer serializer);
@ -477,6 +640,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_u_8(int self, SseSerializer serializer);
@protected
void sse_encode_u_8_array_32(U8Array32 self, SseSerializer serializer);
@protected
void sse_encode_unit(void self, SseSerializer serializer);

View file

@ -8,10 +8,14 @@
import 'bridge.dart';
import 'bridge/callbacks.dart';
import 'bridge/wrapper/backup.dart';
import 'bridge/wrapper/key_manager.dart';
import 'bridge/wrapper/user_discovery.dart';
import 'dart:async';
import 'dart:convert';
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';
abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@ -100,6 +104,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
Object dco_decode_DartOpaque(dynamic raw);
@protected
Map<PlatformInt64, Uint8List> dco_decode_Map_i_64_list_prim_u_8_strict_None(
dynamic raw,
);
@protected
RustStreamSink<String> dco_decode_StreamSink_String_Sse(dynamic raw);
@ -109,17 +118,23 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
AnnouncedUser dco_decode_announced_user(dynamic raw);
@protected
BackupPasswordKeys dco_decode_backup_password_keys(dynamic raw);
@protected
bool dco_decode_bool(dynamic raw);
@protected
AnnouncedUser dco_decode_box_autoadd_announced_user(dynamic raw);
@protected
BackupPasswordKeys dco_decode_box_autoadd_backup_password_keys(dynamic raw);
@protected
PlatformInt64 dco_decode_box_autoadd_i_64(dynamic raw);
@protected
TwonlyConfig dco_decode_box_autoadd_twonly_config(dynamic raw);
InitConfig dco_decode_box_autoadd_init_config(dynamic raw);
@protected
FlutterUserDiscovery dco_decode_flutter_user_discovery(dynamic raw);
@ -127,6 +142,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
PlatformInt64 dco_decode_i_64(dynamic raw);
@protected
InitConfig dco_decode_init_config(dynamic raw);
@protected
PlatformInt64 dco_decode_isize(dynamic raw);
@ -142,6 +160,13 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
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
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);
@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
int dco_decode_u_32(dynamic raw);
@ -169,6 +213,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
int dco_decode_u_8(dynamic raw);
@protected
U8Array32 dco_decode_u_8_array_32(dynamic raw);
@protected
void dco_decode_unit(dynamic raw);
@ -181,6 +228,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
Object sse_decode_DartOpaque(SseDeserializer deserializer);
@protected
Map<PlatformInt64, Uint8List> sse_decode_Map_i_64_list_prim_u_8_strict_None(
SseDeserializer deserializer,
);
@protected
RustStreamSink<String> sse_decode_StreamSink_String_Sse(
SseDeserializer deserializer,
@ -192,6 +244,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
AnnouncedUser sse_decode_announced_user(SseDeserializer deserializer);
@protected
BackupPasswordKeys sse_decode_backup_password_keys(
SseDeserializer deserializer,
);
@protected
bool sse_decode_bool(SseDeserializer deserializer);
@ -200,13 +257,16 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
BackupPasswordKeys sse_decode_box_autoadd_backup_password_keys(
SseDeserializer deserializer,
);
@protected
PlatformInt64 sse_decode_box_autoadd_i_64(SseDeserializer deserializer);
@protected
TwonlyConfig sse_decode_box_autoadd_twonly_config(
SseDeserializer deserializer,
);
InitConfig sse_decode_box_autoadd_init_config(SseDeserializer deserializer);
@protected
FlutterUserDiscovery sse_decode_flutter_user_discovery(
@ -216,6 +276,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
PlatformInt64 sse_decode_i_64(SseDeserializer deserializer);
@protected
InitConfig sse_decode_init_config(SseDeserializer deserializer);
@protected
PlatformInt64 sse_decode_isize(SseDeserializer deserializer);
@ -235,6 +298,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
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
AnnouncedUser? sse_decode_opt_box_autoadd_announced_user(
SseDeserializer deserializer,
@ -260,7 +332,32 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
OtherPromotion sse_decode_other_promotion(SseDeserializer deserializer);
@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
int sse_decode_u_32(SseDeserializer deserializer);
@ -268,6 +365,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
int sse_decode_u_8(SseDeserializer deserializer);
@protected
U8Array32 sse_decode_u_8_array_32(SseDeserializer deserializer);
@protected
void sse_decode_unit(SseDeserializer deserializer);
@ -368,6 +468,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
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
void sse_encode_StreamSink_String_Sse(
RustStreamSink<String> self,
@ -380,6 +486,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_announced_user(AnnouncedUser self, SseSerializer serializer);
@protected
void sse_encode_backup_password_keys(
BackupPasswordKeys self,
SseSerializer serializer,
);
@protected
void sse_encode_bool(bool self, SseSerializer serializer);
@ -389,6 +501,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_backup_password_keys(
BackupPasswordKeys self,
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_i_64(
PlatformInt64 self,
@ -396,8 +514,8 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
);
@protected
void sse_encode_box_autoadd_twonly_config(
TwonlyConfig self,
void sse_encode_box_autoadd_init_config(
InitConfig self,
SseSerializer serializer,
);
@ -410,6 +528,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_i_64(PlatformInt64 self, SseSerializer serializer);
@protected
void sse_encode_init_config(InitConfig self, SseSerializer serializer);
@protected
void sse_encode_isize(PlatformInt64 self, SseSerializer serializer);
@ -434,6 +555,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
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
void sse_encode_opt_box_autoadd_announced_user(
AnnouncedUser? self,
@ -471,7 +601,40 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
);
@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
void sse_encode_u_32(int self, SseSerializer serializer);
@ -479,6 +642,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_u_8(int self, SseSerializer serializer);
@protected
void sse_encode_u_8_array_32(U8Array32 self, SseSerializer serializer);
@protected
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 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:twonly/src/utils/log.dart';
class AppEnvironment {
static late final String cacheDir;
static late final String supportDir;
static late String cacheDir;
static late String supportDir;
static bool _isInitialized = false;
static bool _isInitialized = false;
@ -22,10 +22,9 @@ class AppEnvironment {
_isInitialized = true;
}
static void initTesting() {
if (_isInitialized) return;
cacheDir = '/tmp/twonly_cache';
supportDir = '/tmp/twonly_support';
static void initTesting({String? customCacheDir, String? customSupportDir}) {
cacheDir = customCacheDir ?? '/tmp/twonly_cache';
supportDir = customSupportDir ?? '/tmp/twonly_support';
_isInitialized = true;
}
}
@ -35,9 +34,5 @@ class AppState {
static bool isInBackgroundTask = false;
static bool allowErrorTrackingViaSentry = false;
static bool gotMessageFromServer = false;
static int latestAppVersionId = 110;
}
class AppGlobalKeys {
static final scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
static int latestAppVersionId = 113;
}

View file

@ -1,6 +1,6 @@
import 'dart:async';
import 'dart:io';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:mutex/mutex.dart';
@ -8,11 +8,16 @@ import 'package:provider/provider.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:twonly/app.dart';
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/globals.dart';
import 'package:twonly/locator.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/model/json/signal_identity.model.dart';
import 'package:twonly/src/providers/connection.provider.dart';
import 'package:twonly/src/providers/image_editor.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/upload.api.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/notifications/fcm.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
/// can also be used by the backend without the UI was loaded.
Future<void> twonlyMinimumInitialization() async {
Future<bool> twonlyMinimumInitialization() async {
Log.info('twonlyMinimumInitialization: called');
await exclusiveAccess(
final hasStorageError = await exclusiveAccess(
lockName: 'init',
mutex: _initMutex,
action: () async {
@ -54,15 +59,22 @@ Future<void> twonlyMinimumInitialization() async {
await initFlutterCallbacksForRust();
Log.info('twonlyMinimumInitialization: bridge.initializeTwonlyFlutter()');
try {
await bridge.initializeTwonlyFlutter(
config: bridge.TwonlyConfig(
databasePath: '${AppEnvironment.supportDir}/twonly.sqlite',
dataDirectory: AppEnvironment.supportDir,
config: bridge.InitConfig(
databaseDir: AppEnvironment.supportDir,
dataDir: AppEnvironment.supportDir,
),
);
} catch (e) {
Log.error(e);
return true;
}
Log.info('twonlyMinimumInitialization: finished');
return false;
},
);
return hasStorageError;
}
void main() async {
@ -72,26 +84,30 @@ void main() async {
unawaited(StartupGuard.markAppStartup());
await twonlyMinimumInitialization();
unawaited(initFCMService());
var storageError = await twonlyMinimumInitialization();
await initFCMService();
var userExists = false;
var storageError = false;
var recoveryPossible = false;
if (!storageError) {
try {
userExists = await userService.tryInit();
} catch (e) {
Log.error('Failed to initialize user session due to storage error: $e');
storageError = true;
}
}
if (Platform.isIOS && userExists) {
final dbFile = File('${AppEnvironment.supportDir}/twonly.sqlite');
if (!dbFile.existsSync()) {
Log.error('[twonly] IOS: App was removed and then reinstalled again...');
await SecureStorage.instance.deleteAll();
userExists = false;
if (!userExists && !storageError) {
try {
final userId = await RustKeyManager.getUserId();
if (userId != null) {
recoveryPossible = true;
}
} 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: (_) => 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 {
@ -185,7 +264,6 @@ Future<void> postStartupTasks() async {
// 1. Immediate background cleanup (Non-blocking for UI)
await twonlyDB.messagesDao.purgeMessageTable();
unawaited(twonlyDB.receiptsDao.purgeReceivedReceipts());
unawaited(UserDiscoveryService.removeDeletedContacts());
unawaited(MediaFileService.purgeTempFolder());
// 2. Service initializations
@ -193,25 +271,12 @@ Future<void> postStartupTasks() async {
unawaited(finishStartedPreprocessing());
unawaited(createPushAvatars());
if (userService.currentUser.userDiscoveryInitializationError) {
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',
);
}
}());
}
unawaited(UserDiscoveryService.verifyInitializationOnStartup());
await Future.delayed(const Duration(seconds: 10));
unawaited(initializeBackgroundTaskManager());
// 3. Delayed tasks (Wait for app to settle)
await Future.delayed(const Duration(minutes: 2));
unawaited(performTwonlySafeBackup());
unawaited(BackupService.makeBackup());
unawaited(cleanLogFile());
}

View file

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

View file

@ -1,4 +1,6 @@
class KeyValueKeys {
static const String lastPeriodicTaskExecution =
'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 settingsSubscription = '/settings/subscription';
static const String settingsBackup = '/settings/backup';
static const String settingsBackupServer = '/settings/backup/server';
static const String settingsBackupRecovery = '/settings/backup/recovery';
static const String settingsBackupSetup = '/settings/backup/setup';
static const String settingsAppearance = '/settings/appearance';

View file

@ -1,11 +1,15 @@
class SecureStorageKeys {
@Deprecated('Use the secure storage in rust')
static const String signalIdentity = 'signal_identity';
@Deprecated('Use the secure storage in rust')
static const String signalSignedPreKey = 'signed_pre_key_store';
@Deprecated('Use the login 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 sendingPushKeys = 'push_keys_sending';
}

View file

@ -139,15 +139,10 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
}
Future<Group?> _insertGroup(GroupsCompanion group) async {
try {
await into(groups).insert(group);
return await (select(
await into(groups).insertOnConflictUpdate(group);
return (select(
groups,
)..where((t) => t.groupId.equals(group.groupId.value))).getSingle();
} catch (e) {
Log.error('Could not insert group: $e');
return null;
}
)..where((t) => t.groupId.equals(group.groupId.value))).getSingleOrNull();
}
Future<List<Contact>> getGroupContact(String groupId) async {
@ -277,7 +272,7 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
groups.groupId.equalsExp(groupMembers.groupId),
),
],
)..where(groups.isDirectChat.isNull()));
)..where(groups.isDirectChat.equals(false)));
return query.map((row) => row.readTable(groupMembers)).get();
} catch (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/user_discovery.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/log.dart';
part 'key_verification.dao.g.dart';
@ -89,10 +90,12 @@ class KeyVerificationDao extends DatabaseAccessor<TwonlyDB>
),
innerJoin(kv, kv.contactId.equalsExp(ur.fromContactId)),
],
)..where(
)
..where(
ur.announcedUserId.equals(contactId) &
ur.publicKeyVerifiedTimestamp.isNotNull(),
);
)
..groupBy([contacts.userId]);
return query.watch().map((rows) {
return rows.map((row) {
@ -116,7 +119,8 @@ class KeyVerificationDao extends DatabaseAccessor<TwonlyDB>
..where(
ur.publicKeyVerifiedTimestamp.isNotNull() &
ur.announcedUserId.equalsExp(ur.fromContactId).not(),
);
)
..groupBy([ur.announcedUserId]);
final rows = await query.get();
return rows.length;
@ -173,6 +177,7 @@ class KeyVerificationDao extends DatabaseAccessor<TwonlyDB>
}
Future<void> addKeyVerification(int contactId, VerificationType type) async {
try {
await into(keyVerifications).insertOnConflictUpdate(
KeyVerificationsCompanion(
contactId: Value(contactId),
@ -185,5 +190,8 @@ class KeyVerificationDao extends DatabaseAccessor<TwonlyDB>
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 '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/utils/secure_storage.dart';
class SignalSignedPreKeyStore extends SignedPreKeyStore {
Future<HashMap<int, Uint8List>> getStore() async {
Future<HashMap<int, Uint8List>> getSignalSignedPreKeyStoreOld() async {
final storeSerialized = await SecureStorage.instance.read(
key: SecureStorageKeys.signalSignedPreKey,
);
@ -23,32 +23,23 @@ class SignalSignedPreKeyStore extends SignedPreKeyStore {
return store;
}
Future<void> safeStore(HashMap<int, Uint8List> store) async {
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,
);
}
class SignalSignedPreKeyStore extends SignedPreKeyStore {
@override
Future<SignedPreKeyRecord> loadSignedPreKey(int signedPreKeyId) async {
final store = await getStore();
if (!store.containsKey(signedPreKeyId)) {
final store = await RustKeyManager.loadSignedPrekey(
signedPreKeyId: signedPreKeyId,
);
if (store == null) {
throw InvalidKeyIdException(
'No such signed prekey record! $signedPreKeyId',
);
}
return SignedPreKeyRecord.fromSerialized(store[signedPreKeyId]!);
return SignedPreKeyRecord.fromSerialized(store);
}
@override
Future<List<SignedPreKeyRecord>> loadSignedPreKeys() async {
final store = await getStore();
final store = await RustKeyManager.loadSignedPrekeys();
final results = <SignedPreKeyRecord>[];
for (final serialized in store.values) {
results.add(SignedPreKeyRecord.fromSerialized(serialized));
@ -61,19 +52,21 @@ class SignalSignedPreKeyStore extends SignedPreKeyStore {
int signedPreKeyId,
SignedPreKeyRecord record,
) async {
final store = await getStore();
store[signedPreKeyId] = record.serialize();
await safeStore(store);
await RustKeyManager.storeSignedPrekey(
signedPreKeyId: signedPreKeyId,
record: record.serialize(),
);
}
@override
Future<bool> containsSignedPreKey(int signedPreKeyId) async =>
(await getStore()).containsKey(signedPreKeyId);
await RustKeyManager.loadSignedPrekey(
signedPreKeyId: signedPreKeyId,
) !=
null;
@override
Future<void> removeSignedPreKey(int signedPreKeyId) async {
final store = await getStore();
store.remove(signedPreKeyId);
await safeStore(store);
await RustKeyManager.removeSignedPrekey(signedPreKeyId: signedPreKeyId);
}
}

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/reactions.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/tables/contacts.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/reactions.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_pre_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,
UserDiscoveryOwnPromotions,
UserDiscoveryShares,
Shortcuts,
ShortcutMembers,
],
daos: [
MessagesDao,
@ -62,6 +66,7 @@ part 'twonly.db.g.dart';
MediaFilesDao,
UserDiscoveryDao,
KeyVerificationDao,
ShortcutsDao,
],
)
class TwonlyDB extends _$TwonlyDB {
@ -74,7 +79,7 @@ class TwonlyDB extends _$TwonlyDB {
TwonlyDB.forTesting(DatabaseConnection super.connection);
@override
int get schemaVersion => 12;
int get schemaVersion => 13;
static QueryExecutor _openConnection() {
return driftDatabase(
@ -186,6 +191,10 @@ class TwonlyDB extends _$TwonlyDB {
await m.addColumn(schema.contacts, column);
}
},
from12To13: (m, schema) async {
await m.createTable(schema.shortcuts);
await m.createTable(schema.shortcutMembers);
},
)(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,
$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({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
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, Schema11 schema) from10To11,
required Future<void> Function(i1.Migrator m, Schema12 schema) from11To12,
required Future<void> Function(i1.Migrator m, Schema13 schema) from12To13,
}) {
return (currentVersion, database) async {
switch (currentVersion) {
@ -6652,6 +7127,11 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema);
await from11To12(migrator, schema);
return 12;
case 12:
final schema = Schema13(database: database);
final migrator = i1.Migrator(database, schema);
await from12To13(migrator, schema);
return 13;
default:
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, Schema11 schema) from10To11,
required Future<void> Function(i1.Migrator m, Schema12 schema) from11To12,
required Future<void> Function(i1.Migrator m, Schema13 schema) from12To13,
}) => i0.VersionedSchema.stepByStepHelper(
step: migrationSteps(
from1To2: from1To2,
@ -6683,5 +7164,6 @@ i1.OnUpgrade stepByStep({
from9To10: from9To10,
from10To11: from10To11,
from11To12: from11To12,
from12To13: from12To13,
),
);

View file

@ -596,6 +596,18 @@ abstract class AppLocalizations {
/// **'Notification'**
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.
///
/// In en, this message translates to:
@ -1286,18 +1298,6 @@ abstract class AppLocalizations {
/// **'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.
///
/// 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.'**
String get backupNoPasswordRecovery;
/// No description provided for @backupServer.
/// No description provided for @backupIdentityHeader.
///
/// In en, this message translates to:
/// **'Server'**
String get backupServer;
/// **'Identity'**
String get backupIdentityHeader;
/// No description provided for @backupMaxBackupSize.
/// No description provided for @backupArchiveHeader.
///
/// In en, this message translates to:
/// **'max. backup size'**
String get backupMaxBackupSize;
/// No description provided for @backupStorageRetention.
///
/// In en, this message translates to:
/// **'Storage retention'**
String get backupStorageRetention;
/// **'Contacts, Settings and Messages'**
String get backupArchiveHeader;
/// No description provided for @backupLastBackupDate.
///
@ -1448,12 +1442,6 @@ abstract class AppLocalizations {
/// **'Result'**
String get backupLastBackupResult;
/// No description provided for @backupData.
///
/// In en, this message translates to:
/// **'Data-Backup'**
String get backupData;
/// No description provided for @backupInsecurePassword.
///
/// In en, this message translates to:
@ -1511,39 +1499,15 @@ abstract class AppLocalizations {
/// No description provided for @backupPasswordRequirement.
///
/// 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;
/// No description provided for @backupExpertSettings.
///
/// In en, this message translates to:
/// **'Expert settings'**
String get backupExpertSettings;
/// No description provided for @backupEnableBackup.
///
/// In en, this message translates to:
/// **'Activate automatic backup'**
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.
///
/// In en, this message translates to:
@ -2330,12 +2294,6 @@ abstract class AppLocalizations {
/// **'Open your own QR code'**
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.
///
/// In en, this message translates to:
@ -2354,6 +2312,24 @@ abstract class AppLocalizations {
/// **'Resume Setup'**
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.
///
/// In en, this message translates to:
@ -3061,6 +3037,126 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'{maker} changed their display name from {oldName} to {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

View file

@ -277,6 +277,13 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get settingsNotification => 'Benachrichtigung';
@override
String get settingsNotifyPermission => 'Benachrichtigungsberechtigung';
@override
String get settingsNotifyPermissionDesc =>
'Systemeinstellungen öffnen, um Push-Benachrichtigungen zu erlauben.';
@override
String get settingsNotifyTroubleshooting => 'Fehlersuche';
@ -658,12 +665,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get open => 'Offene';
@override
String get createVoucher => 'Gutschein kaufen';
@override
String get redeemVoucher => 'Gutschein einlösen';
@override
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.';
@override
String get backupServer => 'Server';
String get backupIdentityHeader => 'Identität';
@override
String get backupMaxBackupSize => 'max. Backup-Größe';
@override
String get backupStorageRetention => 'Speicheraufbewahrung';
String get backupArchiveHeader => 'Kontakte, Einstellungen und Nachrichten';
@override
String get backupLastBackupDate => 'Letztes Backup';
@ -742,9 +740,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get backupLastBackupResult => 'Ergebnis';
@override
String get backupData => 'Daten-Backup';
@override
String get backupInsecurePassword => 'Unsicheres Passwort';
@ -777,24 +772,11 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get backupPasswordRequirement =>
'Das Passwort muss mindestens 8 Zeichen lang sein.';
@override
String get backupExpertSettings => 'Experteneinstellungen';
'Das Passwort muss mindestens 10 Zeichen lang sein.';
@override
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
String get backupTwonlySaveNow => 'Jetzt speichern';
@ -1271,9 +1253,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get openYourOwnQRcode => 'Eigenen QR-Code öffnen';
@override
String get skipForNow => 'Vorerst überspringen';
@override
String get finishSetupCardTitle => 'Profil vervollständigen';
@ -1284,6 +1263,16 @@ class AppLocalizationsDe extends AppLocalizations {
@override
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
String get onboardingFinishLater => 'Später abschließen';
@ -1714,11 +1703,81 @@ class AppLocalizationsDe extends AppLocalizations {
@override
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
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
String get settingsNotification => 'Notification';
@override
String get settingsNotifyPermission => 'Notification permissions';
@override
String get settingsNotifyPermissionDesc =>
'Open system settings to allow push notifications.';
@override
String get settingsNotifyTroubleshooting => 'Troubleshooting';
@ -652,12 +659,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get open => 'Open';
@override
String get createVoucher => 'Buy voucher';
@override
String get redeemVoucher => 'Redeem voucher';
@override
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.';
@override
String get backupServer => 'Server';
String get backupIdentityHeader => 'Identity';
@override
String get backupMaxBackupSize => 'max. backup size';
@override
String get backupStorageRetention => 'Storage retention';
String get backupArchiveHeader => 'Contacts, Settings and Messages';
@override
String get backupLastBackupDate => 'Last backup';
@ -736,9 +734,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get backupLastBackupResult => 'Result';
@override
String get backupData => 'Data-Backup';
@override
String get backupInsecurePassword => 'Insecure password';
@ -771,24 +766,11 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get backupPasswordRequirement =>
'Password must be at least 8 characters long.';
@override
String get backupExpertSettings => 'Expert settings';
'Password must be at least 10 characters long.';
@override
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
String get backupTwonlySaveNow => 'Save now';
@ -1262,9 +1244,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get openYourOwnQRcode => 'Open your own QR code';
@override
String get skipForNow => 'Skip for now';
@override
String get finishSetupCardTitle => 'Complete your profile';
@ -1275,6 +1254,16 @@ class AppLocalizationsEn extends AppLocalizations {
@override
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
String get onboardingFinishLater => 'Finish later';
@ -1706,4 +1695,73 @@ class AppLocalizationsEn extends AppLocalizations {
String makerChangedDisplayName(Object maker, Object oldName, Object 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)
bool updateFCMToken = true;
@JsonKey(defaultValue: true)
bool canUseLoginTokenForAuth = true;
// --- BACKUP ---
DateTime? nextTimeToShowBackupNotice;
BackupServer? backupServer;
@Deprecated('Use the secure storage in rust')
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:
// - users in the "Tester" Plan can, if they want, take part of the user study
@ -175,19 +183,3 @@ class TwonlySafeBackup {
List<int> encryptionKey;
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
..userDiscoverySharePromotion =
json['userDiscoverySharePromotion'] as bool? ?? true
..userDiscoveryInitializationError =
json['userDiscoveryInitializationError'] as bool? ?? false
..currentPreKeyIndexStart =
(json['currentPreKeyIndexStart'] as num?)?.toInt() ?? 100000
..currentSignedPreKeyIndexStart =
@ -80,17 +82,15 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
.toList()
..hideChangeLog = json['hideChangeLog'] as bool? ?? true
..updateFCMToken = json['updateFCMToken'] as bool? ?? true
..nextTimeToShowBackupNotice = json['nextTimeToShowBackupNotice'] == null
? null
: DateTime.parse(json['nextTimeToShowBackupNotice'] as String)
..backupServer = json['backupServer'] == null
? null
: BackupServer.fromJson(json['backupServer'] as Map<String, dynamic>)
..canUseLoginTokenForAuth =
json['canUseLoginTokenForAuth'] as bool? ?? true
..twonlySafeBackup = json['twonlySafeBackup'] == null
? null
: TwonlySafeBackup.fromJson(
json['twonlySafeBackup'] as Map<String, dynamic>,
)
..isBackupEnabled = json['isBackupEnabled'] as bool? ?? false
..fcmToken = json['fcmToken'] as String?
..askedForUserStudyPermission =
json['askedForUserStudyPermission'] as bool? ?? false
..userStudyParticipantsToken =
@ -142,15 +142,16 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'userDiscoveryRequiresManualApproval':
instance.userDiscoveryRequiresManualApproval,
'userDiscoverySharePromotion': instance.userDiscoverySharePromotion,
'userDiscoveryInitializationError': instance.userDiscoveryInitializationError,
'currentPreKeyIndexStart': instance.currentPreKeyIndexStart,
'currentSignedPreKeyIndexStart': instance.currentSignedPreKeyIndexStart,
'lastChangeLogHash': instance.lastChangeLogHash,
'hideChangeLog': instance.hideChangeLog,
'updateFCMToken': instance.updateFCMToken,
'nextTimeToShowBackupNotice': instance.nextTimeToShowBackupNotice
?.toIso8601String(),
'backupServer': instance.backupServer,
'canUseLoginTokenForAuth': instance.canUseLoginTokenForAuth,
'twonlySafeBackup': instance.twonlySafeBackup,
'isBackupEnabled': instance.isBackupEnabled,
'fcmToken': instance.fcmToken,
'askedForUserStudyPermission': instance.askedForUserStudyPermission,
'userStudyParticipantsToken': instance.userStudyParticipantsToken,
'userStudyCountNewFriendsViaSuggestion':
@ -201,16 +202,3 @@ const _$LastBackupUploadStateEnumMap = {
LastBackupUploadState.failed: 'failed',
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,
'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': [
Handshake_RequestPOW$json,
Handshake_Register$json,
Handshake_GetAuthChallenge$json,
Handshake_GetUserIdByUsername$json,
Handshake_GetAuthToken$json,
Handshake_Authenticate$json
Handshake_Authenticate$json,
Handshake_AuthenticateWithLoginToken$json
],
'8': [
{'1': 'Handshake'},
@ -186,9 +206,19 @@ const Handshake_Register$json = {
{'1': 'is_ios', '3': 8, '4': 1, '5': 8, '10': 'isIos'},
{'1': 'lang_code', '3': 9, '4': 1, '5': 9, '10': 'langCode'},
{'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': [
{'1': '_invite_code'},
{'1': '_login_token'},
],
};
@ -197,6 +227,14 @@ const Handshake_GetAuthChallenge$json = {
'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')
const Handshake_GetAuthToken$json = {
'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`.
final $typed_data.Uint8List handshakeDescriptor = $convert.base64Decode(
'CglIYW5kc2hha2USQgoIcmVnaXN0ZXIYASABKAsyJC5jbGllbnRfdG9fc2VydmVyLkhhbmRzaG'
@ -256,20 +312,30 @@ final $typed_data.Uint8List handshakeDescriptor = $convert.base64Decode(
'LkdldEF1dGhUb2tlbkgAUgxnZXRBdXRoVG9rZW4STgoMYXV0aGVudGljYXRlGAQgASgLMiguY2'
'xpZW50X3RvX3NlcnZlci5IYW5kc2hha2UuQXV0aGVudGljYXRlSABSDGF1dGhlbnRpY2F0ZRJI'
'CgpyZXF1ZXN0UE9XGAUgASgLMiYuY2xpZW50X3RvX3NlcnZlci5IYW5kc2hha2UuUmVxdWVzdF'
'BPV0gAUgpyZXF1ZXN0UE9XGgwKClJlcXVlc3RQT1calAMKCFJlZ2lzdGVyEhoKCHVzZXJuYW1l'
'GAEgASgJUgh1c2VybmFtZRIkCgtpbnZpdGVfY29kZRgCIAEoCUgAUgppbnZpdGVDb2RliAEBEi'
'4KE3B1YmxpY19pZGVudGl0eV9rZXkYAyABKAxSEXB1YmxpY0lkZW50aXR5S2V5EiMKDXNpZ25l'
'ZF9wcmVrZXkYBCABKAxSDHNpZ25lZFByZWtleRI2ChdzaWduZWRfcHJla2V5X3NpZ25hdHVyZR'
'gFIAEoDFIVc2lnbmVkUHJla2V5U2lnbmF0dXJlEigKEHNpZ25lZF9wcmVrZXlfaWQYBiABKANS'
'DnNpZ25lZFByZWtleUlkEicKD3JlZ2lzdHJhdGlvbl9pZBgHIAEoA1IOcmVnaXN0cmF0aW9uSW'
'QSFQoGaXNfaW9zGAggASgIUgVpc0lvcxIbCglsYW5nX2NvZGUYCSABKAlSCGxhbmdDb2RlEiIK'
'DXByb29mX29mX3dvcmsYCiABKANSC3Byb29mT2ZXb3JrQg4KDF9pbnZpdGVfY29kZRoSChBHZX'
'RBdXRoQ2hhbGxlbmdlGkMKDEdldEF1dGhUb2tlbhIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQS'
'GgoIcmVzcG9uc2UYAiABKAxSCHJlc3BvbnNlGugBCgxBdXRoZW50aWNhdGUSFwoHdXNlcl9pZB'
'gBIAEoA1IGdXNlcklkEh0KCmF1dGhfdG9rZW4YAiABKAxSCWF1dGhUb2tlbhIkCgthcHBfdmVy'
'c2lvbhgDIAEoCUgAUgphcHBWZXJzaW9uiAEBEiAKCWRldmljZV9pZBgEIAEoA0gBUghkZXZpY2'
'VJZIgBARIoCg1pbl9iYWNrZ3JvdW5kGAUgASgISAJSDGluQmFja2dyb3VuZIgBAUIOCgxfYXBw'
'X3ZlcnNpb25CDAoKX2RldmljZV9pZEIQCg5faW5fYmFja2dyb3VuZEILCglIYW5kc2hha2U=');
'BPV0gAUgpyZXF1ZXN0UE9XEnsKHWF1dGhlbnRpY2F0ZV93aXRoX2xvZ2luX3Rva2VuGAYgASgL'
'MjYuY2xpZW50X3RvX3NlcnZlci5IYW5kc2hha2UuQXV0aGVudGljYXRlV2l0aExvZ2luVG9rZW'
'5IAFIaYXV0aGVudGljYXRlV2l0aExvZ2luVG9rZW4SZgoWZ2V0X3VzZXJpZF9ieV91c2VybmFt'
'ZRgHIAEoCzIvLmNsaWVudF90b19zZXJ2ZXIuSGFuZHNoYWtlLkdldFVzZXJJZEJ5VXNlcm5hbW'
'VIAFITZ2V0VXNlcmlkQnlVc2VybmFtZRoMCgpSZXF1ZXN0UE9XGsoDCghSZWdpc3RlchIaCgh1'
'c2VybmFtZRgBIAEoCVIIdXNlcm5hbWUSJAoLaW52aXRlX2NvZGUYAiABKAlIAFIKaW52aXRlQ2'
'9kZYgBARIuChNwdWJsaWNfaWRlbnRpdHlfa2V5GAMgASgMUhFwdWJsaWNJZGVudGl0eUtleRIj'
'Cg1zaWduZWRfcHJla2V5GAQgASgMUgxzaWduZWRQcmVrZXkSNgoXc2lnbmVkX3ByZWtleV9zaW'
'duYXR1cmUYBSABKAxSFXNpZ25lZFByZWtleVNpZ25hdHVyZRIoChBzaWduZWRfcHJla2V5X2lk'
'GAYgASgDUg5zaWduZWRQcmVrZXlJZBInCg9yZWdpc3RyYXRpb25faWQYByABKANSDnJlZ2lzdH'
'JhdGlvbklkEhUKBmlzX2lvcxgIIAEoCFIFaXNJb3MSGwoJbGFuZ19jb2RlGAkgASgJUghsYW5n'
'Q29kZRIiCg1wcm9vZl9vZl93b3JrGAogASgDUgtwcm9vZk9mV29yaxIkCgtsb2dpbl90b2tlbh'
'gLIAEoDEgBUgpsb2dpblRva2VuiAEBQg4KDF9pbnZpdGVfY29kZUIOCgxfbG9naW5fdG9rZW4a'
'EgoQR2V0QXV0aENoYWxsZW5nZRoxChNHZXRVc2VySWRCeVVzZXJuYW1lEhoKCHVzZXJuYW1lGA'
'EgASgJUgh1c2VybmFtZRpDCgxHZXRBdXRoVG9rZW4SFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklk'
'EhoKCHJlc3BvbnNlGAIgASgMUghyZXNwb25zZRroAQoMQXV0aGVudGljYXRlEhcKB3VzZXJfaW'
'QYASABKANSBnVzZXJJZBIdCgphdXRoX3Rva2VuGAIgASgMUglhdXRoVG9rZW4SJAoLYXBwX3Zl'
'cnNpb24YAyABKAlIAFIKYXBwVmVyc2lvbogBARIgCglkZXZpY2VfaWQYBCABKANIAVIIZGV2aW'
'NlSWSIAQESKAoNaW5fYmFja2dyb3VuZBgFIAEoCEgCUgxpbkJhY2tncm91bmSIAQFCDgoMX2Fw'
'cF92ZXJzaW9uQgwKCl9kZXZpY2VfaWRCEAoOX2luX2JhY2tncm91bmQaxgEKGkF1dGhlbnRpY2'
'F0ZVdpdGhMb2dpblRva2VuEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBIsChJzZWNyZXRfbG9n'
'aW5fdG9rZW4YAiABKAxSEHNlY3JldExvZ2luVG9rZW4SHwoLYXBwX3ZlcnNpb24YAyABKAlSCm'
'FwcFZlcnNpb24SGwoJZGV2aWNlX2lkGAQgASgDUghkZXZpY2VJZBIjCg1pbl9iYWNrZ3JvdW5k'
'GAUgASgIUgxpbkJhY2tncm91bmRCCwoJSGFuZHNoYWtl');
@$core.Deprecated('Use applicationDataDescriptor instead')
const ApplicationData$json = {
@ -321,13 +387,13 @@ const ApplicationData$json = {
'10': 'updateGoogleFcmToken'
},
{
'1': 'getLocation',
'1': 'deprecated_9',
'3': 9,
'4': 1,
'5': 11,
'6': '.client_to_server.ApplicationData.GetLocation',
'6': '.client_to_server.ApplicationData.Deprecated',
'9': 0,
'10': 'getLocation'
'10': 'deprecated9'
},
{
'1': 'getCurrentPlanInfos',
@ -339,13 +405,13 @@ const ApplicationData$json = {
'10': 'getCurrentPlanInfos'
},
{
'1': 'redeemVoucher',
'1': 'deprecated_11',
'3': 11,
'4': 1,
'5': 11,
'6': '.client_to_server.ApplicationData.RedeemVoucher',
'6': '.client_to_server.ApplicationData.Deprecated',
'9': 0,
'10': 'redeemVoucher'
'10': 'deprecated11'
},
{
'1': 'getAvailablePlans',
@ -357,58 +423,58 @@ const ApplicationData$json = {
'10': 'getAvailablePlans'
},
{
'1': 'createVoucher',
'1': 'deprecated_13',
'3': 13,
'4': 1,
'5': 11,
'6': '.client_to_server.ApplicationData.CreateVoucher',
'6': '.client_to_server.ApplicationData.Deprecated',
'9': 0,
'10': 'createVoucher'
'10': 'deprecated13'
},
{
'1': 'getVouchers',
'1': 'deprecated_14',
'3': 14,
'4': 1,
'5': 11,
'6': '.client_to_server.ApplicationData.GetVouchers',
'6': '.client_to_server.ApplicationData.Deprecated',
'9': 0,
'10': 'getVouchers'
'10': 'deprecated14'
},
{
'1': 'switchtoPayedPlan',
'1': 'deprecated_15',
'3': 15,
'4': 1,
'5': 11,
'6': '.client_to_server.ApplicationData.SwitchToPayedPlan',
'6': '.client_to_server.ApplicationData.Deprecated',
'9': 0,
'10': 'switchtoPayedPlan'
'10': 'deprecated15'
},
{
'1': 'getAddaccountsInvites',
'1': 'deprecated_16',
'3': 16,
'4': 1,
'5': 11,
'6': '.client_to_server.ApplicationData.GetAddAccountsInvites',
'6': '.client_to_server.ApplicationData.Deprecated',
'9': 0,
'10': 'getAddaccountsInvites'
'10': 'deprecated16'
},
{
'1': 'redeemAdditionalCode',
'1': 'deprecated_17',
'3': 17,
'4': 1,
'5': 11,
'6': '.client_to_server.ApplicationData.RedeemAdditionalCode',
'6': '.client_to_server.ApplicationData.Deprecated',
'9': 0,
'10': 'redeemAdditionalCode'
'10': 'deprecated17'
},
{
'1': 'updatePlanOptions',
'1': 'deprecated_19',
'3': 19,
'4': 1,
'5': 11,
'6': '.client_to_server.ApplicationData.UpdatePlanOptions',
'6': '.client_to_server.ApplicationData.Deprecated',
'9': 0,
'10': 'updatePlanOptions'
'10': 'deprecated19'
},
{
'1': 'downloadDone',
@ -500,6 +566,15 @@ const ApplicationData$json = {
'9': 0,
'10': 'addAdditionalUser'
},
{
'1': 'set_login_token',
'3': 30,
'4': 1,
'5': 11,
'6': '.client_to_server.ApplicationData.SetLoginToken',
'9': 0,
'10': 'setLoginToken'
},
],
'3': [
ApplicationData_TextMessage$json,
@ -507,16 +582,9 @@ const ApplicationData$json = {
ApplicationData_ChangeUsername$json,
ApplicationData_UpdateGoogleFcmToken$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_GetAddAccountsInvites$json,
ApplicationData_GetCurrentPlanInfos$json,
ApplicationData_RedeemAdditionalCode$json,
ApplicationData_RemoveAdditionalUser$json,
ApplicationData_GetPrekeysByUserId$json,
ApplicationData_GetSignedPreKeyByUserId$json,
@ -526,7 +594,9 @@ const ApplicationData$json = {
ApplicationData_IPAPurchase$json,
ApplicationData_IPAForceCheck$json,
ApplicationData_DeleteAccount$json,
ApplicationData_AddAdditionalUser$json
ApplicationData_AddAdditionalUser$json,
ApplicationData_SetLoginToken$json,
ApplicationData_Deprecated$json
],
'8': [
{'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')
const ApplicationData_GetAvailablePlans$json = {
'1': 'GetAvailablePlans',
@ -645,14 +671,6 @@ const ApplicationData_GetCurrentPlanInfos$json = {
'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')
const ApplicationData_RemoveAdditionalUser$json = {
'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`.
final $typed_data.Uint8List applicationDataDescriptor = $convert.base64Decode(
'Cg9BcHBsaWNhdGlvbkRhdGESUQoLdGV4dE1lc3NhZ2UYASABKAsyLS5jbGllbnRfdG9fc2Vydm'
@ -755,66 +786,61 @@ final $typed_data.Uint8List applicationDataDescriptor = $convert.base64Decode(
'bnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5HZXRVc2VyQnlJZEgAUgtnZXRVc2VyQnlJZB'
'JsChR1cGRhdGVHb29nbGVGY21Ub2tlbhgIIAEoCzI2LmNsaWVudF90b19zZXJ2ZXIuQXBwbGlj'
'YXRpb25EYXRhLlVwZGF0ZUdvb2dsZUZjbVRva2VuSABSFHVwZGF0ZUdvb2dsZUZjbVRva2VuEl'
'EKC2dldExvY2F0aW9uGAkgASgLMi0uY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEu'
'R2V0TG9jYXRpb25IAFILZ2V0TG9jYXRpb24SaQoTZ2V0Q3VycmVudFBsYW5JbmZvcxgKIAEoCz'
'EKDGRlcHJlY2F0ZWRfORgJIAEoCzIsLmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRh'
'LkRlcHJlY2F0ZWRIAFILZGVwcmVjYXRlZDkSaQoTZ2V0Q3VycmVudFBsYW5JbmZvcxgKIAEoCz'
'I1LmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkdldEN1cnJlbnRQbGFuSW5mb3NI'
'AFITZ2V0Q3VycmVudFBsYW5JbmZvcxJXCg1yZWRlZW1Wb3VjaGVyGAsgASgLMi8uY2xpZW50X3'
'RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuUmVkZWVtVm91Y2hlckgAUg1yZWRlZW1Wb3VjaGVy'
'EmMKEWdldEF2YWlsYWJsZVBsYW5zGAwgASgLMjMuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdG'
'lvbkRhdGEuR2V0QXZhaWxhYmxlUGxhbnNIAFIRZ2V0QXZhaWxhYmxlUGxhbnMSVwoNY3JlYXRl'
'Vm91Y2hlchgNIAEoCzIvLmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkNyZWF0ZV'
'ZvdWNoZXJIAFINY3JlYXRlVm91Y2hlchJRCgtnZXRWb3VjaGVycxgOIAEoCzItLmNsaWVudF90'
'b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkdldFZvdWNoZXJzSABSC2dldFZvdWNoZXJzEmMKEX'
'N3aXRjaHRvUGF5ZWRQbGFuGA8gASgLMjMuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRh'
'dGEuU3dpdGNoVG9QYXllZFBsYW5IAFIRc3dpdGNodG9QYXllZFBsYW4SbwoVZ2V0QWRkYWNjb3'
'VudHNJbnZpdGVzGBAgASgLMjcuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuR2V0'
'QWRkQWNjb3VudHNJbnZpdGVzSABSFWdldEFkZGFjY291bnRzSW52aXRlcxJsChRyZWRlZW1BZG'
'RpdGlvbmFsQ29kZRgRIAEoCzI2LmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLlJl'
'ZGVlbUFkZGl0aW9uYWxDb2RlSABSFHJlZGVlbUFkZGl0aW9uYWxDb2RlEmMKEXVwZGF0ZVBsYW'
'5PcHRpb25zGBMgASgLMjMuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuVXBkYXRl'
'UGxhbk9wdGlvbnNIAFIRdXBkYXRlUGxhbk9wdGlvbnMSVAoMZG93bmxvYWREb25lGBQgASgLMi'
'4uY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuRG93bmxvYWREb25lSABSDGRvd25s'
'b2FkRG9uZRJ1ChdnZXRTaWduZWRQcmVrZXlCeVVzZXJpZBgWIAEoCzI5LmNsaWVudF90b19zZX'
'J2ZXIuQXBwbGljYXRpb25EYXRhLkdldFNpZ25lZFByZUtleUJ5VXNlcklkSABSF2dldFNpZ25l'
'ZFByZWtleUJ5VXNlcmlkEmYKEnVwZGF0ZVNpZ25lZFByZWtleRgXIAEoCzI0LmNsaWVudF90b1'
'9zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLlVwZGF0ZVNpZ25lZFByZUtleUgAUhJ1cGRhdGVTaWdu'
'ZWRQcmVrZXkSVwoNZGVsZXRlQWNjb3VudBgYIAEoCzIvLmNsaWVudF90b19zZXJ2ZXIuQXBwbG'
'ljYXRpb25EYXRhLkRlbGV0ZUFjY291bnRIAFINZGVsZXRlQWNjb3VudBJOCgpyZXBvcnRVc2Vy'
'GBkgASgLMiwuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuUmVwb3J0VXNlckgAUg'
'pyZXBvcnRVc2VyEloKDmNoYW5nZVVzZXJuYW1lGBogASgLMjAuY2xpZW50X3RvX3NlcnZlci5B'
'cHBsaWNhdGlvbkRhdGEuQ2hhbmdlVXNlcm5hbWVIAFIOY2hhbmdlVXNlcm5hbWUSUQoLaXBhUH'
'VyY2hhc2UYGyABKAsyLS5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5JUEFQdXJj'
'aGFzZUgAUgtpcGFQdXJjaGFzZRJXCg1pcGFGb3JjZUNoZWNrGBwgASgLMi8uY2xpZW50X3RvX3'
'NlcnZlci5BcHBsaWNhdGlvbkRhdGEuSVBBRm9yY2VDaGVja0gAUg1pcGFGb3JjZUNoZWNrEmwK'
'FHJlbW92ZUFkZGl0aW9uYWxVc2VyGBIgASgLMjYuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdG'
'lvbkRhdGEuUmVtb3ZlQWRkaXRpb25hbFVzZXJIAFIUcmVtb3ZlQWRkaXRpb25hbFVzZXISYwoR'
'YWRkQWRkaXRpb25hbFVzZXIYHSABKAsyMy5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRG'
'F0YS5BZGRBZGRpdGlvbmFsVXNlckgAUhFhZGRBZGRpdGlvbmFsVXNlchpqCgtUZXh0TWVzc2Fn'
'AFITZ2V0Q3VycmVudFBsYW5JbmZvcxJTCg1kZXByZWNhdGVkXzExGAsgASgLMiwuY2xpZW50X3'
'RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuRGVwcmVjYXRlZEgAUgxkZXByZWNhdGVkMTESYwoR'
'Z2V0QXZhaWxhYmxlUGxhbnMYDCABKAsyMy5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRG'
'F0YS5HZXRBdmFpbGFibGVQbGFuc0gAUhFnZXRBdmFpbGFibGVQbGFucxJTCg1kZXByZWNhdGVk'
'XzEzGA0gASgLMiwuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuRGVwcmVjYXRlZE'
'gAUgxkZXByZWNhdGVkMTMSUwoNZGVwcmVjYXRlZF8xNBgOIAEoCzIsLmNsaWVudF90b19zZXJ2'
'ZXIuQXBwbGljYXRpb25EYXRhLkRlcHJlY2F0ZWRIAFIMZGVwcmVjYXRlZDE0ElMKDWRlcHJlY2'
'F0ZWRfMTUYDyABKAsyLC5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5EZXByZWNh'
'dGVkSABSDGRlcHJlY2F0ZWQxNRJTCg1kZXByZWNhdGVkXzE2GBAgASgLMiwuY2xpZW50X3RvX3'
'NlcnZlci5BcHBsaWNhdGlvbkRhdGEuRGVwcmVjYXRlZEgAUgxkZXByZWNhdGVkMTYSUwoNZGVw'
'cmVjYXRlZF8xNxgRIAEoCzIsLmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkRlcH'
'JlY2F0ZWRIAFIMZGVwcmVjYXRlZDE3ElMKDWRlcHJlY2F0ZWRfMTkYEyABKAsyLC5jbGllbnRf'
'dG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5EZXByZWNhdGVkSABSDGRlcHJlY2F0ZWQxORJUCg'
'xkb3dubG9hZERvbmUYFCABKAsyLi5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5E'
'b3dubG9hZERvbmVIAFIMZG93bmxvYWREb25lEnUKF2dldFNpZ25lZFByZWtleUJ5VXNlcmlkGB'
'YgASgLMjkuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuR2V0U2lnbmVkUHJlS2V5'
'QnlVc2VySWRIAFIXZ2V0U2lnbmVkUHJla2V5QnlVc2VyaWQSZgoSdXBkYXRlU2lnbmVkUHJla2'
'V5GBcgASgLMjQuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuVXBkYXRlU2lnbmVk'
'UHJlS2V5SABSEnVwZGF0ZVNpZ25lZFByZWtleRJXCg1kZWxldGVBY2NvdW50GBggASgLMi8uY2'
'xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuRGVsZXRlQWNjb3VudEgAUg1kZWxldGVB'
'Y2NvdW50Ek4KCnJlcG9ydFVzZXIYGSABKAsyLC5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW'
'9uRGF0YS5SZXBvcnRVc2VySABSCnJlcG9ydFVzZXISWgoOY2hhbmdlVXNlcm5hbWUYGiABKAsy'
'MC5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5DaGFuZ2VVc2VybmFtZUgAUg5jaG'
'FuZ2VVc2VybmFtZRJRCgtpcGFQdXJjaGFzZRgbIAEoCzItLmNsaWVudF90b19zZXJ2ZXIuQXBw'
'bGljYXRpb25EYXRhLklQQVB1cmNoYXNlSABSC2lwYVB1cmNoYXNlElcKDWlwYUZvcmNlQ2hlY2'
'sYHCABKAsyLy5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5JUEFGb3JjZUNoZWNr'
'SABSDWlwYUZvcmNlQ2hlY2sSbAoUcmVtb3ZlQWRkaXRpb25hbFVzZXIYEiABKAsyNi5jbGllbn'
'RfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5SZW1vdmVBZGRpdGlvbmFsVXNlckgAUhRyZW1v'
'dmVBZGRpdGlvbmFsVXNlchJjChFhZGRBZGRpdGlvbmFsVXNlchgdIAEoCzIzLmNsaWVudF90b1'
'9zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkFkZEFkZGl0aW9uYWxVc2VySABSEWFkZEFkZGl0aW9u'
'YWxVc2VyElkKD3NldF9sb2dpbl90b2tlbhgeIAEoCzIvLmNsaWVudF90b19zZXJ2ZXIuQXBwbG'
'ljYXRpb25EYXRhLlNldExvZ2luVG9rZW5IAFINc2V0TG9naW5Ub2tlbhpqCgtUZXh0TWVzc2Fn'
'ZRIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQSEgoEYm9keRgDIAEoDFIEYm9keRIgCglwdXNoX2'
'RhdGEYBCABKAxIAFIIcHVzaERhdGGIAQFCDAoKX3B1c2hfZGF0YRovChFHZXRVc2VyQnlVc2Vy'
'bmFtZRIaCgh1c2VybmFtZRgBIAEoCVIIdXNlcm5hbWUaLAoOQ2hhbmdlVXNlcm5hbWUSGgoIdX'
'Nlcm5hbWUYASABKAlSCHVzZXJuYW1lGjUKFFVwZGF0ZUdvb2dsZUZjbVRva2VuEh0KCmdvb2ds'
'ZV9mY20YASABKAlSCWdvb2dsZUZjbRomCgtHZXRVc2VyQnlJZBIXCgd1c2VyX2lkGAEgASgDUg'
'Z1c2VySWQaKQoNUmVkZWVtVm91Y2hlchIYCgd2b3VjaGVyGAEgASgJUgd2b3VjaGVyGnAKEVN3'
'aXRjaFRvUGF5ZWRQbGFuEhcKB3BsYW5faWQYASABKAlSBnBsYW5JZBIfCgtwYXlfbW9udGhseR'
'gCIAEoCFIKcGF5TW9udGhseRIhCgxhdXRvX3JlbmV3YWwYAyABKAhSC2F1dG9SZW5ld2FsGjYK'
'EVVwZGF0ZVBsYW5PcHRpb25zEiEKDGF1dG9fcmVuZXdhbBgBIAEoCFILYXV0b1JlbmV3YWwaMA'
'oNQ3JlYXRlVm91Y2hlchIfCgt2YWx1ZV9jZW50cxgBIAEoDVIKdmFsdWVDZW50cxoNCgtHZXRM'
'b2NhdGlvbhoNCgtHZXRWb3VjaGVycxoTChFHZXRBdmFpbGFibGVQbGFucxoXChVHZXRBZGRBY2'
'NvdW50c0ludml0ZXMaFQoTR2V0Q3VycmVudFBsYW5JbmZvcxo3ChRSZWRlZW1BZGRpdGlvbmFs'
'Q29kZRIfCgtpbnZpdGVfY29kZRgCIAEoCVIKaW52aXRlQ29kZRovChRSZW1vdmVBZGRpdGlvbm'
'FsVXNlchIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQaLQoSR2V0UHJla2V5c0J5VXNlcklkEhcK'
'B3VzZXJfaWQYASABKANSBnVzZXJJZBoyChdHZXRTaWduZWRQcmVLZXlCeVVzZXJJZBIXCgd1c2'
'VyX2lkGAEgASgDUgZ1c2VySWQamwEKElVwZGF0ZVNpZ25lZFByZUtleRIoChBzaWduZWRfcHJl'
'a2V5X2lkGAEgASgDUg5zaWduZWRQcmVrZXlJZBIjCg1zaWduZWRfcHJla2V5GAIgASgMUgxzaW'
'duZWRQcmVrZXkSNgoXc2lnbmVkX3ByZWtleV9zaWduYXR1cmUYAyABKAxSFXNpZ25lZFByZWtl'
'eVNpZ25hdHVyZRo1CgxEb3dubG9hZERvbmUSJQoOZG93bmxvYWRfdG9rZW4YASABKAxSDWRvd2'
'5sb2FkVG9rZW4aTgoKUmVwb3J0VXNlchIoChByZXBvcnRlZF91c2VyX2lkGAEgASgDUg5yZXBv'
'cnRlZFVzZXJJZBIWCgZyZWFzb24YAiABKAlSBnJlYXNvbhpxCgtJUEFQdXJjaGFzZRIdCgpwcm'
'9kdWN0X2lkGAEgASgJUglwcm9kdWN0SWQSFgoGc291cmNlGAIgASgJUgZzb3VyY2USKwoRdmVy'
'aWZpY2F0aW9uX2RhdGEYAyABKAlSEHZlcmlmaWNhdGlvbkRhdGEaDwoNSVBBRm9yY2VDaGVjax'
'oPCg1EZWxldGVBY2NvdW50GiwKEUFkZEFkZGl0aW9uYWxVc2VyEhcKB3VzZXJfaWQYASABKANS'
'BnVzZXJJZEIRCg9BcHBsaWNhdGlvbkRhdGE=');
'Z1c2VySWQaEwoRR2V0QXZhaWxhYmxlUGxhbnMaFwoVR2V0QWRkQWNjb3VudHNJbnZpdGVzGhUK'
'E0dldEN1cnJlbnRQbGFuSW5mb3MaLwoUUmVtb3ZlQWRkaXRpb25hbFVzZXISFwoHdXNlcl9pZB'
'gBIAEoA1IGdXNlcklkGi0KEkdldFByZWtleXNCeVVzZXJJZBIXCgd1c2VyX2lkGAEgASgDUgZ1'
'c2VySWQaMgoXR2V0U2lnbmVkUHJlS2V5QnlVc2VySWQSFwoHdXNlcl9pZBgBIAEoA1IGdXNlck'
'lkGpsBChJVcGRhdGVTaWduZWRQcmVLZXkSKAoQc2lnbmVkX3ByZWtleV9pZBgBIAEoA1IOc2ln'
'bmVkUHJla2V5SWQSIwoNc2lnbmVkX3ByZWtleRgCIAEoDFIMc2lnbmVkUHJla2V5EjYKF3NpZ2'
'5lZF9wcmVrZXlfc2lnbmF0dXJlGAMgASgMUhVzaWduZWRQcmVrZXlTaWduYXR1cmUaNQoMRG93'
'bmxvYWREb25lEiUKDmRvd25sb2FkX3Rva2VuGAEgASgMUg1kb3dubG9hZFRva2VuGk4KClJlcG'
'9ydFVzZXISKAoQcmVwb3J0ZWRfdXNlcl9pZBgBIAEoA1IOcmVwb3J0ZWRVc2VySWQSFgoGcmVh'
'c29uGAIgASgJUgZyZWFzb24acQoLSVBBUHVyY2hhc2USHQoKcHJvZHVjdF9pZBgBIAEoCVIJcH'
'JvZHVjdElkEhYKBnNvdXJjZRgCIAEoCVIGc291cmNlEisKEXZlcmlmaWNhdGlvbl9kYXRhGAMg'
'ASgJUhB2ZXJpZmljYXRpb25EYXRhGg8KDUlQQUZvcmNlQ2hlY2saDwoNRGVsZXRlQWNjb3VudB'
'osChFBZGRBZGRpdGlvbmFsVXNlchIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQaMAoNU2V0TG9n'
'aW5Ub2tlbhIfCgtsb2dpbl90b2tlbhgBIAEoDFIKbG9naW5Ub2tlbhoMCgpEZXByZWNhdGVkQh'
'EKD0FwcGxpY2F0aW9uRGF0YQ==');
@$core.Deprecated('Use responseDescriptor instead')
const Response$json = {

View file

@ -16,12 +16,9 @@ import 'package:fixnum/fixnum.dart' as $fixnum;
import 'package:protobuf/protobuf.dart' as $pb;
import 'error.pbenum.dart' as $0;
import 'server_to_client.pbenum.dart';
export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions;
export 'server_to_client.pbenum.dart';
enum ServerToClient_V { v0, notSet }
class ServerToClient extends $pb.GeneratedMessage {
@ -752,90 +749,6 @@ class Response_AddAccountsInvites extends $pb.GeneratedMessage {
$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 {
factory Response_AdditionalAccount({
$fixnum.Int64? userId,
@ -905,158 +818,82 @@ class Response_AdditionalAccount extends $pb.GeneratedMessage {
void clearPlanId() => $_clearField(3);
}
class Response_Voucher extends $pb.GeneratedMessage {
factory Response_Voucher({
$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;
}
class Response_Deprecated extends $pb.GeneratedMessage {
factory Response_Deprecated() => create();
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]) =>
create()..mergeFromBuffer(data, registry);
factory Response_Voucher.fromJson($core.String json,
factory Response_Deprecated.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'Response.Voucher',
_omitMessageNames ? '' : 'Response.Deprecated',
package:
const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'),
createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'voucherId')
..aInt64(2, _omitFieldNames ? '' : 'valueCents')
..aOB(3, _omitFieldNames ? '' : 'redeemed')
..aOB(4, _omitFieldNames ? '' : 'requested')
..aInt64(5, _omitFieldNames ? '' : 'createdAtUnixTimestamp')
..hasRequiredFields = false;
@$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.')
Response_Voucher copyWith(void Function(Response_Voucher) updates) =>
super.copyWith((message) => updates(message as Response_Voucher))
as Response_Voucher;
Response_Deprecated copyWith(void Function(Response_Deprecated) updates) =>
super.copyWith((message) => updates(message as Response_Deprecated))
as Response_Deprecated;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static Response_Voucher create() => Response_Voucher._();
static Response_Deprecated create() => Response_Deprecated._();
@$core.override
Response_Voucher createEmptyInstance() => create();
Response_Deprecated createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static Response_Voucher getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<Response_Voucher>(create);
static Response_Voucher? _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);
static Response_Deprecated getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<Response_Deprecated>(create);
static Response_Deprecated? _defaultInstance;
}
class Response_Vouchers extends $pb.GeneratedMessage {
factory Response_Vouchers({
$core.Iterable<Response_Voucher>? vouchers,
}) {
final result = create();
if (vouchers != null) result.vouchers.addAll(vouchers);
return result;
}
class Response_Transaction extends $pb.GeneratedMessage {
factory Response_Transaction() => create();
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]) =>
create()..mergeFromBuffer(data, registry);
factory Response_Vouchers.fromJson($core.String json,
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.Vouchers',
_omitMessageNames ? '' : 'Response.Transaction',
package:
const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'),
createEmptyInstance: create)
..pPM<Response_Voucher>(1, _omitFieldNames ? '' : 'vouchers',
subBuilder: Response_Voucher.create)
..hasRequiredFields = false;
@$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.')
Response_Vouchers copyWith(void Function(Response_Vouchers) updates) =>
super.copyWith((message) => updates(message as Response_Vouchers))
as Response_Vouchers;
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_Vouchers create() => Response_Vouchers._();
static Response_Transaction create() => Response_Transaction._();
@$core.override
Response_Vouchers createEmptyInstance() => create();
Response_Transaction createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static Response_Vouchers getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<Response_Vouchers>(create);
static Response_Vouchers? _defaultInstance;
@$pb.TagNumber(1)
$pb.PbList<Response_Voucher> get vouchers => $_getList(0);
static Response_Transaction getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<Response_Transaction>(create);
static Response_Transaction? _defaultInstance;
}
class Response_PlanBallance extends $pb.GeneratedMessage {
@ -1195,85 +1032,6 @@ class Response_PlanBallance extends $pb.GeneratedMessage {
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 {
factory Response_PreKey({
$fixnum.Int64? id,
@ -1754,11 +1512,11 @@ enum Response_Ok_Ok {
uploadtoken,
userdata,
authtoken,
location,
deprecated7,
authenticated,
plans,
planballance,
vouchers,
deprecated11,
addaccountsinvites,
downloadtokens,
signedprekey,
@ -1774,11 +1532,11 @@ class Response_Ok extends $pb.GeneratedMessage {
Response_UploadToken? uploadtoken,
Response_UserData? userdata,
$core.List<$core.int>? authtoken,
Response_Location? location,
Response_Deprecated? deprecated7,
Response_Authenticated? authenticated,
Response_Plans? plans,
Response_PlanBallance? planballance,
Response_Vouchers? vouchers,
Response_Deprecated? deprecated11,
Response_AddAccountsInvites? addaccountsinvites,
Response_DownloadTokens? downloadtokens,
Response_SignedPreKey? signedprekey,
@ -1791,11 +1549,11 @@ class Response_Ok extends $pb.GeneratedMessage {
if (uploadtoken != null) result.uploadtoken = uploadtoken;
if (userdata != null) result.userdata = userdata;
if (authtoken != null) result.authtoken = authtoken;
if (location != null) result.location = location;
if (deprecated7 != null) result.deprecated7 = deprecated7;
if (authenticated != null) result.authenticated = authenticated;
if (plans != null) result.plans = plans;
if (planballance != null) result.planballance = planballance;
if (vouchers != null) result.vouchers = vouchers;
if (deprecated11 != null) result.deprecated11 = deprecated11;
if (addaccountsinvites != null)
result.addaccountsinvites = addaccountsinvites;
if (downloadtokens != null) result.downloadtokens = downloadtokens;
@ -1820,11 +1578,11 @@ class Response_Ok extends $pb.GeneratedMessage {
4: Response_Ok_Ok.uploadtoken,
5: Response_Ok_Ok.userdata,
6: Response_Ok_Ok.authtoken,
7: Response_Ok_Ok.location,
7: Response_Ok_Ok.deprecated7,
8: Response_Ok_Ok.authenticated,
9: Response_Ok_Ok.plans,
10: Response_Ok_Ok.planballance,
11: Response_Ok_Ok.vouchers,
11: Response_Ok_Ok.deprecated11,
12: Response_Ok_Ok.addaccountsinvites,
13: Response_Ok_Ok.downloadtokens,
14: Response_Ok_Ok.signedprekey,
@ -1847,16 +1605,16 @@ class Response_Ok extends $pb.GeneratedMessage {
subBuilder: Response_UserData.create)
..a<$core.List<$core.int>>(
6, _omitFieldNames ? '' : 'authtoken', $pb.PbFieldType.OY)
..aOM<Response_Location>(7, _omitFieldNames ? '' : 'location',
subBuilder: Response_Location.create)
..aOM<Response_Deprecated>(7, _omitFieldNames ? '' : 'deprecated7',
protoName: 'deprecated_7', subBuilder: Response_Deprecated.create)
..aOM<Response_Authenticated>(8, _omitFieldNames ? '' : 'authenticated',
subBuilder: Response_Authenticated.create)
..aOM<Response_Plans>(9, _omitFieldNames ? '' : 'plans',
subBuilder: Response_Plans.create)
..aOM<Response_PlanBallance>(10, _omitFieldNames ? '' : 'planballance',
subBuilder: Response_PlanBallance.create)
..aOM<Response_Vouchers>(11, _omitFieldNames ? '' : 'vouchers',
subBuilder: Response_Vouchers.create)
..aOM<Response_Deprecated>(11, _omitFieldNames ? '' : 'deprecated11',
protoName: 'deprecated_11', subBuilder: Response_Deprecated.create)
..aOM<Response_AddAccountsInvites>(
12, _omitFieldNames ? '' : 'addaccountsinvites',
subBuilder: Response_AddAccountsInvites.create)
@ -1979,15 +1737,15 @@ class Response_Ok extends $pb.GeneratedMessage {
void clearAuthtoken() => $_clearField(6);
@$pb.TagNumber(7)
Response_Location get location => $_getN(6);
Response_Deprecated get deprecated7 => $_getN(6);
@$pb.TagNumber(7)
set location(Response_Location value) => $_setField(7, value);
set deprecated7(Response_Deprecated value) => $_setField(7, value);
@$pb.TagNumber(7)
$core.bool hasLocation() => $_has(6);
$core.bool hasDeprecated7() => $_has(6);
@$pb.TagNumber(7)
void clearLocation() => $_clearField(7);
void clearDeprecated7() => $_clearField(7);
@$pb.TagNumber(7)
Response_Location ensureLocation() => $_ensure(6);
Response_Deprecated ensureDeprecated7() => $_ensure(6);
@$pb.TagNumber(8)
Response_Authenticated get authenticated => $_getN(7);
@ -2023,15 +1781,15 @@ class Response_Ok extends $pb.GeneratedMessage {
Response_PlanBallance ensurePlanballance() => $_ensure(9);
@$pb.TagNumber(11)
Response_Vouchers get vouchers => $_getN(10);
Response_Deprecated get deprecated11 => $_getN(10);
@$pb.TagNumber(11)
set vouchers(Response_Vouchers value) => $_setField(11, value);
set deprecated11(Response_Deprecated value) => $_setField(11, value);
@$pb.TagNumber(11)
$core.bool hasVouchers() => $_has(10);
$core.bool hasDeprecated11() => $_has(10);
@$pb.TagNumber(11)
void clearVouchers() => $_clearField(11);
void clearDeprecated11() => $_clearField(11);
@$pb.TagNumber(11)
Response_Vouchers ensureVouchers() => $_ensure(10);
Response_Deprecated ensureDeprecated11() => $_ensure(10);
@$pb.TagNumber(12)
Response_AddAccountsInvites get addaccountsinvites => $_getN(11);

View file

@ -9,48 +9,3 @@
// ignore_for_file: curly_braces_in_flow_control_structures
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
// 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_AddAccountsInvite$json,
Response_AddAccountsInvites$json,
Response_Transaction$json,
Response_AdditionalAccount$json,
Response_Voucher$json,
Response_Vouchers$json,
Response_Deprecated$json,
Response_Transaction$json,
Response_PlanBallance$json,
Response_Location$json,
Response_PreKey$json,
Response_SignedPreKey$json,
Response_UserData$json,
@ -180,7 +178,6 @@ const Response$json = {
Response_ProofOfWork$json,
Response_Ok$json
],
'4': [Response_TransactionTypes$json],
'8': [
{'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')
const Response_AdditionalAccount$json = {
'1': 'AdditionalAccount',
@ -318,36 +292,13 @@ const Response_AdditionalAccount$json = {
};
@$core.Deprecated('Use responseDescriptor instead')
const Response_Voucher$json = {
'1': 'Voucher',
'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'
},
],
const Response_Deprecated$json = {
'1': 'Deprecated',
};
@$core.Deprecated('Use responseDescriptor instead')
const Response_Vouchers$json = {
'1': 'Vouchers',
'2': [
{
'1': 'vouchers',
'3': 1,
'4': 3,
'5': 11,
'6': '.server_to_client.Response.Voucher',
'10': 'vouchers'
},
],
const Response_Transaction$json = {
'1': 'Transaction',
};
@$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')
const Response_PreKey$json = {
'1': 'PreKey',
@ -602,13 +543,13 @@ const Response_Ok$json = {
},
{'1': 'authtoken', '3': 6, '4': 1, '5': 12, '9': 0, '10': 'authtoken'},
{
'1': 'location',
'1': 'deprecated_7',
'3': 7,
'4': 1,
'5': 11,
'6': '.server_to_client.Response.Location',
'6': '.server_to_client.Response.Deprecated',
'9': 0,
'10': 'location'
'10': 'deprecated7'
},
{
'1': 'authenticated',
@ -638,13 +579,13 @@ const Response_Ok$json = {
'10': 'planballance'
},
{
'1': 'vouchers',
'1': 'deprecated_11',
'3': 11,
'4': 1,
'5': 11,
'6': '.server_to_client.Response.Vouchers',
'6': '.server_to_client.Response.Deprecated',
'9': 0,
'10': 'vouchers'
'10': 'deprecated11'
},
{
'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`.
final $typed_data.Uint8List responseDescriptor = $convert.base64Decode(
'CghSZXNwb25zZRIvCgJvaxgBIAEoCzIdLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuT2tIAF'
@ -720,64 +646,53 @@ final $typed_data.Uint8List responseDescriptor = $convert.base64Decode(
'bnQuUmVzcG9uc2UuUGxhblIFcGxhbnMaTQoRQWRkQWNjb3VudHNJbnZpdGUSFwoHcGxhbl9pZB'
'gBIAEoCVIGcGxhbklkEh8KC2ludml0ZV9jb2RlGAIgASgJUgppbnZpdGVDb2RlGlwKEkFkZEFj'
'Y291bnRzSW52aXRlcxJGCgdpbnZpdGVzGAEgAygLMiwuc2VydmVyX3RvX2NsaWVudC5SZXNwb2'
'5zZS5BZGRBY2NvdW50c0ludml0ZVIHaW52aXRlcxrFAQoLVHJhbnNhY3Rpb24SIwoNZGVwb3Np'
'dF9jZW50cxgBIAEoA1IMZGVwb3NpdENlbnRzElYKEHRyYW5zYWN0aW9uX3R5cGUYAiABKA4yKy'
'5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlRyYW5zYWN0aW9uVHlwZXNSD3RyYW5zYWN0aW9u'
'VHlwZRI5ChljcmVhdGVkX2F0X3VuaXhfdGltZXN0YW1wGAMgASgDUhZjcmVhdGVkQXRVbml4VG'
'ltZXN0YW1wGkUKEUFkZGl0aW9uYWxBY2NvdW50EhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBIX'
'CgdwbGFuX2lkGAMgASgJUgZwbGFuSWQavgEKB1ZvdWNoZXISHQoKdm91Y2hlcl9pZBgBIAEoCV'
'IJdm91Y2hlcklkEh8KC3ZhbHVlX2NlbnRzGAIgASgDUgp2YWx1ZUNlbnRzEhoKCHJlZGVlbWVk'
'GAMgASgIUghyZWRlZW1lZBIcCglyZXF1ZXN0ZWQYBCABKAhSCXJlcXVlc3RlZBI5ChljcmVhdG'
'VkX2F0X3VuaXhfdGltZXN0YW1wGAUgASgDUhZjcmVhdGVkQXRVbml4VGltZXN0YW1wGkoKCFZv'
'dWNoZXJzEj4KCHZvdWNoZXJzGAEgAygLMiIuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5Wb3'
'VjaGVyUgh2b3VjaGVycxqXBQoMUGxhbkJhbGxhbmNlEkAKHXVzZWRfZGFpbHlfbWVkaWFfdXBs'
'b2FkX2xpbWl0GAEgASgDUhl1c2VkRGFpbHlNZWRpYVVwbG9hZExpbWl0Ej4KHHVzZWRfdXBsb2'
'FkX21lZGlhX3NpemVfbGltaXQYAiABKANSGHVzZWRVcGxvYWRNZWRpYVNpemVMaW1pdBIzChNw'
'YXltZW50X3BlcmlvZF9kYXlzGAMgASgDSABSEXBheW1lbnRQZXJpb2REYXlziAEBEksKIGxhc3'
'RfcGF5bWVudF9kb25lX3VuaXhfdGltZXN0YW1wGAQgASgDSAFSHGxhc3RQYXltZW50RG9uZVVu'
'aXhUaW1lc3RhbXCIAQESSgoMdHJhbnNhY3Rpb25zGAUgAygLMiYuc2VydmVyX3RvX2NsaWVudC'
'5SZXNwb25zZS5UcmFuc2FjdGlvblIMdHJhbnNhY3Rpb25zEl0KE2FkZGl0aW9uYWxfYWNjb3Vu'
'dHMYBiADKAsyLC5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLkFkZGl0aW9uYWxBY2NvdW50Uh'
'JhZGRpdGlvbmFsQWNjb3VudHMSJgoMYXV0b19yZW5ld2FsGAcgASgISAJSC2F1dG9SZW5ld2Fs'
'iAEBEkIKG2FkZGl0aW9uYWxfYWNjb3VudF9vd25lcl9pZBgIIAEoA0gDUhhhZGRpdGlvbmFsQW'
'Njb3VudE93bmVySWSIAQFCFgoUX3BheW1lbnRfcGVyaW9kX2RheXNCIwohX2xhc3RfcGF5bWVu'
'dF9kb25lX3VuaXhfdGltZXN0YW1wQg8KDV9hdXRvX3JlbmV3YWxCHgocX2FkZGl0aW9uYWxfYW'
'Njb3VudF9vd25lcl9pZBpOCghMb2NhdGlvbhIWCgZjb3VudHkYASABKAlSBmNvdW50eRIWCgZy'
'ZWdpb24YAiABKAlSBnJlZ2lvbhISCgRjaXR5GAMgASgJUgRjaXR5GjAKBlByZUtleRIOCgJpZB'
'gBIAEoA1ICaWQSFgoGcHJla2V5GAIgASgMUgZwcmVrZXkalQEKDFNpZ25lZFByZUtleRIoChBz'
'aWduZWRfcHJla2V5X2lkGAEgASgDUg5zaWduZWRQcmVrZXlJZBIjCg1zaWduZWRfcHJla2V5GA'
'IgASgMUgxzaWduZWRQcmVrZXkSNgoXc2lnbmVkX3ByZWtleV9zaWduYXR1cmUYAyABKAxSFXNp'
'Z25lZFByZWtleVNpZ25hdHVyZRr2AwoIVXNlckRhdGESFwoHdXNlcl9pZBgBIAEoA1IGdXNlck'
'lkEjsKB3ByZWtleXMYAiADKAsyIS5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlByZUtleVIH'
'cHJla2V5cxIfCgh1c2VybmFtZRgHIAEoDEgAUgh1c2VybmFtZYgBARIzChNwdWJsaWNfaWRlbn'
'RpdHlfa2V5GAMgASgMSAFSEXB1YmxpY0lkZW50aXR5S2V5iAEBEigKDXNpZ25lZF9wcmVrZXkY'
'BCABKAxIAlIMc2lnbmVkUHJla2V5iAEBEjsKF3NpZ25lZF9wcmVrZXlfc2lnbmF0dXJlGAUgAS'
'gMSANSFXNpZ25lZFByZWtleVNpZ25hdHVyZYgBARItChBzaWduZWRfcHJla2V5X2lkGAYgASgD'
'SARSDnNpZ25lZFByZWtleUlkiAEBEiwKD3JlZ2lzdHJhdGlvbl9pZBgIIAEoA0gFUg5yZWdpc3'
'RyYXRpb25JZIgBAUILCglfdXNlcm5hbWVCFgoUX3B1YmxpY19pZGVudGl0eV9rZXlCEAoOX3Np'
'Z25lZF9wcmVrZXlCGgoYX3NpZ25lZF9wcmVrZXlfc2lnbmF0dXJlQhMKEV9zaWduZWRfcHJla2'
'V5X2lkQhIKEF9yZWdpc3RyYXRpb25faWQaWQoLVXBsb2FkVG9rZW4SIQoMdXBsb2FkX3Rva2Vu'
'GAEgASgMUgt1cGxvYWRUb2tlbhInCg9kb3dubG9hZF90b2tlbnMYAiADKAxSDmRvd25sb2FkVG'
'9rZW5zGjkKDkRvd25sb2FkVG9rZW5zEicKD2Rvd25sb2FkX3Rva2VucxgBIAMoDFIOZG93bmxv'
'YWRUb2tlbnMaRQoLUHJvb2ZPZldvcmsSFgoGcHJlZml4GAEgASgJUgZwcmVmaXgSHgoKZGlmZm'
'ljdWx0eRgCIAEoA1IKZGlmZmljdWx0eRrDBwoCT2sSFAoETm9uZRgBIAEoCEgAUgROb25lEhgK'
'BnVzZXJpZBgCIAEoA0gAUgZ1c2VyaWQSJgoNYXV0aGNoYWxsZW5nZRgDIAEoDEgAUg1hdXRoY2'
'hhbGxlbmdlEkoKC3VwbG9hZHRva2VuGAQgASgLMiYuc2VydmVyX3RvX2NsaWVudC5SZXNwb25z'
'ZS5VcGxvYWRUb2tlbkgAUgt1cGxvYWR0b2tlbhJBCgh1c2VyZGF0YRgFIAEoCzIjLnNlcnZlcl'
'90b19jbGllbnQuUmVzcG9uc2UuVXNlckRhdGFIAFIIdXNlcmRhdGESHgoJYXV0aHRva2VuGAYg'
'ASgMSABSCWF1dGh0b2tlbhJBCghsb2NhdGlvbhgHIAEoCzIjLnNlcnZlcl90b19jbGllbnQuUm'
'VzcG9uc2UuTG9jYXRpb25IAFIIbG9jYXRpb24SUAoNYXV0aGVudGljYXRlZBgIIAEoCzIoLnNl'
'cnZlcl90b19jbGllbnQuUmVzcG9uc2UuQXV0aGVudGljYXRlZEgAUg1hdXRoZW50aWNhdGVkEj'
'gKBXBsYW5zGAkgASgLMiAuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5QbGFuc0gAUgVwbGFu'
'cxJNCgxwbGFuYmFsbGFuY2UYCiABKAsyJy5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlBsYW'
'5CYWxsYW5jZUgAUgxwbGFuYmFsbGFuY2USQQoIdm91Y2hlcnMYCyABKAsyIy5zZXJ2ZXJfdG9f'
'Y2xpZW50LlJlc3BvbnNlLlZvdWNoZXJzSABSCHZvdWNoZXJzEl8KEmFkZGFjY291bnRzaW52aX'
'RlcxgMIAEoCzItLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuQWRkQWNjb3VudHNJbnZpdGVz'
'SABSEmFkZGFjY291bnRzaW52aXRlcxJTCg5kb3dubG9hZHRva2VucxgNIAEoCzIpLnNlcnZlcl'
'90b19jbGllbnQuUmVzcG9uc2UuRG93bmxvYWRUb2tlbnNIAFIOZG93bmxvYWR0b2tlbnMSTQoM'
'c2lnbmVkcHJla2V5GA4gASgLMicuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5TaWduZWRQcm'
'VLZXlIAFIMc2lnbmVkcHJla2V5EkoKC3Byb29mT2ZXb3JrGA8gASgLMiYuc2VydmVyX3RvX2Ns'
'aWVudC5SZXNwb25zZS5Qcm9vZk9mV29ya0gAUgtwcm9vZk9mV29ya0IECgJPayKWAQoQVHJhbn'
'NhY3Rpb25UeXBlcxIKCgZSZWZ1bmQQABITCg9Wb3VjaGVyUmVkZWVtZWQQARISCg5Wb3VjaGVy'
'Q3JlYXRlZBACEggKBENhc2gQAxIPCgtQbGFuVXBncmFkZRAEEgsKB1Vua25vd24QBRIUChBUaG'
'Fua3NGb3JUZXN0aW5nEAYSDwoLQXV0b1JlbmV3YWwQB0IKCghSZXNwb25zZQ==');
'5zZS5BZGRBY2NvdW50c0ludml0ZVIHaW52aXRlcxpFChFBZGRpdGlvbmFsQWNjb3VudBIXCgd1'
'c2VyX2lkGAEgASgDUgZ1c2VySWQSFwoHcGxhbl9pZBgDIAEoCVIGcGxhbklkGgwKCkRlcHJlY2'
'F0ZWQaDQoLVHJhbnNhY3Rpb24alwUKDFBsYW5CYWxsYW5jZRJACh11c2VkX2RhaWx5X21lZGlh'
'X3VwbG9hZF9saW1pdBgBIAEoA1IZdXNlZERhaWx5TWVkaWFVcGxvYWRMaW1pdBI+Chx1c2VkX3'
'VwbG9hZF9tZWRpYV9zaXplX2xpbWl0GAIgASgDUhh1c2VkVXBsb2FkTWVkaWFTaXplTGltaXQS'
'MwoTcGF5bWVudF9wZXJpb2RfZGF5cxgDIAEoA0gAUhFwYXltZW50UGVyaW9kRGF5c4gBARJLCi'
'BsYXN0X3BheW1lbnRfZG9uZV91bml4X3RpbWVzdGFtcBgEIAEoA0gBUhxsYXN0UGF5bWVudERv'
'bmVVbml4VGltZXN0YW1wiAEBEkoKDHRyYW5zYWN0aW9ucxgFIAMoCzImLnNlcnZlcl90b19jbG'
'llbnQuUmVzcG9uc2UuVHJhbnNhY3Rpb25SDHRyYW5zYWN0aW9ucxJdChNhZGRpdGlvbmFsX2Fj'
'Y291bnRzGAYgAygLMiwuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5BZGRpdGlvbmFsQWNjb3'
'VudFISYWRkaXRpb25hbEFjY291bnRzEiYKDGF1dG9fcmVuZXdhbBgHIAEoCEgCUgthdXRvUmVu'
'ZXdhbIgBARJCChthZGRpdGlvbmFsX2FjY291bnRfb3duZXJfaWQYCCABKANIA1IYYWRkaXRpb2'
'5hbEFjY291bnRPd25lcklkiAEBQhYKFF9wYXltZW50X3BlcmlvZF9kYXlzQiMKIV9sYXN0X3Bh'
'eW1lbnRfZG9uZV91bml4X3RpbWVzdGFtcEIPCg1fYXV0b19yZW5ld2FsQh4KHF9hZGRpdGlvbm'
'FsX2FjY291bnRfb3duZXJfaWQaMAoGUHJlS2V5Eg4KAmlkGAEgASgDUgJpZBIWCgZwcmVrZXkY'
'AiABKAxSBnByZWtleRqVAQoMU2lnbmVkUHJlS2V5EigKEHNpZ25lZF9wcmVrZXlfaWQYASABKA'
'NSDnNpZ25lZFByZWtleUlkEiMKDXNpZ25lZF9wcmVrZXkYAiABKAxSDHNpZ25lZFByZWtleRI2'
'ChdzaWduZWRfcHJla2V5X3NpZ25hdHVyZRgDIAEoDFIVc2lnbmVkUHJla2V5U2lnbmF0dXJlGv'
'YDCghVc2VyRGF0YRIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQSOwoHcHJla2V5cxgCIAMoCzIh'
'LnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuUHJlS2V5UgdwcmVrZXlzEh8KCHVzZXJuYW1lGA'
'cgASgMSABSCHVzZXJuYW1liAEBEjMKE3B1YmxpY19pZGVudGl0eV9rZXkYAyABKAxIAVIRcHVi'
'bGljSWRlbnRpdHlLZXmIAQESKAoNc2lnbmVkX3ByZWtleRgEIAEoDEgCUgxzaWduZWRQcmVrZX'
'mIAQESOwoXc2lnbmVkX3ByZWtleV9zaWduYXR1cmUYBSABKAxIA1IVc2lnbmVkUHJla2V5U2ln'
'bmF0dXJliAEBEi0KEHNpZ25lZF9wcmVrZXlfaWQYBiABKANIBFIOc2lnbmVkUHJla2V5SWSIAQ'
'ESLAoPcmVnaXN0cmF0aW9uX2lkGAggASgDSAVSDnJlZ2lzdHJhdGlvbklkiAEBQgsKCV91c2Vy'
'bmFtZUIWChRfcHVibGljX2lkZW50aXR5X2tleUIQCg5fc2lnbmVkX3ByZWtleUIaChhfc2lnbm'
'VkX3ByZWtleV9zaWduYXR1cmVCEwoRX3NpZ25lZF9wcmVrZXlfaWRCEgoQX3JlZ2lzdHJhdGlv'
'bl9pZBpZCgtVcGxvYWRUb2tlbhIhCgx1cGxvYWRfdG9rZW4YASABKAxSC3VwbG9hZFRva2VuEi'
'cKD2Rvd25sb2FkX3Rva2VucxgCIAMoDFIOZG93bmxvYWRUb2tlbnMaOQoORG93bmxvYWRUb2tl'
'bnMSJwoPZG93bmxvYWRfdG9rZW5zGAEgAygMUg5kb3dubG9hZFRva2VucxpFCgtQcm9vZk9mV2'
'9yaxIWCgZwcmVmaXgYASABKAlSBnByZWZpeBIeCgpkaWZmaWN1bHR5GAIgASgDUgpkaWZmaWN1'
'bHR5GtcHCgJPaxIUCgROb25lGAEgASgISABSBE5vbmUSGAoGdXNlcmlkGAIgASgDSABSBnVzZX'
'JpZBImCg1hdXRoY2hhbGxlbmdlGAMgASgMSABSDWF1dGhjaGFsbGVuZ2USSgoLdXBsb2FkdG9r'
'ZW4YBCABKAsyJi5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlVwbG9hZFRva2VuSABSC3VwbG'
'9hZHRva2VuEkEKCHVzZXJkYXRhGAUgASgLMiMuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5V'
'c2VyRGF0YUgAUgh1c2VyZGF0YRIeCglhdXRodG9rZW4YBiABKAxIAFIJYXV0aHRva2VuEkoKDG'
'RlcHJlY2F0ZWRfNxgHIAEoCzIlLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuRGVwcmVjYXRl'
'ZEgAUgtkZXByZWNhdGVkNxJQCg1hdXRoZW50aWNhdGVkGAggASgLMiguc2VydmVyX3RvX2NsaW'
'VudC5SZXNwb25zZS5BdXRoZW50aWNhdGVkSABSDWF1dGhlbnRpY2F0ZWQSOAoFcGxhbnMYCSAB'
'KAsyIC5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlBsYW5zSABSBXBsYW5zEk0KDHBsYW5iYW'
'xsYW5jZRgKIAEoCzInLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuUGxhbkJhbGxhbmNlSABS'
'DHBsYW5iYWxsYW5jZRJMCg1kZXByZWNhdGVkXzExGAsgASgLMiUuc2VydmVyX3RvX2NsaWVudC'
'5SZXNwb25zZS5EZXByZWNhdGVkSABSDGRlcHJlY2F0ZWQxMRJfChJhZGRhY2NvdW50c2ludml0'
'ZXMYDCABKAsyLS5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLkFkZEFjY291bnRzSW52aXRlc0'
'gAUhJhZGRhY2NvdW50c2ludml0ZXMSUwoOZG93bmxvYWR0b2tlbnMYDSABKAsyKS5zZXJ2ZXJf'
'dG9fY2xpZW50LlJlc3BvbnNlLkRvd25sb2FkVG9rZW5zSABSDmRvd25sb2FkdG9rZW5zEk0KDH'
'NpZ25lZHByZWtleRgOIAEoCzInLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuU2lnbmVkUHJl'
'S2V5SABSDHNpZ25lZHByZWtleRJKCgtwcm9vZk9mV29yaxgPIAEoCzImLnNlcnZlcl90b19jbG'
'llbnQuUmVzcG9uc2UuUHJvb2ZPZldvcmtIAFILcHJvb2ZPZldvcmtCBAoCT2tCCgoIUmVzcG9u'
'c2U=');

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/settings/account.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_setup.view.dart';
import 'package:twonly/src/visual/views/settings/chat/chat_reactions.view.dart';
@ -165,10 +164,6 @@ final routerProvider = GoRouter(
path: 'backup',
builder: (context, state) => const BackupView(),
routes: [
GoRoute(
path: 'server',
builder: (context, state) => const BackupServerView(),
),
GoRoute(
path: 'recovery',
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:mutex/mutex.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/locator.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/utils.api.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/pushkeys.notifications.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 apiSecure = kReleaseMode ? 's' : 's';
String get apiEndpoint => 'http$apiSecure://$apiHost/api/';
final _planUpdateController = StreamController<SubscriptionPlan>.broadcast();
Stream<SubscriptionPlan> get onPlanUpdated => _planUpdateController.stream;
@ -92,6 +95,7 @@ class ApiService {
try {
final channel = IOWebSocketChannel.connect(
Uri.parse(apiUrl),
pingInterval: const Duration(seconds: 30),
);
_channel = channel;
_channel!.stream.listen(_onData, onDone: _onDone, onError: _onError);
@ -122,7 +126,7 @@ class ApiService {
twonlyDB.markUpdated();
unawaited(syncFlameCounters());
unawaited(setupNotificationWithUsers());
unawaited(signalHandleNewServerConnection());
unawaited(SignalIdentityService.onAuthenticated());
resetResyncedUsers();
resetUserDiscoveryRequestUpdates();
unawaited(fetchGroupStatesForUnjoinedGroups());
@ -244,11 +248,11 @@ class ApiService {
try {
final msg = server.ServerToClient.fromBuffer(msgBuffer as Uint8List);
if (msg.v0.hasResponse()) {
await removeFromRetransmissionBuffer(msg.v0.seq);
final completer = _pendingRequests.remove(msg.v0.seq);
if (completer != null && !completer.isCompleted) {
completer.complete(msg);
}
unawaited(removeFromRetransmissionBuffer(msg.v0.seq));
} else {
unawaited(handleServerMessage(msg));
}
@ -414,6 +418,7 @@ class ApiService {
),
);
}
await twonlyDB.receiptsDao.deleteReceiptForUser(contactId);
}
}
return res;
@ -450,6 +455,21 @@ class ApiService {
await onAuthenticated();
} else {
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;
}
@ -466,16 +486,62 @@ class ApiService {
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 {
return lockAuthentication.protect(() async {
if (isAuthenticated) return;
if (await getSignalIdentity() == null) {
Log.error('Signal identity not found.');
if (!userService.isUserCreated) {
return;
}
if (!userService.isUserCreated) return;
if (userService.currentUser.canUseLoginTokenForAuth) {
await tryAuthenticateWithLoginToken();
return;
}
if (await tryAuthenticateWithToken()) {
return;
}
@ -542,6 +608,8 @@ class ApiService {
final signedPreKey = (await signalStore.loadSignedPreKeys())[0];
final loginToken = await RustKeyManager.getLoginToken();
final register = Handshake_Register()
..username = username
..publicIdentityKey = (await signalStore.getIdentityKeyPair())
@ -552,6 +620,7 @@ class ApiService {
..signedPrekeySignature = signedPreKey.signature
..signedPrekeyId = Int64(signedPreKey.id)
..langCode = ui.PlatformDispatcher.instance.locale.languageCode
..loginToken = loginToken
..proofOfWork = Int64(proofOfWorkResult)
..isIos = Platform.isIOS;
@ -617,13 +686,28 @@ class ApiService {
return sendRequestSync(req, ensureRetransmission: true);
}
Future<Result> getCurrentLocation() async {
final get = ApplicationData_GetLocation();
final appData = ApplicationData()..getLocation = get;
Future<Result> _setLoginToken(List<int> token) async {
final get = ApplicationData_SetLoginToken()..loginToken = token;
final appData = ApplicationData()..setLoginToken = get;
final req = createClientToServerFromApplicationData(appData);
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 {
final get = ApplicationData_GetUserByUsername()..username = username;
final appData = ApplicationData()..getUserByUsername = get;
@ -652,27 +736,6 @@ class ApiService {
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 {
final get = ApplicationData_RemoveAdditionalUser()..userId = userId;
final appData = ApplicationData()..removeAdditionalUser = get;
@ -687,34 +750,6 @@ class ApiService {
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 {
final get = ApplicationData_ReportUser()
..reportedUserId = Int64(userId)
@ -731,13 +766,6 @@ class ApiService {
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 {
final get = ApplicationData_UpdateGoogleFcmToken()..googleFcm = googleFcm;
final appData = ApplicationData()..updateGoogleFcmToken = get;

View file

@ -60,6 +60,15 @@ Future<bool> handleNewContactRequest(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(
fromUserId,
const ContactsCompanion(
@ -68,10 +77,6 @@ Future<void> handleContactAccept(int fromUserId) async {
deletedByUser: Value(false),
),
);
final contact = await twonlyDB.contactsDao
.getContactByUserId(fromUserId)
.getSingleOrNull();
if (contact != null) {
await twonlyDB.groupsDao.createNewDirectChat(
fromUserId,
GroupsCompanion(
@ -79,7 +84,6 @@ Future<void> handleContactAccept(int fromUserId) async {
),
);
}
}
Future<bool> handleContactRequest(
int fromUserId,
@ -143,8 +147,8 @@ Future<void> handleContactUpdate(
groupId: Value(group.groupId),
type: const Value(GroupActionType.updatedContactUsername),
contactId: Value(fromUserId),
oldGroupName: Value('@${contact.username}'),
newGroupName: Value('@${contactUpdate.username}'),
oldGroupName: Value(contact.username),
newGroupName: Value(contactUpdate.username),
),
);
}
@ -157,7 +161,7 @@ Future<void> handleContactUpdate(
groupId: Value(group.groupId),
type: const Value(GroupActionType.updatedContactDisplayName),
contactId: Value(fromUserId),
oldGroupName: Value(contact.displayName ?? ''),
oldGroupName: Value(contact.displayName ?? contact.username),
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/services/api/messages.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';
Future<void> handleGroupCreate(

View file

@ -267,13 +267,13 @@ Future<void> requestMediaReupload(String mediaId) async {
final messages = await twonlyDB.messagesDao.getMessagesByMediaId(mediaId);
for (final message in messages) {
if (message.openedAt != null) continue;
if (message.openedAt != null || message.senderId == null) continue;
await sendCipherText(
messages.first.senderId!,
message.senderId!,
EncryptedContent(
mediaUpdate: EncryptedContent_MediaUpdate(
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');
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/services/api/mediafiles/download.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/utils/log.dart';
@ -22,8 +22,11 @@ Future<void> initFileDownloader() async {
if (update.task.taskId.contains('download_')) {
await handleDownloadStatusUpdate(update);
}
if (update.task.taskId.contains('backup')) {
await handleBackupStatusUpdate(update);
if (update.task.taskId.contains('backup_')) {
await BackupService.handleBackupStatusUpdate(
update.task.taskId,
update,
);
}
case TaskProgressUpdate():
Log.info(

View file

@ -1,6 +1,4 @@
import 'dart:async';
import 'dart:convert';
import 'package:background_downloader/background_downloader.dart';
import 'package:clock/clock.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:twonly/globals.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/messages.table.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/services/api/mediafiles/media_background.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/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/exclusive_access.utils.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/secure_storage.dart';
import 'package:workmanager/workmanager.dart' hide TaskStatus;
final lockRetransmission = Mutex();
@ -620,22 +617,17 @@ Future<void> _uploadUploadRequest(MediaFileService media) async {
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 =
'http${apiService.apiSecure}://${apiService.apiHost}/api/upload';
// try {
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(
taskId: 'upload_${media.mediaFile.mediaId}',
displayName: media.mediaFile.type.name,
@ -643,9 +635,7 @@ Future<void> _uploadUploadRequest(MediaFileService media) async {
url: apiUrl,
priority: 0,
retries: 10,
headers: {
'x-twonly-auth-token': apiAuthToken,
},
headers: headers,
);
final connectivityResult = await Connectivity().checkConnectivity();

View file

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

View file

@ -4,7 +4,6 @@ import 'dart:io';
import 'package:clock/clock.dart';
import 'package:drift/drift.dart';
import 'package:hashlib/random.dart';
import 'package:mutex/mutex.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/locator.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/user_discovery.c2c.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/notifications/background.notifications.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/misc.dart';
final lockHandleServerMessage = Mutex();
Future<void> handleServerMessage(server.ServerToClient msg) async {
return lockHandleServerMessage.protect(() async {
Log.info('Processing a 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);
AppState.gotMessageFromServer = true;
Log.info('Message from server proccessed.');
});
}
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:fixnum/fixnum.dart';
import 'package:twonly/core/bridge/wrapper/key_manager.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/twonly.db.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/notifications/pushkeys.notifications.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> {
Result.error(this.error) : value = null;
@ -106,3 +113,36 @@ Future<bool> importSignalContactAndCreateRequest(
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() {
Workmanager().executeTask((task, inputData) async {
SentryWidgetsFlutterBinding.ensureInitialized();
await AppEnvironment.init();
switch (task) {
case 'eu.twonly.periodic_task':
// if (await initBackgroundExecution()) {
// await handlePeriodicTask();
// }
break;
case 'eu.twonly.processing_task':
case _ when task.startsWith('progressing_finish_uploads_'):
if (await initBackgroundExecution()) {
await handleProcessingTask();
}
@ -58,7 +61,6 @@ Future<bool> initBackgroundExecution() async {
return false;
}
await AppEnvironment.init();
AppState.isInBackgroundTask = true;
if (await StartupGuard.isAppStarting()) {
@ -130,6 +132,7 @@ Future<void> handlePeriodicTask({int lastExecutionInSecondsLimit = 120}) async {
return;
}
try {
while (!AppState.gotMessageFromServer) {
if (stopwatch.elapsed.inSeconds >= 15) {
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 Future.delayed(const Duration(milliseconds: 2000));
} finally {
await apiService.close(() {});
stopwatch.stop();
}
Log.info('eu.twonly.periodic_task finished after ${stopwatch.elapsed}.');
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);
if (group == null) return;
if (group.isDirectChat) {
final contacts = await twonlyDB.groupsDao.getGroupContact(
group.groupId,
);
@ -113,7 +112,6 @@ Future<void> incFlameCounter(
),
);
}
}
final totalMediaCounter = group.totalMediaCounter + 1;
var flameCounter = group.flameCounter;

View file

@ -31,7 +31,7 @@ Future<void> createThumbnailsForVideo(
'It took ${stopwatch.elapsedMilliseconds}ms to create the thumbnail.',
);
} else {
Log.error(
Log.warn(
'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_plus/cryptography_plus.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/secure_storage.keys.dart';
import 'package:twonly/src/localization/generated/app_localizations.dart';
@ -266,8 +266,7 @@ Future<void> showLocalPushNotificationWithoutUserId(
}
Future<String?> getAvatarIcon(int contactId) async {
final directory = await getApplicationCacheDirectory();
final avatarsDirectory = Directory('${directory.path}/avatars');
final avatarsDirectory = Directory('${AppEnvironment.cacheDir}/avatars');
final filePath = '${avatarsDirectory.path}/$contactId.png';
final file = File(filePath);
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_core/firebase_core.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:twonly/globals.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/secure_storage.keys.dart';
import 'package:twonly/src/services/background/callback_dispatcher.background.dart';
import 'package:twonly/src/services/notifications/background.notifications.dart';
import 'package:twonly/src/services/notifications/setup.notifications.dart';
import 'package:twonly/src/services/user.service.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
Future<void> checkForTokenUpdates() async {
const storage = FlutterSecureStorage();
final storedToken = await storage.read(key: SecureStorageKeys.googleFcm);
try {
if (!userService.isUserCreated) return;
if (Platform.isIOS) {
var apnsToken = await FirebaseMessaging.instance.getAPNSToken();
for (var i = 0; i < 20; i++) {
@ -47,23 +43,22 @@ Future<void> checkForTokenUpdates() async {
Log.info('Loaded FCM token.');
if (storedToken == null || fcmToken != storedToken) {
Log.info('Got new FCM TOKEN.');
await storage.write(key: SecureStorageKeys.googleFcm, value: fcmToken);
if (userService.currentUser.fcmToken == null ||
fcmToken != userService.currentUser.fcmToken) {
Log.info('Got new FCM token.');
await UserService.update((u) {
u.updateFCMToken = true;
u
..updateFCMToken = true
..fcmToken = fcmToken;
});
}
FirebaseMessaging.instance.onTokenRefresh
.listen((fcmToken) async {
Log.info('Got new FCM TOKEN.');
await storage.write(
key: SecureStorageKeys.googleFcm,
value: fcmToken,
);
await UserService.update((u) {
u.updateFCMToken = true;
u
..updateFCMToken = true
..fcmToken = fcmToken;
});
})
.onError((err) {
@ -75,11 +70,16 @@ Future<void> checkForTokenUpdates() async {
}
Future<void> initFCMAfterAuthenticated({bool force = false}) async {
final fcmToken = userService.currentUser.fcmToken;
if (userService.currentUser.updateFCMToken || force) {
const storage = FlutterSecureStorage();
final storedToken = await storage.read(key: SecureStorageKeys.googleFcm);
if (storedToken != null) {
final res = await apiService.updateFCMToken(storedToken);
if (fcmToken == null) {
Log.error('FCM token could not be updated as it is empty');
await checkForTokenUpdates();
return;
}
final res = await apiService.updateFCMToken(
fcmToken,
);
if (res.isSuccess) {
Log.info('Uploaded new FCM token!');
await UserService.update((u) {
@ -88,9 +88,6 @@ Future<void> initFCMAfterAuthenticated({bool force = false}) async {
} else {
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.');
await FirebaseMessaging.instance.deleteToken();
Log.info('Old FCM deleted.');
await const FlutterSecureStorage().delete(key: SecureStorageKeys.googleFcm);
await UserService.update((u) => u.fcmToken = null);
await checkForTokenUpdates();
await initFCMAfterAuthenticated(force: true);
}
@ -119,7 +116,9 @@ Future<void> initFCMService() async {
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
SentryWidgetsFlutterBinding.ensureInitialized();
await AppEnvironment.init();
final isInitialized = await initBackgroundExecution();
await setupPushNotification();
Log.info('Handling a background message: ${message.messageId}');
await handleRemoteMessage(message);

View file

@ -12,16 +12,12 @@ import 'package:twonly/src/utils/log.dart';
Future<CiphertextMessage?> signalEncryptMessage(
int target,
Uint8List plaintextContent, {
bool useLock = true,
}) async {
if (useLock) {
Uint8List plaintextContent,
) async {
return lockingSignalProtocol.protect<CiphertextMessage?>(() async {
return _signalEncryptMessage(target, plaintextContent);
});
}
return _signalEncryptMessage(target, plaintextContent);
}
Future<CiphertextMessage?> _signalEncryptMessage(
int target,
@ -44,7 +40,9 @@ signalDecryptMessage(
Uint8List encryptedContentRaw,
int type,
) 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 {
final session = SessionCipher.fromStore(
(await getSignalStore())!,
@ -64,28 +62,51 @@ signalDecryptMessage(
);
default:
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) {
Log.warn(e);
return (
null,
PlaintextContent_DecryptionErrorMessage_Type.PREKEY_UNKNOWN,
false,
);
} on DuplicateMessageException catch (e) {
Log.info(e.toString());
return (null, null);
return (null, null, false);
} on InvalidMessageException catch (e) {
Log.warn(e);
if (!resyncedUsers.contains(fromUserId)) {
if (await handleSessionResync(fromUserId, useLock: false)) {
// This flag prevents from resyncing the session the client received multiple new
// messages from the server he could not decrypt
return (
null,
PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN,
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);
// This message contains a new PreKeyBundle establishing a new signal session
// This message contains a new PreKeyBundle establishing a new signal
// session
await sendCipherText(
fromUserId,
EncryptedContent(
@ -93,14 +114,9 @@ signalDecryptMessage(
type: EncryptedContent_ErrorMessages_Type.SESSION_OUT_OF_SYNC,
),
),
useLock: false,
);
}
}
return (null, PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN);
} catch (e) {
Log.error(e);
return (null, PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN);
}
});
return (decryptedContent, errorType);
}

View file

@ -1,30 +1,21 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:clock/clock.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/core/bridge/wrapper/key_manager.dart';
import 'package:twonly/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/services/signal/consts.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/user.service.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/secure_storage.dart';
Future<IdentityKeyPair?> getSignalIdentityKeyPair() async {
final signalIdentity = await getSignalIdentity();
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 {
class SignalIdentityService {
static Future<void> onAuthenticated() async {
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 =
(userService.currentUser.signalLastSignedPreKeyUpdated!).isAfter(
fortyEightHoursAgo,
@ -56,6 +47,7 @@ Future<void> signalHandleNewServerConnection() async {
Log.info('updated signed pre key');
}
}
}
Future<List<PreKeyRecord>> signalGetPreKeys() async {
return lockingSignalProtocol.protect(() async {
@ -75,64 +67,45 @@ Future<List<PreKeyRecord>> signalGetPreKeys() async {
Future<SignalIdentity?> getSignalIdentity() async {
try {
var signalIdentityJson = await SecureStorage.instance.read(
key: SecureStorageKeys.signalIdentity,
final identity = await RustKeyManager.getSignalIdentity();
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) {
Log.error('could not load signal identity: $e');
return null;
}
}
Future<IdentityKeyPair?> getSignalIdentityKeyPair() async {
final signalIdentity = await getSignalIdentity();
if (signalIdentity == null) return null;
return IdentityKeyPair.fromSerialized(signalIdentity.identityKeyPairU8List);
}
Future<Uint8List> getUserPublicKey() async {
Log.info('getUserPublicKey: getting identity');
final signalIdentity = (await getSignalIdentity())!;
Log.info('getUserPublicKey: getting signal store');
final signalStore = await getSignalStoreFromIdentity(signalIdentity);
Log.info('getUserPublicKey: getting key pair');
final keyPair = await signalStore.getIdentityKeyPair();
Log.info('getUserPublicKey: serializing public key');
return keyPair.getPublicKey().serialize();
}
Future<void> createIfNotExistsSignalIdentity() async {
final signalIdentity = await SecureStorage.instance.read(
key: SecureStorageKeys.signalIdentity,
);
if (signalIdentity != null) {
return;
}
// check if identity already exists
if (await getSignalIdentity() != null) return;
final identityKeyPair = generateIdentityKeyPair();
final registrationId = generateRegistrationId(true);
final signalStore = SignalSignalProtocolStore(
identityKeyPair,
registrationId,
);
final signedPreKey = generateSignedPreKey(identityKeyPair, defaultDeviceId);
final signedPreKeyStore = <int, Uint8List>{};
signedPreKeyStore[signedPreKey.id] = signedPreKey.serialize();
await signalStore.signedPreKeyStore.storeSignedPreKey(
signedPreKey.id,
signedPreKey,
);
final storedSignalIdentity = SignalIdentity(
identityKeyPairU8List: identityKeyPair.serialize(),
await RustKeyManager.importSignalIdentity(
identityKeyPairStructure: identityKeyPair.serialize(),
registrationId: registrationId,
);
await SecureStorage.instance.write(
key: SecureStorageKeys.signalIdentity,
value: jsonEncode(storedSignalIdentity),
signedPreKeyStore: signedPreKeyStore,
);
}

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/utils/log.dart';
Future<bool> processSignalUserData(
Response_UserData userData, {
bool useLock = true,
}) async {
if (useLock) {
Future<bool> processSignalUserData(Response_UserData userData) async {
return lockingSignalProtocol.protect(() async {
return _processSignalUserData(userData);
});
}
return _processSignalUserData(userData);
}
Future<bool> _processSignalUserData(Response_UserData userData) async {
final SignalProtocolStore? signalStore = await getSignalStore();
@ -106,14 +100,11 @@ Future<Uint8List?> getPublicKeyFromContact(int contactId) async {
}
}
Future<bool> handleSessionResync(
int fromUserId, {
bool useLock = true,
}) async {
Future<bool> handleSessionResync(int fromUserId) async {
final userData = await apiService.getUserById(fromUserId);
if (userData != null) {
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.');
return false;

View file

@ -2,9 +2,11 @@ import 'dart:async';
import 'dart:convert';
import 'package:mutex/mutex.dart';
import 'package:twonly/core/bridge/wrapper/key_manager.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/secure_storage.keys.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/secure_storage.dart';
@ -26,21 +28,49 @@ class UserService {
static Future<UserData?> getUser() async {
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(
key: SecureStorageKeys.userData,
);
if (userDataJson == null) {
return null;
}
return UserData.fromJson(
if (userDataJson != null) {
final userData = UserData.fromJson(
jsonDecode(userDataJson) as Map<String, dynamic>,
);
// 3. Run migration
await _migrateFromSecureStorage(userData);
return userData;
}
return null;
} catch (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(
void Function(UserData userData) updateUser,
) async {
@ -53,10 +83,7 @@ class UserService {
user.defaultShowTime = null;
}
updateUser(user);
await SecureStorage.instance.write(
key: SecureStorageKeys.userData,
value: jsonEncode(user),
);
await KeyValueStore.put('user', user.toJson());
userService.currentUser = user;
} catch (e) {
Log.error('Could not update the user: $e');
@ -66,6 +93,16 @@ class UserService {
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() {
_userDataUpdateController.add(null);
}

View file

@ -1,8 +1,11 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:drift/drift.dart';
import 'package:flutter/foundation.dart';
import 'package:twonly/core/bridge/wrapper/user_discovery.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/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)
..addColumns([twonlyDB.contacts.userId])
..where(twonlyDB.contacts.accountDeleted.equals(true));
@ -216,4 +219,35 @@ class UserDiscoveryService {
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';
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 {
return File('${AppEnvironment.supportDir}/keyvalue/$key.json');
@ -16,7 +20,7 @@ class KeyValueStore {
static Future<T> _exclusive<T>(String key, Future<T> Function() action) {
return exclusiveAccess(
lockName: 'keyvalue-$key',
mutex: _mutex,
mutex: _getMutex(key),
action: action,
);
}
@ -32,8 +36,8 @@ class KeyValueStore {
}
});
static Future<Map<String, dynamic>?> get(String key) =>
_exclusive(key, () async {
static Future<Map<String, dynamic>?> get(String key) async {
return _exclusive(key, () async {
final file = await _getFilePath(key);
try {
if (file.existsSync()) {
@ -48,9 +52,10 @@ class KeyValueStore {
return null;
}
});
}
static Future<void> put(String key, Map<String, dynamic> value) =>
_exclusive(key, () async {
static Future<void> put(String key, Map<String, dynamic> value) async {
return _exclusive(key, () async {
try {
final file = await _getFilePath(key);
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) {
if (msg.contains('SqliteException')) {
// 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;
}

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';
const units = <String>['Bytes', 'KB', 'MB', 'GB', 'TB'];
final unitIndex = (log(bytes) / log(1000)).floor();
final formattedSize = bytes / pow(1000, unitIndex);
return '${formattedSize.toStringAsFixed(decimalPlaces)} ${units[unitIndex]}';
return '${formattedSize.ceil()} ${units[unitIndex]}';
}
bool isUUIDNewer(String uuid1, String uuid2) {

View file

@ -9,6 +9,9 @@ RegExp emojiRegex() => RegExp(
);
bool isOneEmoji(String character) {
if (EmojiAnimationComp.animatedIcons.containsKey(character)) {
return true;
}
final matches = emojiRegex().allMatches(character);
if (matches.length == 1) {
final match = matches.first;
@ -82,6 +85,7 @@ class EmojiAnimationComp extends StatelessWidget {
'😴': 'sleep.lottie',
'🤒': 'thermometer-face.lottie',
'🤕': 'bandage-face.lottie',
'🫪': 'distorted_face.json',
'🤥': 'liar.lottie',
'😇': 'halo.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/model/protobuf/client/generated/messages.pb.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/visual/elements/better_list_title.element.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/qr.utils.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:url_launcher/url_launcher_string.dart';
@ -46,16 +47,19 @@ class CameraScannedOverlay extends StatelessWidget {
onTap: () async {
c.isLoading = true;
mainController.setState();
showSnackbar(
context,
context.lang.requestedUserToastText(c.profile.username),
level: SnackbarLevel.success,
);
if (await addNewContactFromPublicProfile(c.profile) &&
context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
context.lang.requestedUserToastText(c.profile.username),
),
duration: const Duration(seconds: 8),
),
);
// showSnackbar(
// context,
// context.lang.requestedUserToastText(c.profile.username),
// level: SnackbarLevel.success,
// );
}
},
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/utils/log.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/screenshot.helper.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();
return imageBytes;
} catch (e) {
if (context.mounted) {
// ignore: use_build_context_synchronously
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error loading picture: $e'),
duration: const Duration(seconds: 3),
),
if (mounted) {
showSnackbar(
context,
'Error loading picture: $e',
);
Log.error(e);
}
return null;
}
@ -284,6 +283,8 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
await Future.delayed(const Duration(milliseconds: 1000));
}
if (!mounted) return;
await mc.cameraController?.pausePreview();
if (!mounted) {
return;
@ -342,6 +343,9 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
await _deInitVolumeControl();
if (!mounted) return true;
// Cache active camera ID since ShareImageEditorView closes the camera and resets state parameters.
final initialCameraId = mc.selectedCameraDetails.cameraId;
final shouldReturn =
await Navigator.push(
context,
@ -382,7 +386,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
return true;
}
await mc.selectCamera(
mc.selectedCameraDetails.cameraId,
initialCameraId,
false,
);
return false;
@ -606,17 +610,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
void _showCameraException(dynamic e) {
Log.error('$e');
try {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error: $e'),
duration: const Duration(seconds: 3),
),
);
}
// ignore: empty_catches
} catch (e) {}
if (mounted) showSnackbar(context, 'Error: $e');
}
@override

View file

@ -3,6 +3,7 @@ import 'dart:io';
import 'package:camera/camera.dart';
import 'package:clock/clock.dart';
import 'package:drift/drift.dart' show Value;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.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/misc.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/views/camera/camera_preview_components/camera_preview_controller_view.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 get currentFilterType => _currentFilterType;
Future<void>? _pendingDisposal;
Future<void> closeCamera() async {
contactsVerified = {};
scannedNewProfiles = {};
@ -94,14 +98,18 @@ class MainCameraController {
final cameraControllerTemp = cameraController;
cameraController = null;
// 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();
});
},
);
initCameraStarted = false;
selectedCameraDetails = SelectedCameraDetails();
}
Future<void> selectCamera(int sCameraId, bool init) async {
await _pendingDisposal;
initCameraStarted = true;
if (AppEnvironment.cameras.isEmpty) {
@ -136,7 +144,13 @@ class MainCameraController {
? ImageFormatGroup.nv21
: ImageFormatGroup.bgra8888,
);
try {
await cameraController?.initialize();
} catch (e) {
Log.error(e);
cameraController = null; // ensure uninitialized controller is not reused
return;
}
await cameraController?.startImageStream(_processCameraImage);
await cameraController?.setZoomLevel(selectedCameraDetails.scaleFactor);
if (userService.currentUser.videoStabilizationEnabled && !kDebugMode) {
@ -216,7 +230,7 @@ class MainCameraController {
(e.code == 'setFocusPointFailed' || e.code == 'setFocusModeFailed')) {
Log.info('Focus point or mode not supported on this device');
} else {
Log.error(e);
Log.warn(e);
}
}
@ -356,7 +370,14 @@ class MainCameraController {
if (res == null) continue;
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) {
await HapticFeedback.heavyImpact();
scannedNewProfiles[profile.userId.toInt()] = ScannedNewProfile(
@ -373,18 +394,14 @@ class MainCameraController {
);
await HapticFeedback.heavyImpact();
if (verificationOk) {
AppGlobalKeys.scaffoldMessengerKey.currentState?.showSnackBar(
SnackBar(
content: Text(
AppGlobalKeys.scaffoldMessengerKey.currentContext?.lang
.verifiedPublicKey(
final context = cameraPreviewKey.currentContext;
if (verificationOk && context != null && context.mounted) {
showSnackbar(
context,
context.lang.verifiedPublicKey(
getContactDisplayName(contact),
) ??
'',
),
duration: const Duration(seconds: 6),
),
level: SnackbarLevel.success,
);
}
}

View file

@ -77,10 +77,12 @@ class SaveToGalleryButtonState extends State<SaveToGalleryButton> {
await newService.storeMediaFile();
}
if (mounted) {
setState(() {
_imageSaved = true;
_imageSaving = false;
});
}
},
child: Row(
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/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/shortcut_row.comp.dart';
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/background.layer.dart';
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),
BestFriendsSelector(
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/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/location_filter.dart';
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/filters/stickers.dart';
/// Main layer
class FilterLayer extends StatefulWidget {
@ -75,7 +75,6 @@ class _FilterLayerState extends State<FilterLayer> {
List<Widget> pages = [
const FilterSkeleton(),
const DateTimeFilter(),
// const LocationFilter(),
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/group_list_item.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 {
const ChatListView({super.key});
@ -215,6 +216,7 @@ class _ChatListViewState extends State<ChatListView> {
child: Column(
children: [
const FinishSetupComp(),
const MissingBackupComp(),
if (_groupsNotPinned.isEmpty &&
_groupsPinned.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(
padding: const EdgeInsets.all(8),
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/services.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:twonly/globals.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
@ -131,8 +131,7 @@ class _MessageInputState extends State<MessageInput> {
_currentDuration = 0;
});
await HapticFeedback.heavyImpact();
final audioTmpPath =
'${(await getApplicationCacheDirectory()).path}/recording.m4a';
final audioTmpPath = '${AppEnvironment.cacheDir}/recording.m4a';
unawaited(
recorderController.record(
path: audioTmpPath,

View file

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

View file

@ -10,8 +10,10 @@ import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart
as server;
import 'package:twonly/src/model/protobuf/client/generated/qr.pb.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/qr.utils.dart';
import 'package:twonly/src/visual/components/snackbar.dart';
class AddContactViaQrLinkView extends StatefulWidget {
const AddContactViaQrLinkView({
@ -69,11 +71,8 @@ class _AddContactViaQrLinkViewState extends State<AddContactViaQrLinkView> {
context.pop();
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: $e')),
);
}
if (mounted) showSnackbar(context, 'Error: $e');
Log.error(e);
} finally {
if (mounted) {
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/flame_counter.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/elements/better_list_title.element.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 (!delete) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.lang.deleteUserErrorMessage),
duration: const Duration(seconds: 8),
),
);
showSnackbar(context, context.lang.deleteUserErrorMessage);
return;
}
@ -157,11 +153,10 @@ class _ContactViewState extends State<ContactView> {
final res = await apiService.reportUser(contact.userId, reason);
if (!mounted) return;
if (res.isSuccess) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.lang.userGotReported),
duration: const Duration(seconds: 3),
),
showSnackbar(
context,
context.lang.userGotReported,
level: SnackbarLevel.info,
);
} else {
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/tables/groups.table.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/visual/components/alert.dialog.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/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/elements/better_list_title.element.dart';
import 'package:twonly/src/visual/views/contact/contact.view.dart';
@ -343,10 +344,8 @@ Future<String?> showGroupNameChangeDialog(
}
void showNetworkIssue(BuildContext context) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.lang.groupNetworkIssue),
duration: const Duration(seconds: 3),
),
showSnackbar(
context,
context.lang.groupNetworkIssue,
);
}

View file

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/src/database/daos/contacts.dao.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/visual/components/avatar_icon.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/visual/components/avatar_icon.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/decorations/input_text.decoration.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 (!selectedUsers.contains(userId)) {
if (selectedUsers.length + alreadyInGroup.length > 256) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.lang.groupSizeLimitError(256)),
duration: const Duration(seconds: 3),
),
);
showSnackbar(context, context.lang.groupSizeLimitError(256));
return;
}
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/model/protobuf/client/generated/messages.pb.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/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/views/groups/group.view.dart';
@ -107,11 +108,10 @@ class GroupMemberContextMenu extends StatelessWidget {
),
);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.lang.contactRequestSend),
duration: const Duration(seconds: 3),
),
showSnackbar(
context,
context.lang.contactRequestSend,
level: SnackbarLevel.success,
);
}
}

View file

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

View file

@ -1,13 +1,11 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:restart_app/restart_app.dart';
import 'package:twonly/src/model/json/userdata.model.dart';
import 'package:twonly/src/services/backup/restore.backup.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/services/backup.service.dart';
import 'package:twonly/src/utils/misc.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/views/settings/backup/backup_server.view.dart';
class BackupRecoveryView extends StatefulWidget {
const BackupRecoveryView({super.key});
@ -19,7 +17,6 @@ class BackupRecoveryView extends StatefulWidget {
class _BackupRecoveryViewState extends State<BackupRecoveryView> {
bool obscureText = true;
bool isLoading = false;
BackupServer? backupServer;
final TextEditingController usernameCtrl = TextEditingController();
final TextEditingController passwordCtrl = TextEditingController();
@ -28,30 +25,37 @@ class _BackupRecoveryViewState extends State<BackupRecoveryView> {
isLoading = true;
});
try {
await recoverBackup(
final error = await BackupService.startFullBackupRecovery(
usernameCtrl.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(
notificationTitle: 'Backup successfully recovered.',
notificationBody: 'Click here to open the app again',
notificationTitle: context.lang.recoverSuccessTitle,
notificationBody: context.lang.recoverSuccessBody,
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(() {
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),
Center(
child: FilledButton.icon(

View file

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

View file

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

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