mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-05-25 12:12:13 +00:00
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:
commit
f7211fed08
169 changed files with 14658 additions and 3449 deletions
10
CHANGELOG.md
10
CHANGELOG.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
14
android/app/src/main/kotlin/io/crates/keyring/Keyring.kt
Normal file
14
android/app/src/main/kotlin/io/crates/keyring/Keyring.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
1
assets/animated_icons/distorted_face.json
Normal file
1
assets/animated_icons/distorted_face.json
Normal file
File diff suppressed because one or more lines are too long
23
lib/app.dart
23
lib/app.dart
|
|
@ -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,
|
||||
|
|
|
|||
29
lib/core/backup/backup_password.dart
Normal file
29
lib/core/backup/backup_password.dart
Normal 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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
87
lib/core/bridge/wrapper/backup.dart
Normal file
87
lib/core/bridge/wrapper/backup.dart
Normal 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;
|
||||
}
|
||||
77
lib/core/bridge/wrapper/key_manager.dart
Normal file
77
lib/core/bridge/wrapper/key_manager.dart
Normal 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
28
lib/core/context.dart
Normal 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
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
29
lib/core/keys/backup_password_keys.dart
Normal file
29
lib/core/keys/backup_password_keys.dart
Normal 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
20
lib/core/lib.dart
Normal 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));
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
135
lib/main.dart
135
lib/main.dart
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
79
lib/src/database/daos/shortcuts.dao.dart
Normal file
79
lib/src/database/daos/shortcuts.dao.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
25
lib/src/database/daos/shortcuts.dao.g.dart
Normal file
25
lib/src/database/daos/shortcuts.dao.g.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
2901
lib/src/database/schemas/twonly_db/drift_schema_v13.json
Normal file
2901
lib/src/database/schemas/twonly_db/drift_schema_v13.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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,
|
||||
);
|
||||
|
|
@ -21,34 +21,25 @@ class SignalSignedPreKeyStore extends SignedPreKeyStore {
|
|||
store[item[0] as int] = base64Decode(item[1] as String);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
26
lib/src/database/tables/shortcuts.table.dart
Normal file
26
lib/src/database/tables/shortcuts.table.dart
Normal 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};
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
51
lib/src/model/json/backup.model.dart
Normal file
51
lib/src/model/json/backup.model.dart
Normal 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);
|
||||
}
|
||||
65
lib/src/model/json/backup.model.g.dart
Normal file
65
lib/src/model/json/backup.model.g.dart
Normal 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',
|
||||
};
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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=');
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,17 +77,12 @@ 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(
|
||||
groupName: Value(getContactDisplayName(contact)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> handleContactRequest(
|
||||
|
|
@ -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),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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!));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
363
lib/src/services/backup.service.dart
Normal file
363
lib/src/services/backup.service.dart
Normal 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,
|
||||
}
|
||||
|
|
@ -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';
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -12,15 +12,11 @@ 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(
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -55,6 +46,7 @@ Future<void> signalHandleNewServerConnection() async {
|
|||
} else {
|
||||
Log.info('updated signed pre key');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<PreKeyRecord>> signalGetPreKeys() 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,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,16 +8,10 @@ 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 {
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
);
|
||||
}
|
||||
}());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -59,4 +64,5 @@ class KeyValueStore {
|
|||
Log.error('Error writing file: $e');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
258
lib/src/visual/components/snackbar.dart
Normal file
258
lib/src/visual/components/snackbar.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
292
lib/src/visual/views/camera/add_new_shortcut.view.dart
Normal file
292
lib/src/visual/views/camera/add_new_shortcut.view.dart
Normal 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);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,10 +77,12 @@ class SaveToGalleryButtonState extends State<SaveToGalleryButton> {
|
|||
await newService.storeMediaFile();
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_imageSaved = true;
|
||||
_imageSaving = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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(() {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in a new issue