mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-07-17 22:14:08 +00:00
commit
d31dad7179
153 changed files with 40804 additions and 2140 deletions
13
CHANGELOG.md
13
CHANGELOG.md
|
|
@ -1,5 +1,18 @@
|
|||
# Changelog
|
||||
|
||||
## 0.3.7
|
||||
|
||||
- Fix: Multiple UI issues
|
||||
|
||||
## 0.3.6
|
||||
|
||||
- Improve: Visibility of the verification badge
|
||||
|
||||
## 0.3.5
|
||||
|
||||
- Fix: Performance issue caused by an out-of-sync Signal session
|
||||
- Fix: Multiple smaller bug fixes
|
||||
|
||||
## 0.3.3
|
||||
|
||||
- Fix: Multiple UI issues
|
||||
|
|
|
|||
33
README.md
33
README.md
|
|
@ -35,7 +35,6 @@ If you decide to give twonly a try, please keep in mind that it is still in its
|
|||
- End-to-End encryption using the [Signal Protocol](https://de.wikipedia.org/wiki/Signal-Protokoll)
|
||||
- Open Source and can be downloaded directly from GitHub
|
||||
- No email or phone number required to register
|
||||
- Privacy friendly - Everything is stored on the device
|
||||
- The backend is hosted exclusively in Europe
|
||||
|
||||
## Roadmap
|
||||
|
|
@ -43,7 +42,6 @@ If you decide to give twonly a try, please keep in mind that it is still in its
|
|||
### Currently
|
||||
|
||||
- Focus on user-friendliness so that people enjoy using the app
|
||||
- User discovery without a phone number
|
||||
- Passwordless recovery without a phone number
|
||||
- Implementation of features so that Snapchat can actually be replaced
|
||||
- E2EE cloud backup of memories
|
||||
|
|
@ -63,36 +61,9 @@ If you discover a security issue in twonly, please adhere to the coordinated vul
|
|||
us your report to security@twonly.eu. We also offer for critical security issues a small bug bounties, but we can not
|
||||
guarantee a bounty currently :/
|
||||
|
||||
<!-- ## Contribution
|
||||
|
||||
If you have any questions or feature requests, feel free to start a new discussion. Issues are limited to bugs, and for maintainers only. -->
|
||||
|
||||
## Development
|
||||
|
||||
<details>
|
||||
<summary>Setup Instructions (macOS)</summary>
|
||||
|
||||
## Building
|
||||
|
||||
Some dependencies are downloaded directly from the source as there are some new changes which are not yet published on
|
||||
pub.dev or because they require some special installation.
|
||||
|
||||
```bash
|
||||
git submodule update --init --recursive
|
||||
|
||||
cd dependencies/flutter_zxing
|
||||
git submodule update --init --recursive
|
||||
./scripts/update_ios_macos_src.s
|
||||
```
|
||||
|
||||
## Debugging files
|
||||
|
||||
```bash
|
||||
run-as eu.twonly.testing ls /data/user/0/eu.twonly.testing/
|
||||
```
|
||||
|
||||
</details>
|
||||
## Contribution
|
||||
|
||||
Currently there are still some core features and rewrites open I want to do like the switch to the MLS protocol involving a huge rewrite in Rust. Because of this, contributions are currently not wanted. You can still view the code (if you find any security issues please contact me!). Also, issues are currently closed. If you find a bug, please use the in-app option as there you can upload your debug log which helps a lot.
|
||||
|
||||
## Signing Keys
|
||||
|
||||
|
|
|
|||
31
lib/app.dart
31
lib/app.dart
|
|
@ -5,9 +5,13 @@ import 'package:flutter_localizations/flutter_localizations.dart';
|
|||
import 'package:provider/provider.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/constants/keyvalue.keys.dart';
|
||||
import 'package:twonly/src/constants/routes.keys.dart';
|
||||
import 'package:twonly/src/localization/generated/app_localizations.dart';
|
||||
import 'package:twonly/src/model/json/onboarding_state.model.dart';
|
||||
import 'package:twonly/src/providers/routing.provider.dart';
|
||||
import 'package:twonly/src/providers/settings.provider.dart';
|
||||
import 'package:twonly/src/utils/keyvalue.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/pow.dart';
|
||||
import 'package:twonly/src/visual/components/app_outdated.comp.dart';
|
||||
|
|
@ -18,7 +22,7 @@ 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/recovery_from_secure_storage.view.dart';
|
||||
import 'package:twonly/src/visual/views/unlock_twonly.view.dart';
|
||||
|
||||
class App extends StatefulWidget {
|
||||
|
|
@ -151,6 +155,10 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
|||
|
||||
Future<void> initAsync() async {
|
||||
Log.info('AppWidgetState: initAsync started');
|
||||
final onboardingState = await KeyValueStore.getModel<OnboardingState>(
|
||||
KeyValueKeys.onboardingState,
|
||||
);
|
||||
_showOnboarding = !onboardingState.hasOnboardingFinished;
|
||||
if (userService.isUserCreated) {
|
||||
if (_initialPage != 0) {
|
||||
final count = await twonlyDB.contactsDao.getContactsCount();
|
||||
|
|
@ -180,8 +188,19 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
|||
} else {
|
||||
_proofOfWork = (null, disabled);
|
||||
}
|
||||
|
||||
if (onboardingState.hasStartedPasswordlessRecovery ||
|
||||
onboardingState.emailRecoveryRequested) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
routerProvider.push(Routes.recoverPasswordless);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// await PasswordlessRecoveryService.handleRecoveryLink(
|
||||
// 'https://me.twonly.eu/r/#7fdb8f08-0927-4e44-8761-038993414e48/1p7SKEzpxE3wSW9FQw60EUI4OpSW2U4EskdXLw8xg48',
|
||||
// );
|
||||
|
||||
setState(() {
|
||||
_isLoaded = true;
|
||||
});
|
||||
|
|
@ -223,9 +242,13 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
|||
}
|
||||
} else if (_showOnboarding) {
|
||||
child = OnboardingView(
|
||||
callbackOnSuccess: () => setState(() {
|
||||
_showOnboarding = false;
|
||||
}),
|
||||
callbackOnSuccess: () async {
|
||||
await KeyValueStore.update<OnboardingState>(
|
||||
key: KeyValueKeys.onboardingState,
|
||||
update: (state) => state.hasOnboardingFinished = true,
|
||||
);
|
||||
if (mounted) setState(() => _showOnboarding = false);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
child = RegisterView(
|
||||
|
|
|
|||
173
lib/core/bridge/callbacks/user_discovery.dart
Normal file
173
lib/core/bridge/callbacks/user_discovery.dart
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
// 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 '../../bridge.dart';
|
||||
import '../../frb_generated.dart';
|
||||
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
||||
|
||||
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`
|
||||
|
||||
class UserDiscoveryStoreFlutter {
|
||||
const UserDiscoveryStoreFlutter();
|
||||
|
||||
Future<AnnouncedUser?> getAnnouncedUserByPublicId({
|
||||
required PlatformInt64 publicId,
|
||||
}) => RustLib.instance.api
|
||||
.crateBridgeCallbacksUserDiscoveryUserDiscoveryStoreFlutterGetAnnouncedUserByPublicId(
|
||||
that: this,
|
||||
publicId: publicId,
|
||||
);
|
||||
|
||||
Future<String> getConfig() => RustLib.instance.api
|
||||
.crateBridgeCallbacksUserDiscoveryUserDiscoveryStoreFlutterGetConfig(
|
||||
that: this,
|
||||
);
|
||||
|
||||
Future<Uint8List?> getContactPromotion({
|
||||
required PlatformInt64 contactId,
|
||||
}) => RustLib.instance.api
|
||||
.crateBridgeCallbacksUserDiscoveryUserDiscoveryStoreFlutterGetContactPromotion(
|
||||
that: this,
|
||||
contactId: contactId,
|
||||
);
|
||||
|
||||
Future<Uint8List?> getContactVersion({
|
||||
required PlatformInt64 contactId,
|
||||
}) => RustLib.instance.api
|
||||
.crateBridgeCallbacksUserDiscoveryUserDiscoveryStoreFlutterGetContactVersion(
|
||||
that: this,
|
||||
contactId: contactId,
|
||||
);
|
||||
|
||||
Future<List<OtherPromotion>> getOtherPromotionsByPublicId({
|
||||
required PlatformInt64 publicId,
|
||||
}) => RustLib.instance.api
|
||||
.crateBridgeCallbacksUserDiscoveryUserDiscoveryStoreFlutterGetOtherPromotionsByPublicId(
|
||||
that: this,
|
||||
publicId: publicId,
|
||||
);
|
||||
|
||||
Future<List<Uint8List>> getOwnPromotionsAfterVersion({
|
||||
required int version,
|
||||
}) => RustLib.instance.api
|
||||
.crateBridgeCallbacksUserDiscoveryUserDiscoveryStoreFlutterGetOwnPromotionsAfterVersion(
|
||||
that: this,
|
||||
version: version,
|
||||
);
|
||||
|
||||
Future<Uint8List> getShareForContact({
|
||||
required PlatformInt64 contactId,
|
||||
}) => RustLib.instance.api
|
||||
.crateBridgeCallbacksUserDiscoveryUserDiscoveryStoreFlutterGetShareForContact(
|
||||
that: this,
|
||||
contactId: contactId,
|
||||
);
|
||||
|
||||
Future<void> pushNewUserRelation({
|
||||
required PlatformInt64 fromContactId,
|
||||
required AnnouncedUser announcedUser,
|
||||
PlatformInt64? publicKeyVerifiedTimestamp,
|
||||
}) => RustLib.instance.api
|
||||
.crateBridgeCallbacksUserDiscoveryUserDiscoveryStoreFlutterPushNewUserRelation(
|
||||
that: this,
|
||||
fromContactId: fromContactId,
|
||||
announcedUser: announcedUser,
|
||||
publicKeyVerifiedTimestamp: publicKeyVerifiedTimestamp,
|
||||
);
|
||||
|
||||
Future<void> pushOwnPromotionAndClearOldVersion({
|
||||
required PlatformInt64 contactId,
|
||||
required int version,
|
||||
required List<int> promotion,
|
||||
}) => RustLib.instance.api
|
||||
.crateBridgeCallbacksUserDiscoveryUserDiscoveryStoreFlutterPushOwnPromotionAndClearOldVersion(
|
||||
that: this,
|
||||
contactId: contactId,
|
||||
version: version,
|
||||
promotion: promotion,
|
||||
);
|
||||
|
||||
Future<void> setContactVersion({
|
||||
required PlatformInt64 contactId,
|
||||
required List<int> update,
|
||||
}) => RustLib.instance.api
|
||||
.crateBridgeCallbacksUserDiscoveryUserDiscoveryStoreFlutterSetContactVersion(
|
||||
that: this,
|
||||
contactId: contactId,
|
||||
update: update,
|
||||
);
|
||||
|
||||
Future<void> setShares({required List<Uint8List> shares}) => RustLib
|
||||
.instance
|
||||
.api
|
||||
.crateBridgeCallbacksUserDiscoveryUserDiscoveryStoreFlutterSetShares(
|
||||
that: this,
|
||||
shares: shares,
|
||||
);
|
||||
|
||||
Future<void> storeOtherPromotion({
|
||||
required OtherPromotion promotion,
|
||||
}) => RustLib.instance.api
|
||||
.crateBridgeCallbacksUserDiscoveryUserDiscoveryStoreFlutterStoreOtherPromotion(
|
||||
that: this,
|
||||
promotion: promotion,
|
||||
);
|
||||
|
||||
Future<void> updateConfig({required String update}) => RustLib.instance.api
|
||||
.crateBridgeCallbacksUserDiscoveryUserDiscoveryStoreFlutterUpdateConfig(
|
||||
that: this,
|
||||
update: update,
|
||||
);
|
||||
|
||||
@override
|
||||
int get hashCode => 0;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is UserDiscoveryStoreFlutter && runtimeType == other.runtimeType;
|
||||
}
|
||||
|
||||
class UserDiscoveryUtilsFlutter {
|
||||
const UserDiscoveryUtilsFlutter();
|
||||
|
||||
Future<Uint8List> signData({required List<int> inputData}) => RustLib
|
||||
.instance
|
||||
.api
|
||||
.crateBridgeCallbacksUserDiscoveryUserDiscoveryUtilsFlutterSignData(
|
||||
that: this,
|
||||
inputData: inputData,
|
||||
);
|
||||
|
||||
Future<bool> verifySignature({
|
||||
required List<int> inputData,
|
||||
required List<int> pubkey,
|
||||
required List<int> signature,
|
||||
}) => RustLib.instance.api
|
||||
.crateBridgeCallbacksUserDiscoveryUserDiscoveryUtilsFlutterVerifySignature(
|
||||
that: this,
|
||||
inputData: inputData,
|
||||
pubkey: pubkey,
|
||||
signature: signature,
|
||||
);
|
||||
|
||||
Future<bool> verifyStoredPubkey({
|
||||
required PlatformInt64 fromContactId,
|
||||
required List<int> pubkey,
|
||||
}) => RustLib.instance.api
|
||||
.crateBridgeCallbacksUserDiscoveryUserDiscoveryUtilsFlutterVerifyStoredPubkey(
|
||||
that: this,
|
||||
fromContactId: fromContactId,
|
||||
pubkey: pubkey,
|
||||
);
|
||||
|
||||
@override
|
||||
int get hashCode => 0;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is UserDiscoveryUtilsFlutter && runtimeType == other.runtimeType;
|
||||
}
|
||||
37
lib/core/bridge/wrapper.dart
Normal file
37
lib/core/bridge/wrapper.dart
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
// 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 RustUtils {
|
||||
const RustUtils();
|
||||
|
||||
static Future<List<Uint8List>> generateShares({
|
||||
required List<int> secret,
|
||||
required int total,
|
||||
required int threshold,
|
||||
}) => RustLib.instance.api.crateBridgeWrapperRustUtilsGenerateShares(
|
||||
secret: secret,
|
||||
total: total,
|
||||
threshold: threshold,
|
||||
);
|
||||
|
||||
static Future<Uint8List> recoverSecret({
|
||||
required List<Uint8List> shares,
|
||||
required int threshold,
|
||||
}) => RustLib.instance.api.crateBridgeWrapperRustUtilsRecoverSecret(
|
||||
shares: shares,
|
||||
threshold: threshold,
|
||||
);
|
||||
|
||||
@override
|
||||
int get hashCode => 0;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is RustUtils && runtimeType == other.runtimeType;
|
||||
}
|
||||
|
|
@ -20,6 +20,12 @@ class RustKeyManager {
|
|||
static Future<PlatformInt64?> getUserId() => RustLib.instance.api
|
||||
.crateBridgeWrapperKeyManagerRustKeyManagerGetUserId();
|
||||
|
||||
static Future<void> importSerialized({required List<int> serializedBytes}) =>
|
||||
RustLib.instance.api
|
||||
.crateBridgeWrapperKeyManagerRustKeyManagerImportSerialized(
|
||||
serializedBytes: serializedBytes,
|
||||
);
|
||||
|
||||
static Future<void> importSignalIdentity({
|
||||
required List<int> identityKeyPairStructure,
|
||||
required PlatformInt64 registrationId,
|
||||
|
|
@ -53,6 +59,10 @@ class RustKeyManager {
|
|||
signedPreKeyId: signedPreKeyId,
|
||||
);
|
||||
|
||||
/// Serialize the key_manager. Needed for the passwordless_recovery feature.
|
||||
static Future<Uint8List> serialize() => RustLib.instance.api
|
||||
.crateBridgeWrapperKeyManagerRustKeyManagerSerialize();
|
||||
|
||||
static Future<void> setUserId({required PlatformInt64 userId}) => RustLib
|
||||
.instance
|
||||
.api
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -5,6 +5,8 @@
|
|||
|
||||
import 'bridge.dart';
|
||||
import 'bridge/callbacks.dart';
|
||||
import 'bridge/callbacks/user_discovery.dart';
|
||||
import 'bridge/wrapper.dart';
|
||||
import 'bridge/wrapper/backup.dart';
|
||||
import 'bridge/wrapper/key_manager.dart';
|
||||
import 'bridge/wrapper/user_discovery.dart';
|
||||
|
|
@ -134,6 +136,19 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
InitConfig dco_decode_box_autoadd_init_config(dynamic raw);
|
||||
|
||||
@protected
|
||||
OtherPromotion dco_decode_box_autoadd_other_promotion(dynamic raw);
|
||||
|
||||
@protected
|
||||
UserDiscoveryStoreFlutter dco_decode_box_autoadd_user_discovery_store_flutter(
|
||||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
UserDiscoveryUtilsFlutter dco_decode_box_autoadd_user_discovery_utils_flutter(
|
||||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
FlutterUserDiscovery dco_decode_flutter_user_discovery(dynamic raw);
|
||||
|
||||
|
|
@ -205,6 +220,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
RustKeyManager dco_decode_rust_key_manager(dynamic raw);
|
||||
|
||||
@protected
|
||||
RustUtils dco_decode_rust_utils(dynamic raw);
|
||||
|
||||
@protected
|
||||
int dco_decode_u_32(dynamic raw);
|
||||
|
||||
|
|
@ -217,6 +235,16 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
void dco_decode_unit(dynamic raw);
|
||||
|
||||
@protected
|
||||
UserDiscoveryStoreFlutter dco_decode_user_discovery_store_flutter(
|
||||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
UserDiscoveryUtilsFlutter dco_decode_user_discovery_utils_flutter(
|
||||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
BigInt dco_decode_usize(dynamic raw);
|
||||
|
||||
|
|
@ -266,6 +294,21 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
InitConfig sse_decode_box_autoadd_init_config(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
OtherPromotion sse_decode_box_autoadd_other_promotion(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
UserDiscoveryStoreFlutter sse_decode_box_autoadd_user_discovery_store_flutter(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
UserDiscoveryUtilsFlutter sse_decode_box_autoadd_user_discovery_utils_flutter(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
FlutterUserDiscovery sse_decode_flutter_user_discovery(
|
||||
SseDeserializer deserializer,
|
||||
|
|
@ -357,6 +400,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
RustKeyManager sse_decode_rust_key_manager(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
RustUtils sse_decode_rust_utils(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
int sse_decode_u_32(SseDeserializer deserializer);
|
||||
|
||||
|
|
@ -369,6 +415,16 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
void sse_decode_unit(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
UserDiscoveryStoreFlutter sse_decode_user_discovery_store_flutter(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
UserDiscoveryUtilsFlutter sse_decode_user_discovery_utils_flutter(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
BigInt sse_decode_usize(SseDeserializer deserializer);
|
||||
|
||||
|
|
@ -517,6 +573,24 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_box_autoadd_other_promotion(
|
||||
OtherPromotion self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_box_autoadd_user_discovery_store_flutter(
|
||||
UserDiscoveryStoreFlutter self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_box_autoadd_user_discovery_utils_flutter(
|
||||
UserDiscoveryUtilsFlutter self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_flutter_user_discovery(
|
||||
FlutterUserDiscovery self,
|
||||
|
|
@ -634,6 +708,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_rust_utils(RustUtils self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_u_32(int self, SseSerializer serializer);
|
||||
|
||||
|
|
@ -646,6 +723,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
void sse_encode_unit(void self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_user_discovery_store_flutter(
|
||||
UserDiscoveryStoreFlutter self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_user_discovery_utils_flutter(
|
||||
UserDiscoveryUtilsFlutter self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_usize(BigInt self, SseSerializer serializer);
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@
|
|||
|
||||
import 'bridge.dart';
|
||||
import 'bridge/callbacks.dart';
|
||||
import 'bridge/callbacks/user_discovery.dart';
|
||||
import 'bridge/wrapper.dart';
|
||||
import 'bridge/wrapper/backup.dart';
|
||||
import 'bridge/wrapper/key_manager.dart';
|
||||
import 'bridge/wrapper/user_discovery.dart';
|
||||
|
|
@ -136,6 +138,19 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
InitConfig dco_decode_box_autoadd_init_config(dynamic raw);
|
||||
|
||||
@protected
|
||||
OtherPromotion dco_decode_box_autoadd_other_promotion(dynamic raw);
|
||||
|
||||
@protected
|
||||
UserDiscoveryStoreFlutter dco_decode_box_autoadd_user_discovery_store_flutter(
|
||||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
UserDiscoveryUtilsFlutter dco_decode_box_autoadd_user_discovery_utils_flutter(
|
||||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
FlutterUserDiscovery dco_decode_flutter_user_discovery(dynamic raw);
|
||||
|
||||
|
|
@ -207,6 +222,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
RustKeyManager dco_decode_rust_key_manager(dynamic raw);
|
||||
|
||||
@protected
|
||||
RustUtils dco_decode_rust_utils(dynamic raw);
|
||||
|
||||
@protected
|
||||
int dco_decode_u_32(dynamic raw);
|
||||
|
||||
|
|
@ -219,6 +237,16 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
void dco_decode_unit(dynamic raw);
|
||||
|
||||
@protected
|
||||
UserDiscoveryStoreFlutter dco_decode_user_discovery_store_flutter(
|
||||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
UserDiscoveryUtilsFlutter dco_decode_user_discovery_utils_flutter(
|
||||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
BigInt dco_decode_usize(dynamic raw);
|
||||
|
||||
|
|
@ -268,6 +296,21 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
InitConfig sse_decode_box_autoadd_init_config(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
OtherPromotion sse_decode_box_autoadd_other_promotion(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
UserDiscoveryStoreFlutter sse_decode_box_autoadd_user_discovery_store_flutter(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
UserDiscoveryUtilsFlutter sse_decode_box_autoadd_user_discovery_utils_flutter(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
FlutterUserDiscovery sse_decode_flutter_user_discovery(
|
||||
SseDeserializer deserializer,
|
||||
|
|
@ -359,6 +402,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
RustKeyManager sse_decode_rust_key_manager(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
RustUtils sse_decode_rust_utils(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
int sse_decode_u_32(SseDeserializer deserializer);
|
||||
|
||||
|
|
@ -371,6 +417,16 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
void sse_decode_unit(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
UserDiscoveryStoreFlutter sse_decode_user_discovery_store_flutter(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
UserDiscoveryUtilsFlutter sse_decode_user_discovery_utils_flutter(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
BigInt sse_decode_usize(SseDeserializer deserializer);
|
||||
|
||||
|
|
@ -519,6 +575,24 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_box_autoadd_other_promotion(
|
||||
OtherPromotion self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_box_autoadd_user_discovery_store_flutter(
|
||||
UserDiscoveryStoreFlutter self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_box_autoadd_user_discovery_utils_flutter(
|
||||
UserDiscoveryUtilsFlutter self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_flutter_user_discovery(
|
||||
FlutterUserDiscovery self,
|
||||
|
|
@ -636,6 +710,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_rust_utils(RustUtils self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_u_32(int self, SseSerializer serializer);
|
||||
|
||||
|
|
@ -648,6 +725,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
void sse_encode_unit(void self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_user_discovery_store_flutter(
|
||||
UserDiscoveryStoreFlutter self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_user_discovery_utils_flutter(
|
||||
UserDiscoveryUtilsFlutter self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_usize(BigInt self, SseSerializer serializer);
|
||||
|
||||
|
|
|
|||
|
|
@ -314,9 +314,11 @@ class UserDiscoveryCallbacks {
|
|||
|
||||
static Future<Uint8List?> getContactPromotion(int contactId) async {
|
||||
try {
|
||||
final row = await (twonlyDB.select(
|
||||
twonlyDB.userDiscoveryOwnPromotions,
|
||||
)..where((tbl) => tbl.contactId.equals(contactId))).getSingleOrNull();
|
||||
final query = twonlyDB.select(twonlyDB.userDiscoveryOwnPromotions)
|
||||
..where((tbl) => tbl.contactId.equals(contactId))
|
||||
..orderBy([(tbl) => OrderingTerm.desc(tbl.versionId)])
|
||||
..limit(1);
|
||||
final row = await query.getSingleOrNull();
|
||||
return row?.promotion;
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ abstract class VideoCompressionChannel {
|
|||
});
|
||||
return outputPath;
|
||||
} on PlatformException catch (e) {
|
||||
Log.error('Failed to compress video: $e');
|
||||
Log.warn('Failed to compress video: $e');
|
||||
return null;
|
||||
} finally {
|
||||
_currentProgressCallback = null;
|
||||
|
|
|
|||
|
|
@ -3,4 +3,5 @@ class KeyValueKeys {
|
|||
'last_periodic_task_execution';
|
||||
static const String currentBackupState = 'current_backup_state';
|
||||
static const String backupRecoveryState = 'backup_recovery_state';
|
||||
static const String onboardingState = 'onboarding_state';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ class Routes {
|
|||
static String profileContact(int contactId) => '/profile/contact/$contactId';
|
||||
|
||||
static const String cameraQRScanner = '/camera/qr_scanner';
|
||||
static const String recoverPasswordless = '/recover/passwordless';
|
||||
|
||||
static const String settings = '/settings';
|
||||
static const String settingsProfile = '/settings/profile';
|
||||
|
|
@ -35,8 +36,6 @@ class Routes {
|
|||
'/settings/privacy/block_users';
|
||||
static const String settingsPrivacyUserDiscovery =
|
||||
'/settings/privacy/user_discovery';
|
||||
static const String settingsPrivacyProfileSelection =
|
||||
'/settings/privacy/profile_selection';
|
||||
static const String settingsNotification = '/settings/notification';
|
||||
static const String settingsStorage = '/settings/storage_data';
|
||||
static const String settingsStorageManage = '/settings/storage_data/manage';
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
|||
|
||||
final result = await _insertGroup(insertGroup);
|
||||
if (result != null) {
|
||||
await into(groupMembers).insert(
|
||||
await into(groupMembers).insertOnConflictUpdate(
|
||||
GroupMembersCompanion(
|
||||
groupId: Value(result.groupId),
|
||||
contactId: Value(
|
||||
|
|
|
|||
|
|
@ -259,6 +259,54 @@ class KeyVerificationDao extends DatabaseAccessor<TwonlyDB>
|
|||
});
|
||||
}
|
||||
|
||||
Stream<int> watchUnverifiedGroupMembersCount(String groupId) {
|
||||
final gm = groupMembers;
|
||||
final directKv = alias(keyVerifications, 'directKv');
|
||||
final ur = userDiscoveryUserRelations;
|
||||
final verifierKv = alias(keyVerifications, 'verifierKv');
|
||||
|
||||
final query = select(gm).join([
|
||||
leftOuterJoin(directKv, directKv.contactId.equalsExp(gm.contactId)),
|
||||
leftOuterJoin(
|
||||
ur,
|
||||
ur.announcedUserId.equalsExp(gm.contactId) &
|
||||
ur.publicKeyVerifiedTimestamp.isNotNull() &
|
||||
ur.fromContactId.equalsExp(gm.contactId).not(),
|
||||
),
|
||||
leftOuterJoin(
|
||||
verifierKv,
|
||||
verifierKv.contactId.equalsExp(ur.fromContactId),
|
||||
),
|
||||
])..where(gm.groupId.equals(groupId));
|
||||
|
||||
return query.watch().map((rows) {
|
||||
if (rows.isEmpty) return 0;
|
||||
|
||||
final memberTrustMap = <int, ({bool direct, bool partial})>{};
|
||||
|
||||
for (final row in rows) {
|
||||
final contactId = row.readTable(gm).contactId;
|
||||
final isDirect = row.readTableOrNull(directKv) != null;
|
||||
final isPartial = row.readTableOrNull(verifierKv) != null;
|
||||
|
||||
final current =
|
||||
memberTrustMap[contactId] ?? (direct: false, partial: false);
|
||||
memberTrustMap[contactId] = (
|
||||
direct: current.direct || isDirect,
|
||||
partial: current.partial || isPartial,
|
||||
);
|
||||
}
|
||||
|
||||
var count = 0;
|
||||
for (final trust in memberTrustMap.values) {
|
||||
if (!trust.direct && !trust.partial) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> addKeyVerification(
|
||||
int contactId,
|
||||
VerificationType type, {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:hashlib/random.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
import 'package:twonly/src/database/tables/messages.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
part 'mediafiles.dao.g.dart';
|
||||
|
||||
@DriftAccessor(tables: [MediaFiles])
|
||||
@DriftAccessor(tables: [MediaFiles, Messages])
|
||||
class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
|
||||
with _$MediaFilesDaoMixin {
|
||||
// this constructor is required so that the main database can create an instance
|
||||
|
|
@ -142,7 +143,9 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
|
|||
final query =
|
||||
(select(mediaFiles)..where((t) => t.stored.equals(true))).join([])
|
||||
..groupBy([
|
||||
const CustomExpression<Object>('COALESCE(stored_file_hash, media_id)')
|
||||
const CustomExpression<Object>(
|
||||
'COALESCE(stored_file_hash, media_id)',
|
||||
),
|
||||
]);
|
||||
return query.map((row) => row.readTable(mediaFiles)).watch();
|
||||
}
|
||||
|
|
@ -154,6 +157,17 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
|
|||
.watch();
|
||||
}
|
||||
|
||||
Stream<List<MediaFile>> watchMediaFilesForGroup(String groupId) {
|
||||
final query = select(mediaFiles).join([
|
||||
innerJoin(
|
||||
db.messages,
|
||||
db.messages.mediaId.equalsExp(mediaFiles.mediaId),
|
||||
useColumns: false,
|
||||
),
|
||||
])..where(db.messages.groupId.equals(groupId));
|
||||
return query.map((row) => row.readTable(mediaFiles)).watch();
|
||||
}
|
||||
|
||||
Future<void> updateAllRetransmissionUploadingState() async {
|
||||
await (update(mediaFiles)..where(
|
||||
(t) =>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ part of 'mediafiles.dao.dart';
|
|||
// ignore_for_file: type=lint
|
||||
mixin _$MediaFilesDaoMixin on DatabaseAccessor<TwonlyDB> {
|
||||
$MediaFilesTable get mediaFiles => attachedDatabase.mediaFiles;
|
||||
$GroupsTable get groups => attachedDatabase.groups;
|
||||
$ContactsTable get contacts => attachedDatabase.contacts;
|
||||
$MessagesTable get messages => attachedDatabase.messages;
|
||||
MediaFilesDaoManager get managers => MediaFilesDaoManager(this);
|
||||
}
|
||||
|
||||
|
|
@ -13,4 +16,10 @@ class MediaFilesDaoManager {
|
|||
MediaFilesDaoManager(this._db);
|
||||
$$MediaFilesTableTableManager get mediaFiles =>
|
||||
$$MediaFilesTableTableManager(_db.attachedDatabase, _db.mediaFiles);
|
||||
$$GroupsTableTableManager get groups =>
|
||||
$$GroupsTableTableManager(_db.attachedDatabase, _db.groups);
|
||||
$$ContactsTableTableManager get contacts =>
|
||||
$$ContactsTableTableManager(_db.attachedDatabase, _db.contacts);
|
||||
$$MessagesTableTableManager get messages =>
|
||||
$$MessagesTableTableManager(_db.attachedDatabase, _db.messages);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,15 +32,25 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
MessagesDao(super.db);
|
||||
|
||||
Stream<List<Message>> watchMessageNotOpened(String groupId) {
|
||||
return (select(messages)
|
||||
final query =
|
||||
select(messages).join([
|
||||
leftOuterJoin(
|
||||
mediaFiles,
|
||||
mediaFiles.mediaId.equalsExp(messages.mediaId),
|
||||
),
|
||||
])
|
||||
..where(
|
||||
(t) =>
|
||||
t.openedAt.isNull() &
|
||||
t.groupId.equals(groupId) &
|
||||
t.isDeletedFromSender.equals(false),
|
||||
messages.openedAt.isNull() &
|
||||
messages.groupId.equals(groupId) &
|
||||
messages.isDeletedFromSender.equals(false) &
|
||||
(messages.mediaId.isNull() |
|
||||
mediaFiles.downloadState.isNull() |
|
||||
mediaFiles.downloadState
|
||||
.equals(DownloadState.reuploadRequested.name)
|
||||
.not()),
|
||||
)
|
||||
..orderBy([(t) => OrderingTerm.desc(t.createdAt)]))
|
||||
.watch();
|
||||
..orderBy([OrderingTerm.desc(messages.createdAt)]);
|
||||
return query.map((row) => row.readTable(messages)).watch();
|
||||
}
|
||||
|
||||
Stream<List<Message>> watchMediaNotOpened(String groupId) {
|
||||
|
|
@ -52,9 +62,10 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
),
|
||||
])
|
||||
..where(
|
||||
mediaFiles.downloadState
|
||||
.equals(DownloadState.reuploadRequested.name)
|
||||
.not() &
|
||||
(mediaFiles.downloadState.isNull() |
|
||||
mediaFiles.downloadState
|
||||
.equals(DownloadState.reuploadRequested.name)
|
||||
.not()) &
|
||||
mediaFiles.type.equals(MediaType.audio.name).not() &
|
||||
messages.openedAt.isNull() &
|
||||
messages.groupId.equals(groupId) &
|
||||
|
|
@ -73,8 +84,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
mediaFiles,
|
||||
mediaFiles.mediaId.equalsExp(messages.mediaId),
|
||||
),
|
||||
])
|
||||
..where(
|
||||
])..where(
|
||||
messages.openedAt.isNull() &
|
||||
messages.mediaId.isNotNull() &
|
||||
messages.type.equals(MessageType.media.name) &
|
||||
|
|
@ -92,19 +102,29 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
milliseconds: group!.deleteMessagesAfterMilliseconds,
|
||||
),
|
||||
);
|
||||
return (select(messages)
|
||||
final query =
|
||||
select(messages).join([
|
||||
leftOuterJoin(
|
||||
mediaFiles,
|
||||
mediaFiles.mediaId.equalsExp(messages.mediaId),
|
||||
),
|
||||
])
|
||||
..where(
|
||||
(t) =>
|
||||
t.groupId.equals(groupId) &
|
||||
messages.groupId.equals(groupId) &
|
||||
// messages in groups will only be removed in case all members have received it...
|
||||
// so ensuring that this message is not shown in the messages anymore
|
||||
(t.openedAt.isBiggerThanValue(deletionTime) |
|
||||
t.openedAt.isNull() |
|
||||
t.mediaStored.equals(true)),
|
||||
(messages.openedAt.isBiggerThanValue(deletionTime) |
|
||||
messages.openedAt.isNull() |
|
||||
messages.mediaStored.equals(true)) &
|
||||
(mediaFiles.downloadState
|
||||
.equals(DownloadState.reuploadRequested.name)
|
||||
.not() |
|
||||
mediaFiles.downloadState.isNull()),
|
||||
)
|
||||
..orderBy([(t) => OrderingTerm.desc(t.createdAt)])
|
||||
..limit(1))
|
||||
.watchSingleOrNull();
|
||||
..orderBy([OrderingTerm.desc(messages.createdAt)])
|
||||
..limit(1);
|
||||
|
||||
return query.map((row) => row.readTable(messages)).watchSingleOrNull();
|
||||
}
|
||||
|
||||
Future<Stream<List<Message>>> watchByGroupId(String groupId) async {
|
||||
|
|
@ -264,26 +284,28 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
String text,
|
||||
DateTime timestamp,
|
||||
) async {
|
||||
final msg = await getMessageById(messageId).getSingleOrNull();
|
||||
if (msg == null || msg.content == null || msg.senderId != contactId) {
|
||||
return;
|
||||
}
|
||||
await into(messageHistories).insert(
|
||||
MessageHistoriesCompanion(
|
||||
messageId: Value(messageId),
|
||||
content: Value(msg.content),
|
||||
createdAt: Value(timestamp),
|
||||
),
|
||||
);
|
||||
await (update(messages)..where(
|
||||
(t) => t.messageId.equals(messageId),
|
||||
))
|
||||
.write(
|
||||
MessagesCompanion(
|
||||
content: Value(text),
|
||||
modifiedAt: Value(timestamp),
|
||||
),
|
||||
);
|
||||
await transaction(() async {
|
||||
final msg = await getMessageById(messageId).getSingleOrNull();
|
||||
if (msg == null || msg.content == null || msg.senderId != contactId) {
|
||||
return;
|
||||
}
|
||||
await into(messageHistories).insert(
|
||||
MessageHistoriesCompanion(
|
||||
messageId: Value(messageId),
|
||||
content: Value(msg.content),
|
||||
createdAt: Value(timestamp),
|
||||
),
|
||||
);
|
||||
await (update(messages)..where(
|
||||
(t) => t.messageId.equals(messageId),
|
||||
))
|
||||
.write(
|
||||
MessagesCompanion(
|
||||
content: Value(text),
|
||||
modifiedAt: Value(timestamp),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> handleMessagesOpened(
|
||||
|
|
@ -291,21 +313,37 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
List<String> messageIds,
|
||||
DateTime timestamp,
|
||||
) async {
|
||||
if (contactId.present) {
|
||||
final contactExists = await twonlyDB.contactsDao.getContactById(
|
||||
contactId.value,
|
||||
);
|
||||
if (contactExists == null) {
|
||||
Log.info(
|
||||
'handleMessagesOpened: Contact ${contactId.value} does not exist in database, ignoring messages opened action.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (final messageId in messageIds) {
|
||||
try {
|
||||
var actionTimestamp = timestamp;
|
||||
final msg = await getMessageById(messageId).getSingleOrNull();
|
||||
if (msg != null && actionTimestamp.isBefore(msg.createdAt)) {
|
||||
Log.warn(
|
||||
'Receiver clock skew detected for message $messageId. '
|
||||
'Action timestamp $actionTimestamp is before message creation ${msg.createdAt}. '
|
||||
'Clamping to creation time.',
|
||||
);
|
||||
actionTimestamp = msg.createdAt;
|
||||
}
|
||||
|
||||
final ts = actionTimestamp;
|
||||
await transaction(() async {
|
||||
final msg = await getMessageById(messageId).getSingleOrNull();
|
||||
if (msg == null) {
|
||||
Log.info(
|
||||
'handleMessagesOpened: Message $messageId does not exist in database, skipping.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
var ts = timestamp;
|
||||
if (ts.isBefore(msg.createdAt)) {
|
||||
Log.warn(
|
||||
'Receiver clock skew detected for message $messageId. '
|
||||
'Action timestamp $ts is before message creation ${msg.createdAt}. '
|
||||
'Clamping to creation time.',
|
||||
);
|
||||
ts = msg.createdAt;
|
||||
}
|
||||
|
||||
await into(messageActions).insertOnConflictUpdate(
|
||||
MessageActionsCompanion(
|
||||
messageId: Value(messageId),
|
||||
|
|
@ -339,7 +377,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
messages,
|
||||
)..where((tbl) => tbl.messageId.equals(messageId))).write(
|
||||
MessagesCompanion(
|
||||
openedAt: Value(actionTimestamp),
|
||||
openedAt: Value(timestamp),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -348,7 +386,11 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
'handleMessagesOpened completed for message $messageId',
|
||||
);
|
||||
} catch (e) {
|
||||
Log.error('handleMessagesOpened failed for $messageId: $e');
|
||||
Log.warn('handleMessagesOpened failed for $messageId: $e');
|
||||
Log.error(
|
||||
'handleMessagesOpened failed for: $e',
|
||||
onlyIfSentryEnabled: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -358,7 +400,21 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
String messageId,
|
||||
DateTime timestamp,
|
||||
) async {
|
||||
final contactExists = await twonlyDB.contactsDao.getContactById(contactId);
|
||||
if (contactExists == null) {
|
||||
Log.info(
|
||||
'handleMessageAckByServer: Contact $contactId does not exist in database, ignoring message ack.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
await transaction(() async {
|
||||
final msg = await getMessageById(messageId).getSingleOrNull();
|
||||
if (msg == null) {
|
||||
Log.info(
|
||||
'handleMessageAckByServer: Message $messageId does not exist in database, skipping.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
await into(messageActions).insertOnConflictUpdate(
|
||||
MessageActionsCompanion(
|
||||
messageId: Value(messageId),
|
||||
|
|
@ -472,6 +528,15 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
.getSingleOrNull();
|
||||
}
|
||||
|
||||
Stream<MessageAction?> watchLastMessageAction(String messageId) {
|
||||
return (((select(messageActions)..where(
|
||||
(t) => t.messageId.equals(messageId),
|
||||
))
|
||||
..orderBy([(t) => OrderingTerm.desc(t.actionAt)]))
|
||||
..limit(1))
|
||||
.watchSingleOrNull();
|
||||
}
|
||||
|
||||
Future<void> deleteMessagesById(String messageId) {
|
||||
return (delete(messages)..where((t) => t.messageId.equals(messageId))).go();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,11 +30,15 @@ class ReactionsDao extends DatabaseAccessor<TwonlyDB> with _$ReactionsDaoMixin {
|
|||
.getMessageById(messageId)
|
||||
.getSingleOrNull();
|
||||
if (msg == null) {
|
||||
Log.error('updateReaction: Message $messageId not found!');
|
||||
Log.warn('updateReaction: Message $messageId not found!');
|
||||
return;
|
||||
}
|
||||
if (msg.groupId != groupId) {
|
||||
Log.error('updateReaction: Message groupId ${msg.groupId} != $groupId');
|
||||
Log.warn('updateReaction: Message groupId ${msg.groupId} != $groupId');
|
||||
Log.error(
|
||||
'updateReaction: Message groupId mismatch',
|
||||
onlyIfSentryEnabled: true,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
|
|||
))
|
||||
.go();
|
||||
}
|
||||
|
||||
|
||||
Future<void> deleteReceiptsByMessageId(String messageId) async {
|
||||
await (delete(receipts)..where(
|
||||
(t) => t.messageId.equals(messageId),
|
||||
|
|
@ -201,8 +201,8 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
|
|||
);
|
||||
final updatedReceipt = await getReceiptById(newReceiptId);
|
||||
if (updatedReceipt == null) {
|
||||
Log.error(
|
||||
'Tried to change the receipt ID, but could not get the updated receipt...',
|
||||
Log.warn(
|
||||
'[$oldReceiptId] Tried to change the receipt ID to $newReceiptId, but could not get the updated receipt...',
|
||||
);
|
||||
}
|
||||
return updatedReceipt;
|
||||
|
|
@ -253,6 +253,9 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
|
|||
Future<void> gotReceipt(String receiptId) async {
|
||||
await into(
|
||||
receivedReceipts,
|
||||
).insert(ReceivedReceiptsCompanion(receiptId: Value(receiptId)));
|
||||
).insert(
|
||||
ReceivedReceiptsCompanion(receiptId: Value(receiptId)),
|
||||
mode: InsertMode.insertOrIgnore,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/tables/contacts.table.dart';
|
||||
import 'package:twonly/src/database/tables/user_discovery.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
|
|
@ -184,23 +185,51 @@ class UserDiscoveryDao extends DatabaseAccessor<TwonlyDB>
|
|||
results[user]!.add(relationData);
|
||||
}
|
||||
|
||||
final threshold = userService.currentUser.userDiscoveryThreshold;
|
||||
results.removeWhere((user, relations) => relations.length < threshold);
|
||||
|
||||
return results;
|
||||
});
|
||||
}
|
||||
|
||||
Stream<int> watchNewAnnouncementsWithDataCount() {
|
||||
final countExp = userDiscoveryAnnouncedUsers.announcedUserId.count();
|
||||
final announcedContact = alias(contacts, 'announcedContact');
|
||||
final query =
|
||||
select(userDiscoveryAnnouncedUsers).join([
|
||||
innerJoin(
|
||||
userDiscoveryUserRelations,
|
||||
userDiscoveryUserRelations.announcedUserId.equalsExp(
|
||||
userDiscoveryAnnouncedUsers.announcedUserId,
|
||||
),
|
||||
),
|
||||
leftOuterJoin(
|
||||
announcedContact,
|
||||
announcedContact.userId.equalsExp(
|
||||
userDiscoveryAnnouncedUsers.announcedUserId,
|
||||
),
|
||||
),
|
||||
])..where(
|
||||
// Filters: Has a username AND has not been shown to the user yet AND is not an existing contact
|
||||
userDiscoveryAnnouncedUsers.username.isNotNull() &
|
||||
userDiscoveryAnnouncedUsers.wasShownToTheUser.equals(false) &
|
||||
userDiscoveryAnnouncedUsers.isHidden.equals(false) &
|
||||
(announcedContact.userId.isNull() |
|
||||
announcedContact.deletedByUser.equals(true)),
|
||||
);
|
||||
|
||||
final query = selectOnly(userDiscoveryAnnouncedUsers)
|
||||
..addColumns([countExp])
|
||||
..where(
|
||||
// Filters: Has a username AND has not been shown to the user yet
|
||||
userDiscoveryAnnouncedUsers.username.isNotNull() &
|
||||
userDiscoveryAnnouncedUsers.wasShownToTheUser.equals(false) &
|
||||
userDiscoveryAnnouncedUsers.isHidden.equals(false),
|
||||
);
|
||||
return query.watch().map((rows) {
|
||||
final relationCounts = <int, int>{};
|
||||
for (final row in rows) {
|
||||
final announcedUserId = row
|
||||
.readTable(userDiscoveryAnnouncedUsers)
|
||||
.announcedUserId;
|
||||
relationCounts[announcedUserId] =
|
||||
(relationCounts[announcedUserId] ?? 0) + 1;
|
||||
}
|
||||
|
||||
return query.watchSingle().map((row) => row.read(countExp) ?? 0);
|
||||
final threshold = userService.currentUser.userDiscoveryThreshold;
|
||||
return relationCounts.values.where((count) => count >= threshold).length;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> markAllValidAnnouncedUsersAsShown() async {
|
||||
|
|
@ -235,9 +264,9 @@ class UserDiscoveryDao extends DatabaseAccessor<TwonlyDB>
|
|||
}
|
||||
|
||||
Stream<UserDiscoveryAnnouncedUser?> watchAnnouncedUser(int id) {
|
||||
return (select(userDiscoveryAnnouncedUsers)
|
||||
..where((tbl) => tbl.announcedUserId.equals(id)))
|
||||
.watchSingleOrNull();
|
||||
return (select(
|
||||
userDiscoveryAnnouncedUsers,
|
||||
)..where((tbl) => tbl.announcedUserId.equals(id))).watchSingleOrNull();
|
||||
}
|
||||
|
||||
Stream<List<UserDiscoveryAnnouncedUser>> watchAllAnnouncedUsers() =>
|
||||
|
|
|
|||
3127
lib/src/database/schemas/twonly_db/drift_schema_v21.json
Normal file
3127
lib/src/database/schemas/twonly_db/drift_schema_v21.json
Normal file
File diff suppressed because it is too large
Load diff
3137
lib/src/database/schemas/twonly_db/drift_schema_v22.json
Normal file
3137
lib/src/database/schemas/twonly_db/drift_schema_v22.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -42,7 +42,9 @@ class SignalPreKeyStore extends PreKeyStore {
|
|||
);
|
||||
|
||||
try {
|
||||
await twonlyDB.into(twonlyDB.signalPreKeyStores).insert(preKeyCompanion);
|
||||
await twonlyDB
|
||||
.into(twonlyDB.signalPreKeyStores)
|
||||
.insert(preKeyCompanion, mode: InsertMode.insertOrReplace);
|
||||
} catch (e) {
|
||||
Log.error('$e');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,11 @@ class Contacts extends Table {
|
|||
DateTimeColumn get recoveryLastHeartbeat => dateTime().nullable()();
|
||||
BlobColumn get recoverySecretShare => blob().nullable()();
|
||||
|
||||
// This is the share from the contact in case the contact has selected this user as his trusted friend.
|
||||
BlobColumn get recoveryContactsSecretShare => blob().nullable()();
|
||||
DateTimeColumn get recoveryContactsLastHeartbeat => dateTime().nullable()();
|
||||
IntColumn get recoveryContactsThreshold => integer().nullable()();
|
||||
|
||||
BoolColumn get askForFriendPromotions => boolean().nullable()();
|
||||
|
||||
IntColumn get mediaSendCounter => integer().withDefault(const Constant(0))();
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ class TwonlyDB extends _$TwonlyDB {
|
|||
TwonlyDB.forTesting(DatabaseConnection super.connection);
|
||||
|
||||
@override
|
||||
int get schemaVersion => 20;
|
||||
int get schemaVersion => 22;
|
||||
|
||||
static QueryExecutor _openConnection() {
|
||||
final connection = driftDatabase(
|
||||
|
|
@ -259,6 +259,22 @@ class TwonlyDB extends _$TwonlyDB {
|
|||
schema.contacts.recoverySecretShare,
|
||||
);
|
||||
},
|
||||
from20To21: (m, schema) async {
|
||||
await m.addColumn(
|
||||
schema.contacts,
|
||||
schema.contacts.recoveryContactsSecretShare,
|
||||
);
|
||||
await m.addColumn(
|
||||
schema.contacts,
|
||||
schema.contacts.recoveryContactsLastHeartbeat,
|
||||
);
|
||||
},
|
||||
from21To22: (m, schema) async {
|
||||
await m.addColumn(
|
||||
schema.contacts,
|
||||
schema.contacts.recoveryContactsThreshold,
|
||||
);
|
||||
},
|
||||
)(m, from, to);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -252,6 +252,39 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> {
|
|||
type: DriftSqlType.blob,
|
||||
requiredDuringInsert: false,
|
||||
);
|
||||
static const VerificationMeta _recoveryContactsSecretShareMeta =
|
||||
const VerificationMeta('recoveryContactsSecretShare');
|
||||
@override
|
||||
late final GeneratedColumn<Uint8List> recoveryContactsSecretShare =
|
||||
GeneratedColumn<Uint8List>(
|
||||
'recovery_contacts_secret_share',
|
||||
aliasedName,
|
||||
true,
|
||||
type: DriftSqlType.blob,
|
||||
requiredDuringInsert: false,
|
||||
);
|
||||
static const VerificationMeta _recoveryContactsLastHeartbeatMeta =
|
||||
const VerificationMeta('recoveryContactsLastHeartbeat');
|
||||
@override
|
||||
late final GeneratedColumn<DateTime> recoveryContactsLastHeartbeat =
|
||||
GeneratedColumn<DateTime>(
|
||||
'recovery_contacts_last_heartbeat',
|
||||
aliasedName,
|
||||
true,
|
||||
type: DriftSqlType.dateTime,
|
||||
requiredDuringInsert: false,
|
||||
);
|
||||
static const VerificationMeta _recoveryContactsThresholdMeta =
|
||||
const VerificationMeta('recoveryContactsThreshold');
|
||||
@override
|
||||
late final GeneratedColumn<int> recoveryContactsThreshold =
|
||||
GeneratedColumn<int>(
|
||||
'recovery_contacts_threshold',
|
||||
aliasedName,
|
||||
true,
|
||||
type: DriftSqlType.int,
|
||||
requiredDuringInsert: false,
|
||||
);
|
||||
static const VerificationMeta _askForFriendPromotionsMeta =
|
||||
const VerificationMeta('askForFriendPromotions');
|
||||
@override
|
||||
|
|
@ -310,6 +343,9 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> {
|
|||
recoveryIsTrustedFriend,
|
||||
recoveryLastHeartbeat,
|
||||
recoverySecretShare,
|
||||
recoveryContactsSecretShare,
|
||||
recoveryContactsLastHeartbeat,
|
||||
recoveryContactsThreshold,
|
||||
askForFriendPromotions,
|
||||
mediaSendCounter,
|
||||
mediaReceivedCounter,
|
||||
|
|
@ -475,6 +511,33 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> {
|
|||
),
|
||||
);
|
||||
}
|
||||
if (data.containsKey('recovery_contacts_secret_share')) {
|
||||
context.handle(
|
||||
_recoveryContactsSecretShareMeta,
|
||||
recoveryContactsSecretShare.isAcceptableOrUnknown(
|
||||
data['recovery_contacts_secret_share']!,
|
||||
_recoveryContactsSecretShareMeta,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (data.containsKey('recovery_contacts_last_heartbeat')) {
|
||||
context.handle(
|
||||
_recoveryContactsLastHeartbeatMeta,
|
||||
recoveryContactsLastHeartbeat.isAcceptableOrUnknown(
|
||||
data['recovery_contacts_last_heartbeat']!,
|
||||
_recoveryContactsLastHeartbeatMeta,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (data.containsKey('recovery_contacts_threshold')) {
|
||||
context.handle(
|
||||
_recoveryContactsThresholdMeta,
|
||||
recoveryContactsThreshold.isAcceptableOrUnknown(
|
||||
data['recovery_contacts_threshold']!,
|
||||
_recoveryContactsThresholdMeta,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (data.containsKey('ask_for_friend_promotions')) {
|
||||
context.handle(
|
||||
_askForFriendPromotionsMeta,
|
||||
|
|
@ -587,6 +650,18 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> {
|
|||
DriftSqlType.blob,
|
||||
data['${effectivePrefix}recovery_secret_share'],
|
||||
),
|
||||
recoveryContactsSecretShare: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.blob,
|
||||
data['${effectivePrefix}recovery_contacts_secret_share'],
|
||||
),
|
||||
recoveryContactsLastHeartbeat: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.dateTime,
|
||||
data['${effectivePrefix}recovery_contacts_last_heartbeat'],
|
||||
),
|
||||
recoveryContactsThreshold: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.int,
|
||||
data['${effectivePrefix}recovery_contacts_threshold'],
|
||||
),
|
||||
askForFriendPromotions: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.bool,
|
||||
data['${effectivePrefix}ask_for_friend_promotions'],
|
||||
|
|
@ -628,6 +703,9 @@ class Contact extends DataClass implements Insertable<Contact> {
|
|||
final bool recoveryIsTrustedFriend;
|
||||
final DateTime? recoveryLastHeartbeat;
|
||||
final Uint8List? recoverySecretShare;
|
||||
final Uint8List? recoveryContactsSecretShare;
|
||||
final DateTime? recoveryContactsLastHeartbeat;
|
||||
final int? recoveryContactsThreshold;
|
||||
final bool? askForFriendPromotions;
|
||||
final int mediaSendCounter;
|
||||
final int mediaReceivedCounter;
|
||||
|
|
@ -651,6 +729,9 @@ class Contact extends DataClass implements Insertable<Contact> {
|
|||
required this.recoveryIsTrustedFriend,
|
||||
this.recoveryLastHeartbeat,
|
||||
this.recoverySecretShare,
|
||||
this.recoveryContactsSecretShare,
|
||||
this.recoveryContactsLastHeartbeat,
|
||||
this.recoveryContactsThreshold,
|
||||
this.askForFriendPromotions,
|
||||
required this.mediaSendCounter,
|
||||
required this.mediaReceivedCounter,
|
||||
|
|
@ -695,6 +776,21 @@ class Contact extends DataClass implements Insertable<Contact> {
|
|||
if (!nullToAbsent || recoverySecretShare != null) {
|
||||
map['recovery_secret_share'] = Variable<Uint8List>(recoverySecretShare);
|
||||
}
|
||||
if (!nullToAbsent || recoveryContactsSecretShare != null) {
|
||||
map['recovery_contacts_secret_share'] = Variable<Uint8List>(
|
||||
recoveryContactsSecretShare,
|
||||
);
|
||||
}
|
||||
if (!nullToAbsent || recoveryContactsLastHeartbeat != null) {
|
||||
map['recovery_contacts_last_heartbeat'] = Variable<DateTime>(
|
||||
recoveryContactsLastHeartbeat,
|
||||
);
|
||||
}
|
||||
if (!nullToAbsent || recoveryContactsThreshold != null) {
|
||||
map['recovery_contacts_threshold'] = Variable<int>(
|
||||
recoveryContactsThreshold,
|
||||
);
|
||||
}
|
||||
if (!nullToAbsent || askForFriendPromotions != null) {
|
||||
map['ask_for_friend_promotions'] = Variable<bool>(askForFriendPromotions);
|
||||
}
|
||||
|
|
@ -739,6 +835,18 @@ class Contact extends DataClass implements Insertable<Contact> {
|
|||
recoverySecretShare: recoverySecretShare == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(recoverySecretShare),
|
||||
recoveryContactsSecretShare:
|
||||
recoveryContactsSecretShare == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(recoveryContactsSecretShare),
|
||||
recoveryContactsLastHeartbeat:
|
||||
recoveryContactsLastHeartbeat == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(recoveryContactsLastHeartbeat),
|
||||
recoveryContactsThreshold:
|
||||
recoveryContactsThreshold == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(recoveryContactsThreshold),
|
||||
askForFriendPromotions: askForFriendPromotions == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(askForFriendPromotions),
|
||||
|
|
@ -788,6 +896,15 @@ class Contact extends DataClass implements Insertable<Contact> {
|
|||
recoverySecretShare: serializer.fromJson<Uint8List?>(
|
||||
json['recoverySecretShare'],
|
||||
),
|
||||
recoveryContactsSecretShare: serializer.fromJson<Uint8List?>(
|
||||
json['recoveryContactsSecretShare'],
|
||||
),
|
||||
recoveryContactsLastHeartbeat: serializer.fromJson<DateTime?>(
|
||||
json['recoveryContactsLastHeartbeat'],
|
||||
),
|
||||
recoveryContactsThreshold: serializer.fromJson<int?>(
|
||||
json['recoveryContactsThreshold'],
|
||||
),
|
||||
askForFriendPromotions: serializer.fromJson<bool?>(
|
||||
json['askForFriendPromotions'],
|
||||
),
|
||||
|
|
@ -828,6 +945,15 @@ class Contact extends DataClass implements Insertable<Contact> {
|
|||
recoveryLastHeartbeat,
|
||||
),
|
||||
'recoverySecretShare': serializer.toJson<Uint8List?>(recoverySecretShare),
|
||||
'recoveryContactsSecretShare': serializer.toJson<Uint8List?>(
|
||||
recoveryContactsSecretShare,
|
||||
),
|
||||
'recoveryContactsLastHeartbeat': serializer.toJson<DateTime?>(
|
||||
recoveryContactsLastHeartbeat,
|
||||
),
|
||||
'recoveryContactsThreshold': serializer.toJson<int?>(
|
||||
recoveryContactsThreshold,
|
||||
),
|
||||
'askForFriendPromotions': serializer.toJson<bool?>(
|
||||
askForFriendPromotions,
|
||||
),
|
||||
|
|
@ -856,6 +982,9 @@ class Contact extends DataClass implements Insertable<Contact> {
|
|||
bool? recoveryIsTrustedFriend,
|
||||
Value<DateTime?> recoveryLastHeartbeat = const Value.absent(),
|
||||
Value<Uint8List?> recoverySecretShare = const Value.absent(),
|
||||
Value<Uint8List?> recoveryContactsSecretShare = const Value.absent(),
|
||||
Value<DateTime?> recoveryContactsLastHeartbeat = const Value.absent(),
|
||||
Value<int?> recoveryContactsThreshold = const Value.absent(),
|
||||
Value<bool?> askForFriendPromotions = const Value.absent(),
|
||||
int? mediaSendCounter,
|
||||
int? mediaReceivedCounter,
|
||||
|
|
@ -890,6 +1019,15 @@ class Contact extends DataClass implements Insertable<Contact> {
|
|||
recoverySecretShare: recoverySecretShare.present
|
||||
? recoverySecretShare.value
|
||||
: this.recoverySecretShare,
|
||||
recoveryContactsSecretShare: recoveryContactsSecretShare.present
|
||||
? recoveryContactsSecretShare.value
|
||||
: this.recoveryContactsSecretShare,
|
||||
recoveryContactsLastHeartbeat: recoveryContactsLastHeartbeat.present
|
||||
? recoveryContactsLastHeartbeat.value
|
||||
: this.recoveryContactsLastHeartbeat,
|
||||
recoveryContactsThreshold: recoveryContactsThreshold.present
|
||||
? recoveryContactsThreshold.value
|
||||
: this.recoveryContactsThreshold,
|
||||
askForFriendPromotions: askForFriendPromotions.present
|
||||
? askForFriendPromotions.value
|
||||
: this.askForFriendPromotions,
|
||||
|
|
@ -939,6 +1077,15 @@ class Contact extends DataClass implements Insertable<Contact> {
|
|||
recoverySecretShare: data.recoverySecretShare.present
|
||||
? data.recoverySecretShare.value
|
||||
: this.recoverySecretShare,
|
||||
recoveryContactsSecretShare: data.recoveryContactsSecretShare.present
|
||||
? data.recoveryContactsSecretShare.value
|
||||
: this.recoveryContactsSecretShare,
|
||||
recoveryContactsLastHeartbeat: data.recoveryContactsLastHeartbeat.present
|
||||
? data.recoveryContactsLastHeartbeat.value
|
||||
: this.recoveryContactsLastHeartbeat,
|
||||
recoveryContactsThreshold: data.recoveryContactsThreshold.present
|
||||
? data.recoveryContactsThreshold.value
|
||||
: this.recoveryContactsThreshold,
|
||||
askForFriendPromotions: data.askForFriendPromotions.present
|
||||
? data.askForFriendPromotions.value
|
||||
: this.askForFriendPromotions,
|
||||
|
|
@ -973,6 +1120,11 @@ class Contact extends DataClass implements Insertable<Contact> {
|
|||
..write('recoveryIsTrustedFriend: $recoveryIsTrustedFriend, ')
|
||||
..write('recoveryLastHeartbeat: $recoveryLastHeartbeat, ')
|
||||
..write('recoverySecretShare: $recoverySecretShare, ')
|
||||
..write('recoveryContactsSecretShare: $recoveryContactsSecretShare, ')
|
||||
..write(
|
||||
'recoveryContactsLastHeartbeat: $recoveryContactsLastHeartbeat, ',
|
||||
)
|
||||
..write('recoveryContactsThreshold: $recoveryContactsThreshold, ')
|
||||
..write('askForFriendPromotions: $askForFriendPromotions, ')
|
||||
..write('mediaSendCounter: $mediaSendCounter, ')
|
||||
..write('mediaReceivedCounter: $mediaReceivedCounter')
|
||||
|
|
@ -1001,6 +1153,9 @@ class Contact extends DataClass implements Insertable<Contact> {
|
|||
recoveryIsTrustedFriend,
|
||||
recoveryLastHeartbeat,
|
||||
$driftBlobEquality.hash(recoverySecretShare),
|
||||
$driftBlobEquality.hash(recoveryContactsSecretShare),
|
||||
recoveryContactsLastHeartbeat,
|
||||
recoveryContactsThreshold,
|
||||
askForFriendPromotions,
|
||||
mediaSendCounter,
|
||||
mediaReceivedCounter,
|
||||
|
|
@ -1038,6 +1193,13 @@ class Contact extends DataClass implements Insertable<Contact> {
|
|||
other.recoverySecretShare,
|
||||
this.recoverySecretShare,
|
||||
) &&
|
||||
$driftBlobEquality.equals(
|
||||
other.recoveryContactsSecretShare,
|
||||
this.recoveryContactsSecretShare,
|
||||
) &&
|
||||
other.recoveryContactsLastHeartbeat ==
|
||||
this.recoveryContactsLastHeartbeat &&
|
||||
other.recoveryContactsThreshold == this.recoveryContactsThreshold &&
|
||||
other.askForFriendPromotions == this.askForFriendPromotions &&
|
||||
other.mediaSendCounter == this.mediaSendCounter &&
|
||||
other.mediaReceivedCounter == this.mediaReceivedCounter);
|
||||
|
|
@ -1063,6 +1225,9 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
|
|||
final Value<bool> recoveryIsTrustedFriend;
|
||||
final Value<DateTime?> recoveryLastHeartbeat;
|
||||
final Value<Uint8List?> recoverySecretShare;
|
||||
final Value<Uint8List?> recoveryContactsSecretShare;
|
||||
final Value<DateTime?> recoveryContactsLastHeartbeat;
|
||||
final Value<int?> recoveryContactsThreshold;
|
||||
final Value<bool?> askForFriendPromotions;
|
||||
final Value<int> mediaSendCounter;
|
||||
final Value<int> mediaReceivedCounter;
|
||||
|
|
@ -1086,6 +1251,9 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
|
|||
this.recoveryIsTrustedFriend = const Value.absent(),
|
||||
this.recoveryLastHeartbeat = const Value.absent(),
|
||||
this.recoverySecretShare = const Value.absent(),
|
||||
this.recoveryContactsSecretShare = const Value.absent(),
|
||||
this.recoveryContactsLastHeartbeat = const Value.absent(),
|
||||
this.recoveryContactsThreshold = const Value.absent(),
|
||||
this.askForFriendPromotions = const Value.absent(),
|
||||
this.mediaSendCounter = const Value.absent(),
|
||||
this.mediaReceivedCounter = const Value.absent(),
|
||||
|
|
@ -1110,6 +1278,9 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
|
|||
this.recoveryIsTrustedFriend = const Value.absent(),
|
||||
this.recoveryLastHeartbeat = const Value.absent(),
|
||||
this.recoverySecretShare = const Value.absent(),
|
||||
this.recoveryContactsSecretShare = const Value.absent(),
|
||||
this.recoveryContactsLastHeartbeat = const Value.absent(),
|
||||
this.recoveryContactsThreshold = const Value.absent(),
|
||||
this.askForFriendPromotions = const Value.absent(),
|
||||
this.mediaSendCounter = const Value.absent(),
|
||||
this.mediaReceivedCounter = const Value.absent(),
|
||||
|
|
@ -1134,6 +1305,9 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
|
|||
Expression<bool>? recoveryIsTrustedFriend,
|
||||
Expression<DateTime>? recoveryLastHeartbeat,
|
||||
Expression<Uint8List>? recoverySecretShare,
|
||||
Expression<Uint8List>? recoveryContactsSecretShare,
|
||||
Expression<DateTime>? recoveryContactsLastHeartbeat,
|
||||
Expression<int>? recoveryContactsThreshold,
|
||||
Expression<bool>? askForFriendPromotions,
|
||||
Expression<int>? mediaSendCounter,
|
||||
Expression<int>? mediaReceivedCounter,
|
||||
|
|
@ -1166,6 +1340,12 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
|
|||
'recovery_last_heartbeat': recoveryLastHeartbeat,
|
||||
if (recoverySecretShare != null)
|
||||
'recovery_secret_share': recoverySecretShare,
|
||||
if (recoveryContactsSecretShare != null)
|
||||
'recovery_contacts_secret_share': recoveryContactsSecretShare,
|
||||
if (recoveryContactsLastHeartbeat != null)
|
||||
'recovery_contacts_last_heartbeat': recoveryContactsLastHeartbeat,
|
||||
if (recoveryContactsThreshold != null)
|
||||
'recovery_contacts_threshold': recoveryContactsThreshold,
|
||||
if (askForFriendPromotions != null)
|
||||
'ask_for_friend_promotions': askForFriendPromotions,
|
||||
if (mediaSendCounter != null) 'media_send_counter': mediaSendCounter,
|
||||
|
|
@ -1194,6 +1374,9 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
|
|||
Value<bool>? recoveryIsTrustedFriend,
|
||||
Value<DateTime?>? recoveryLastHeartbeat,
|
||||
Value<Uint8List?>? recoverySecretShare,
|
||||
Value<Uint8List?>? recoveryContactsSecretShare,
|
||||
Value<DateTime?>? recoveryContactsLastHeartbeat,
|
||||
Value<int?>? recoveryContactsThreshold,
|
||||
Value<bool?>? askForFriendPromotions,
|
||||
Value<int>? mediaSendCounter,
|
||||
Value<int>? mediaReceivedCounter,
|
||||
|
|
@ -1222,6 +1405,12 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
|
|||
recoveryLastHeartbeat:
|
||||
recoveryLastHeartbeat ?? this.recoveryLastHeartbeat,
|
||||
recoverySecretShare: recoverySecretShare ?? this.recoverySecretShare,
|
||||
recoveryContactsSecretShare:
|
||||
recoveryContactsSecretShare ?? this.recoveryContactsSecretShare,
|
||||
recoveryContactsLastHeartbeat:
|
||||
recoveryContactsLastHeartbeat ?? this.recoveryContactsLastHeartbeat,
|
||||
recoveryContactsThreshold:
|
||||
recoveryContactsThreshold ?? this.recoveryContactsThreshold,
|
||||
askForFriendPromotions:
|
||||
askForFriendPromotions ?? this.askForFriendPromotions,
|
||||
mediaSendCounter: mediaSendCounter ?? this.mediaSendCounter,
|
||||
|
|
@ -1303,6 +1492,21 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
|
|||
recoverySecretShare.value,
|
||||
);
|
||||
}
|
||||
if (recoveryContactsSecretShare.present) {
|
||||
map['recovery_contacts_secret_share'] = Variable<Uint8List>(
|
||||
recoveryContactsSecretShare.value,
|
||||
);
|
||||
}
|
||||
if (recoveryContactsLastHeartbeat.present) {
|
||||
map['recovery_contacts_last_heartbeat'] = Variable<DateTime>(
|
||||
recoveryContactsLastHeartbeat.value,
|
||||
);
|
||||
}
|
||||
if (recoveryContactsThreshold.present) {
|
||||
map['recovery_contacts_threshold'] = Variable<int>(
|
||||
recoveryContactsThreshold.value,
|
||||
);
|
||||
}
|
||||
if (askForFriendPromotions.present) {
|
||||
map['ask_for_friend_promotions'] = Variable<bool>(
|
||||
askForFriendPromotions.value,
|
||||
|
|
@ -1339,6 +1543,11 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
|
|||
..write('recoveryIsTrustedFriend: $recoveryIsTrustedFriend, ')
|
||||
..write('recoveryLastHeartbeat: $recoveryLastHeartbeat, ')
|
||||
..write('recoverySecretShare: $recoverySecretShare, ')
|
||||
..write('recoveryContactsSecretShare: $recoveryContactsSecretShare, ')
|
||||
..write(
|
||||
'recoveryContactsLastHeartbeat: $recoveryContactsLastHeartbeat, ',
|
||||
)
|
||||
..write('recoveryContactsThreshold: $recoveryContactsThreshold, ')
|
||||
..write('askForFriendPromotions: $askForFriendPromotions, ')
|
||||
..write('mediaSendCounter: $mediaSendCounter, ')
|
||||
..write('mediaReceivedCounter: $mediaReceivedCounter')
|
||||
|
|
@ -13121,6 +13330,9 @@ typedef $$ContactsTableCreateCompanionBuilder =
|
|||
Value<bool> recoveryIsTrustedFriend,
|
||||
Value<DateTime?> recoveryLastHeartbeat,
|
||||
Value<Uint8List?> recoverySecretShare,
|
||||
Value<Uint8List?> recoveryContactsSecretShare,
|
||||
Value<DateTime?> recoveryContactsLastHeartbeat,
|
||||
Value<int?> recoveryContactsThreshold,
|
||||
Value<bool?> askForFriendPromotions,
|
||||
Value<int> mediaSendCounter,
|
||||
Value<int> mediaReceivedCounter,
|
||||
|
|
@ -13146,6 +13358,9 @@ typedef $$ContactsTableUpdateCompanionBuilder =
|
|||
Value<bool> recoveryIsTrustedFriend,
|
||||
Value<DateTime?> recoveryLastHeartbeat,
|
||||
Value<Uint8List?> recoverySecretShare,
|
||||
Value<Uint8List?> recoveryContactsSecretShare,
|
||||
Value<DateTime?> recoveryContactsLastHeartbeat,
|
||||
Value<int?> recoveryContactsThreshold,
|
||||
Value<bool?> askForFriendPromotions,
|
||||
Value<int> mediaSendCounter,
|
||||
Value<int> mediaReceivedCounter,
|
||||
|
|
@ -13525,6 +13740,23 @@ class $$ContactsTableFilterComposer
|
|||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<Uint8List> get recoveryContactsSecretShare =>
|
||||
$composableBuilder(
|
||||
column: $table.recoveryContactsSecretShare,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<DateTime> get recoveryContactsLastHeartbeat =>
|
||||
$composableBuilder(
|
||||
column: $table.recoveryContactsLastHeartbeat,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<int> get recoveryContactsThreshold => $composableBuilder(
|
||||
column: $table.recoveryContactsThreshold,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<bool> get askForFriendPromotions => $composableBuilder(
|
||||
column: $table.askForFriendPromotions,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
|
|
@ -13928,6 +14160,23 @@ class $$ContactsTableOrderingComposer
|
|||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<Uint8List> get recoveryContactsSecretShare =>
|
||||
$composableBuilder(
|
||||
column: $table.recoveryContactsSecretShare,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<DateTime> get recoveryContactsLastHeartbeat =>
|
||||
$composableBuilder(
|
||||
column: $table.recoveryContactsLastHeartbeat,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<int> get recoveryContactsThreshold => $composableBuilder(
|
||||
column: $table.recoveryContactsThreshold,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<bool> get askForFriendPromotions => $composableBuilder(
|
||||
column: $table.askForFriendPromotions,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
|
|
@ -14032,6 +14281,23 @@ class $$ContactsTableAnnotationComposer
|
|||
builder: (column) => column,
|
||||
);
|
||||
|
||||
GeneratedColumn<Uint8List> get recoveryContactsSecretShare =>
|
||||
$composableBuilder(
|
||||
column: $table.recoveryContactsSecretShare,
|
||||
builder: (column) => column,
|
||||
);
|
||||
|
||||
GeneratedColumn<DateTime> get recoveryContactsLastHeartbeat =>
|
||||
$composableBuilder(
|
||||
column: $table.recoveryContactsLastHeartbeat,
|
||||
builder: (column) => column,
|
||||
);
|
||||
|
||||
GeneratedColumn<int> get recoveryContactsThreshold => $composableBuilder(
|
||||
column: $table.recoveryContactsThreshold,
|
||||
builder: (column) => column,
|
||||
);
|
||||
|
||||
GeneratedColumn<bool> get askForFriendPromotions => $composableBuilder(
|
||||
column: $table.askForFriendPromotions,
|
||||
builder: (column) => column,
|
||||
|
|
@ -14395,6 +14661,11 @@ class $$ContactsTableTableManager
|
|||
Value<bool> recoveryIsTrustedFriend = const Value.absent(),
|
||||
Value<DateTime?> recoveryLastHeartbeat = const Value.absent(),
|
||||
Value<Uint8List?> recoverySecretShare = const Value.absent(),
|
||||
Value<Uint8List?> recoveryContactsSecretShare =
|
||||
const Value.absent(),
|
||||
Value<DateTime?> recoveryContactsLastHeartbeat =
|
||||
const Value.absent(),
|
||||
Value<int?> recoveryContactsThreshold = const Value.absent(),
|
||||
Value<bool?> askForFriendPromotions = const Value.absent(),
|
||||
Value<int> mediaSendCounter = const Value.absent(),
|
||||
Value<int> mediaReceivedCounter = const Value.absent(),
|
||||
|
|
@ -14418,6 +14689,9 @@ class $$ContactsTableTableManager
|
|||
recoveryIsTrustedFriend: recoveryIsTrustedFriend,
|
||||
recoveryLastHeartbeat: recoveryLastHeartbeat,
|
||||
recoverySecretShare: recoverySecretShare,
|
||||
recoveryContactsSecretShare: recoveryContactsSecretShare,
|
||||
recoveryContactsLastHeartbeat: recoveryContactsLastHeartbeat,
|
||||
recoveryContactsThreshold: recoveryContactsThreshold,
|
||||
askForFriendPromotions: askForFriendPromotions,
|
||||
mediaSendCounter: mediaSendCounter,
|
||||
mediaReceivedCounter: mediaReceivedCounter,
|
||||
|
|
@ -14443,6 +14717,11 @@ class $$ContactsTableTableManager
|
|||
Value<bool> recoveryIsTrustedFriend = const Value.absent(),
|
||||
Value<DateTime?> recoveryLastHeartbeat = const Value.absent(),
|
||||
Value<Uint8List?> recoverySecretShare = const Value.absent(),
|
||||
Value<Uint8List?> recoveryContactsSecretShare =
|
||||
const Value.absent(),
|
||||
Value<DateTime?> recoveryContactsLastHeartbeat =
|
||||
const Value.absent(),
|
||||
Value<int?> recoveryContactsThreshold = const Value.absent(),
|
||||
Value<bool?> askForFriendPromotions = const Value.absent(),
|
||||
Value<int> mediaSendCounter = const Value.absent(),
|
||||
Value<int> mediaReceivedCounter = const Value.absent(),
|
||||
|
|
@ -14466,6 +14745,9 @@ class $$ContactsTableTableManager
|
|||
recoveryIsTrustedFriend: recoveryIsTrustedFriend,
|
||||
recoveryLastHeartbeat: recoveryLastHeartbeat,
|
||||
recoverySecretShare: recoverySecretShare,
|
||||
recoveryContactsSecretShare: recoveryContactsSecretShare,
|
||||
recoveryContactsLastHeartbeat: recoveryContactsLastHeartbeat,
|
||||
recoveryContactsThreshold: recoveryContactsThreshold,
|
||||
askForFriendPromotions: askForFriendPromotions,
|
||||
mediaSendCounter: mediaSendCounter,
|
||||
mediaReceivedCounter: mediaReceivedCounter,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -149,7 +149,7 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @onboardingNotProductBody.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'twonly is financed by donations and an optional subscription. Your data will never be sold.'**
|
||||
/// **'twonly is financed by an optional subscription. Your data will never be sold.'**
|
||||
String get onboardingNotProductBody;
|
||||
|
||||
/// No description provided for @registerUsernameSlogan.
|
||||
|
|
@ -590,48 +590,6 @@ abstract class AppLocalizations {
|
|||
/// **'{len} contact(s)'**
|
||||
String settingsPrivacyBlockUsersCount(Object len);
|
||||
|
||||
/// No description provided for @settingsPrivacyProfileSelectionTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Security Profile'**
|
||||
String get settingsPrivacyProfileSelectionTitle;
|
||||
|
||||
/// No description provided for @securityProfileTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Security Profile'**
|
||||
String get securityProfileTitle;
|
||||
|
||||
/// No description provided for @securityProfileSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Choose the level of protection that fits your daily use. This can be changed at any time in your settings.'**
|
||||
String get securityProfileSubtitle;
|
||||
|
||||
/// No description provided for @securityProfileNormalTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Normal Protection'**
|
||||
String get securityProfileNormalTitle;
|
||||
|
||||
/// No description provided for @securityProfileNormalDesc.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Good balance between a convenient mode without bothering you too much.'**
|
||||
String get securityProfileNormalDesc;
|
||||
|
||||
/// No description provided for @securityProfileStrictTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Strict Protection'**
|
||||
String get securityProfileStrictTitle;
|
||||
|
||||
/// No description provided for @securityProfileStrictDesc.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Maximum anti-phishing protection but may be inconvenient.'**
|
||||
String get securityProfileStrictDesc;
|
||||
|
||||
/// No description provided for @settingsNotification.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -896,18 +854,48 @@ abstract class AppLocalizations {
|
|||
/// **'Verify contacts'**
|
||||
String get contactVerifyNumberTitle;
|
||||
|
||||
/// No description provided for @verifyUserIdentity.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Verify {username}\'s identity'**
|
||||
String verifyUserIdentity(Object username);
|
||||
|
||||
/// No description provided for @contactVerifyNumberSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Verify the identity of your contacts to make sure you are texting the right person.'**
|
||||
String get contactVerifyNumberSubtitle;
|
||||
|
||||
/// No description provided for @inChatContactNotVerified.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Contact not verified.'**
|
||||
String get inChatContactNotVerified;
|
||||
|
||||
/// No description provided for @groupMembersNotVerified.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{count} members are not verified.'**
|
||||
String groupMembersNotVerified(Object count);
|
||||
|
||||
/// No description provided for @userVerifiedTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Contact verified'**
|
||||
String get userVerifiedTitle;
|
||||
|
||||
/// No description provided for @scanUserQrCode.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Scan {username}\'s QR code'**
|
||||
String scanUserQrCode(Object username);
|
||||
|
||||
/// No description provided for @openOwnQrCode.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Open your own QR code'**
|
||||
String get openOwnQrCode;
|
||||
|
||||
/// No description provided for @contactVerifiedBy.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -1604,6 +1592,18 @@ abstract class AppLocalizations {
|
|||
/// **'Change password'**
|
||||
String get backupChangePassword;
|
||||
|
||||
/// No description provided for @backupChangePasswordAuthReason.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Changing backup password'**
|
||||
String get backupChangePasswordAuthReason;
|
||||
|
||||
/// No description provided for @backupChangePasswordAuthFailed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'You can only change your password after you have authenticated!'**
|
||||
String get backupChangePasswordAuthFailed;
|
||||
|
||||
/// No description provided for @twonlySafeRecoverTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -2372,6 +2372,12 @@ abstract class AppLocalizations {
|
|||
/// **'Let a friend scan this QR code to add you'**
|
||||
String get addContactQrSheetSubtext;
|
||||
|
||||
/// No description provided for @letUserScanQrCode.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Let {username} scan this QR code'**
|
||||
String letUserScanQrCode(Object username);
|
||||
|
||||
/// No description provided for @finishSetupCardTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -2729,7 +2735,7 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @verificationBadgeGeneralDesc.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The badge *protects you from scammers and attackers*. It will be displayed next to a contact that has been *manually verified* by you or a friend.'**
|
||||
/// **'The badge gives you the peace of mind that you are messaging the *right person* and your *messages remain confidential*.'**
|
||||
String get verificationBadgeGeneralDesc;
|
||||
|
||||
/// No description provided for @verificationBadgeGreenDesc.
|
||||
|
|
@ -2756,12 +2762,30 @@ abstract class AppLocalizations {
|
|||
/// **'Scan now'**
|
||||
String get scanNow;
|
||||
|
||||
/// No description provided for @qrScannerVerifyHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'To verify, the other person must open their QR code (Chat Lists > QR code button at the bottom right)'**
|
||||
String get qrScannerVerifyHint;
|
||||
|
||||
/// No description provided for @qrScannerVerifyUserHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'To verify, {username} must open their QR code (Chat Lists > QR code button at the bottom right)'**
|
||||
String qrScannerVerifyUserHint(Object username);
|
||||
|
||||
/// No description provided for @openQrCode.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Open QR code'**
|
||||
String get openQrCode;
|
||||
|
||||
/// No description provided for @letFriendScanQrToVerify.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Let a friend scan this QR code to verify you'**
|
||||
String get letFriendScanQrToVerify;
|
||||
|
||||
/// No description provided for @deleteVerificationTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -3659,6 +3683,456 @@ abstract class AppLocalizations {
|
|||
/// In en, this message translates to:
|
||||
/// **'Reset'**
|
||||
String get avatarCustomizeReset;
|
||||
|
||||
/// No description provided for @passwordlessRecovery.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Passwordless Recovery'**
|
||||
String get passwordlessRecovery;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryNotConfigured.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Not configured'**
|
||||
String get passwordlessRecoveryNotConfigured;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryTestPin.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Test PIN'**
|
||||
String get passwordlessRecoveryTestPin;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryTestPinTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Test PIN'**
|
||||
String get passwordlessRecoveryTestPinTitle;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryTestPinHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Enter your PIN'**
|
||||
String get passwordlessRecoveryTestPinHint;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryTestPinCorrect.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'PIN is correct!'**
|
||||
String get passwordlessRecoveryTestPinCorrect;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryTestPinIncorrect.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Incorrect PIN.'**
|
||||
String get passwordlessRecoveryTestPinIncorrect;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryTest.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Test'**
|
||||
String get passwordlessRecoveryTest;
|
||||
|
||||
/// No description provided for @passwordlessRecoverySecondFactorNone.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'None'**
|
||||
String get passwordlessRecoverySecondFactorNone;
|
||||
|
||||
/// No description provided for @passwordlessRecoverySecondFactorEmailLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Email ({email})'**
|
||||
String passwordlessRecoverySecondFactorEmailLabel(Object email);
|
||||
|
||||
/// No description provided for @passwordlessRecoverySecondFactorPin.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'PIN'**
|
||||
String get passwordlessRecoverySecondFactorPin;
|
||||
|
||||
/// No description provided for @passwordlessRecoverySecondFactorEmail.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Email'**
|
||||
String get passwordlessRecoverySecondFactorEmail;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryModify.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Modify Recovery Settings'**
|
||||
String get passwordlessRecoveryModify;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryModifyDesc.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Update your recovery configuration'**
|
||||
String get passwordlessRecoveryModifyDesc;
|
||||
|
||||
/// No description provided for @passwordlessRecoverySecondFactor.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Second Factor'**
|
||||
String get passwordlessRecoverySecondFactor;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryNoFriendsFound.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No trusted friends found.'**
|
||||
String get passwordlessRecoveryNoFriendsFound;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryActiveFriends.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Active Friends'**
|
||||
String get passwordlessRecoveryActiveFriends;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryActiveFriendsDesc.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'These trusted friends are actively using twonly and can probably help you recover your account.'**
|
||||
String get passwordlessRecoveryActiveFriendsDesc;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryInactiveFriends.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Inactive Friends'**
|
||||
String get passwordlessRecoveryInactiveFriends;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryInactiveFriendsDesc.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'These friends either have not yet received their share or do not use twonly actively anymore, which could mean that they maybe cannot help.'**
|
||||
String get passwordlessRecoveryInactiveFriendsDesc;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryNotEnoughFriends.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Not enough friends selected. You need at least 3.'**
|
||||
String get passwordlessRecoveryNotEnoughFriends;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryLoading.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Loading...'**
|
||||
String get passwordlessRecoveryLoading;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryEnableSuccess.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Passwordless recovery successfully enabled!'**
|
||||
String get passwordlessRecoveryEnableSuccess;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryEnableFailed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Failed to enable passwordless recovery.'**
|
||||
String get passwordlessRecoveryEnableFailed;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryNeedAtLeast3.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'You need at least 3 trusted friends.'**
|
||||
String get passwordlessRecoveryNeedAtLeast3;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryInvalidPin.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Invalid PIN'**
|
||||
String get passwordlessRecoveryInvalidPin;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryEnterPin.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Please enter a PIN.'**
|
||||
String get passwordlessRecoveryEnterPin;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryPinMinLength.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'PIN must be at least 4 digits.'**
|
||||
String get passwordlessRecoveryPinMinLength;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryEnterEmail.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Please enter an email address.'**
|
||||
String get passwordlessRecoveryEnterEmail;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryEnableBtn.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Enable Passwordless Recovery'**
|
||||
String get passwordlessRecoveryEnableBtn;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryRecoverBtn.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Recover passwordless'**
|
||||
String get passwordlessRecoveryRecoverBtn;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryModifyBtn.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Modify Passwordless Recovery'**
|
||||
String get passwordlessRecoveryModifyBtn;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryStatusEnabled.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Enabled • {count} trusted friends'**
|
||||
String passwordlessRecoveryStatusEnabled(num count);
|
||||
|
||||
/// No description provided for @passwordlessRecoveryInfoHowItWorks.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'How it works'**
|
||||
String get passwordlessRecoveryInfoHowItWorks;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryInfoHowItWorksDesc.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Because twonly operates without central user accounts or phone numbers to maximize privacy, we rely on a decentralized recovery mechanism. Using Shamir\'s Secret Sharing, your cryptographic identity is split into independent shares and distributed among your trusted friends. To restore access, a predefined threshold of these friends must combine their shares.'**
|
||||
String get passwordlessRecoveryInfoHowItWorksDesc;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryInfoWhySecondFactor.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Why a Second Factor?'**
|
||||
String get passwordlessRecoveryInfoWhySecondFactor;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryInfoWhySecondFactorDesc.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The second factor (Email or PIN) serves as a vital cryptographic safeguard against malicious collusion. If your trusted friends were to coordinate their shares behind your back, they still wouldn\'t be able to decrypt your identity without the second factor key.'**
|
||||
String get passwordlessRecoveryInfoWhySecondFactorDesc;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryInfoGotIt.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Got it'**
|
||||
String get passwordlessRecoveryInfoGotIt;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryMethod.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Second factor method'**
|
||||
String get passwordlessRecoveryMethod;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryMethodNoneDesc.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Without second-factor, your friends could collaborate to recover your account. Therefore, it is recommended to configure a second-factor.'**
|
||||
String get passwordlessRecoveryMethodNoneDesc;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryMethodPinHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Enter PIN'**
|
||||
String get passwordlessRecoveryMethodPinHint;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryMethodEmailHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Enter recovery email address'**
|
||||
String get passwordlessRecoveryMethodEmailHint;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryMethodEmailDesc.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Your email address is *never stored on the server* and is only sent to it in the event of a recovery.'**
|
||||
String get passwordlessRecoveryMethodEmailDesc;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryThresholdDesc.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'To recover your account you need {count} of your selected trusted friends.'**
|
||||
String passwordlessRecoveryThresholdDesc(num count);
|
||||
|
||||
/// No description provided for @passwordlessRecoveryThresholdTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Required trusted friends for recovery'**
|
||||
String get passwordlessRecoveryThresholdTitle;
|
||||
|
||||
/// No description provided for @passwordlessRecoverySelectFriendsNeeded.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Select friends ({count} more needed)'**
|
||||
String passwordlessRecoverySelectFriendsNeeded(num count);
|
||||
|
||||
/// No description provided for @passwordlessRecoverySelectFriends.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Select trusted friends'**
|
||||
String get passwordlessRecoverySelectFriends;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryNoFriendsSelected.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No trusted friends selected yet'**
|
||||
String get passwordlessRecoveryNoFriendsSelected;
|
||||
|
||||
/// No description provided for @passwordlessRecoverySubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Recover your identity without a password.'**
|
||||
String get passwordlessRecoverySubtitle;
|
||||
|
||||
/// No description provided for @recoverPasswordlessExplanation.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'If enabled, you can recover your account by asking your selected friends to help you.'**
|
||||
String get recoverPasswordlessExplanation;
|
||||
|
||||
/// No description provided for @recoverPasswordlessNotificationCardTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Enable notifications'**
|
||||
String get recoverPasswordlessNotificationCardTitle;
|
||||
|
||||
/// No description provided for @recoverPasswordlessNotificationCardSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Get notified when a friend has helped you recover your account.'**
|
||||
String get recoverPasswordlessNotificationCardSubtitle;
|
||||
|
||||
/// No description provided for @recoverPasswordlessNotificationCardBtn.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Enable push notifications'**
|
||||
String get recoverPasswordlessNotificationCardBtn;
|
||||
|
||||
/// No description provided for @recoverPasswordlessQrInstructions.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Let friends scan the QR code or share the link.'**
|
||||
String get recoverPasswordlessQrInstructions;
|
||||
|
||||
/// No description provided for @recoverPasswordlessShareBtn.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Share Recovery Link'**
|
||||
String get recoverPasswordlessShareBtn;
|
||||
|
||||
/// No description provided for @recoverPasswordlessCopyBtn.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Copy'**
|
||||
String get recoverPasswordlessCopyBtn;
|
||||
|
||||
/// No description provided for @recoverPasswordlessCopiedSnackbar.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Link copied to clipboard!'**
|
||||
String get recoverPasswordlessCopiedSnackbar;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryNoShareStored.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No recovery share stored for this contact.'**
|
||||
String get passwordlessRecoveryNoShareStored;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryShareSent.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Recovery share successfully sent!'**
|
||||
String get passwordlessRecoveryShareSent;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryNetworkError.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Network error, please ensure you have internet'**
|
||||
String get passwordlessRecoveryNetworkError;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryInvalidEmail.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The email address is invalid.'**
|
||||
String get passwordlessRecoveryInvalidEmail;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryResendEmail.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Resend recovery email'**
|
||||
String get passwordlessRecoveryResendEmail;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryHelpAFriend.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Help a Friend'**
|
||||
String get passwordlessRecoveryHelpAFriend;
|
||||
|
||||
/// No description provided for @passwordlessRecoverySelectContactDesc.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Please select the contact who requested recovery below to send them their recovery key.'**
|
||||
String get passwordlessRecoverySelectContactDesc;
|
||||
|
||||
/// No description provided for @passwordlessRecoverySearchContacts.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Search contacts...'**
|
||||
String get passwordlessRecoverySearchContacts;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryNoContactsFound.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No contacts found'**
|
||||
String get passwordlessRecoveryNoContactsFound;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryCantHelpHim.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'You can\'t help him'**
|
||||
String get passwordlessRecoveryCantHelpHim;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryDoesAskedYou.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Does {username} has asked you?'**
|
||||
String passwordlessRecoveryDoesAskedYou(Object username);
|
||||
|
||||
/// No description provided for @passwordlessRecoveryVerifySourceDesc.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Please ensure that you actualy received the link/qr code from your friend! If you are unsure, please verify again and then click again on the link or come back.'**
|
||||
String get passwordlessRecoveryVerifySourceDesc;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryNo.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No'**
|
||||
String get passwordlessRecoveryNo;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryYes.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Yes'**
|
||||
String get passwordlessRecoveryYes;
|
||||
|
||||
/// No description provided for @passwordlessRecoveryYesWithTimer.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Yes ({seconds}s)'**
|
||||
String passwordlessRecoveryYesWithTimer(Object seconds);
|
||||
|
||||
/// No description provided for @recoverPasswordlessBeingRecoveredLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Account being recovered'**
|
||||
String get recoverPasswordlessBeingRecoveredLabel;
|
||||
|
||||
/// No description provided for @recoverPasswordlessSharesReceived.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{received} of {threshold} friends have shared'**
|
||||
String recoverPasswordlessSharesReceived(Object received, Object threshold);
|
||||
|
||||
/// No description provided for @recoverPasswordlessRecoverNowBtn.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Recover now'**
|
||||
String get recoverPasswordlessRecoverNowBtn;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get onboardingNotProductBody =>
|
||||
'twonly wird durch Spenden und ein optionales Abonnement finanziert. Deine Daten werden niemals verkauft.';
|
||||
'twonly wird durch ein optionales Abonnement finanziert. Deine Daten werden niemals verkauft.';
|
||||
|
||||
@override
|
||||
String get registerUsernameSlogan => 'Konto erstellen';
|
||||
|
|
@ -268,30 +268,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
return '$len Kontakt(e)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settingsPrivacyProfileSelectionTitle => 'Sicherheitsprofil';
|
||||
|
||||
@override
|
||||
String get securityProfileTitle => 'Sicherheitsprofil';
|
||||
|
||||
@override
|
||||
String get securityProfileSubtitle =>
|
||||
'Wähle das Schutzniveau, das zu deiner täglichen Nutzung passt. Dies kann jederzeit in den Einstellungen geändert werden.';
|
||||
|
||||
@override
|
||||
String get securityProfileNormalTitle => 'Normaler Schutz';
|
||||
|
||||
@override
|
||||
String get securityProfileNormalDesc =>
|
||||
'Gute Balance zwischen Komfort und Sicherheit, ohne dich zu sehr einzuschränken.';
|
||||
|
||||
@override
|
||||
String get securityProfileStrictTitle => 'Strikter Schutz';
|
||||
|
||||
@override
|
||||
String get securityProfileStrictDesc =>
|
||||
'Maximaler Schutz vor Phishing, kann aber unkomfortabel sein.';
|
||||
|
||||
@override
|
||||
String get settingsNotification => 'Benachrichtigung';
|
||||
|
||||
|
|
@ -404,7 +380,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get contactUsLastWarning =>
|
||||
'Dies sind die Informationen, die an uns gesendet werden. Bitte prüfen Sie sie und klicke dann auf „Abschicken“.';
|
||||
'Dies sind die Informationen, die an uns gesendet werden. Bitte prüfe sie und klicke dann auf „Abschicken“.';
|
||||
|
||||
@override
|
||||
String get contactUsSuccess => 'Feedback erfolgreich übermittelt!';
|
||||
|
|
@ -435,13 +411,34 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get contactVerifyNumberTitle => 'Kontakte verifizieren';
|
||||
|
||||
@override
|
||||
String verifyUserIdentity(Object username) {
|
||||
return 'Identität von $username verifizieren';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contactVerifyNumberSubtitle =>
|
||||
'Überprüfe die Identität deiner Kontakte, um sicherzugehen, dass du mit der richtigen Person schreibst.';
|
||||
|
||||
@override
|
||||
String get inChatContactNotVerified => 'Kontakt nicht verifiziert.';
|
||||
|
||||
@override
|
||||
String groupMembersNotVerified(Object count) {
|
||||
return '$count Mitglieder sind nicht verifiziert.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get userVerifiedTitle => 'Kontakt verifiziert';
|
||||
|
||||
@override
|
||||
String scanUserQrCode(Object username) {
|
||||
return 'QR-Code von $username scannen';
|
||||
}
|
||||
|
||||
@override
|
||||
String get openOwnQrCode => 'Eigenen QR-Code öffnen';
|
||||
|
||||
@override
|
||||
String contactVerifiedBy(Object username) {
|
||||
return 'Verifiziert von $username';
|
||||
|
|
@ -841,7 +838,14 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String get backupTwonlySaveNow => 'Jetzt speichern';
|
||||
|
||||
@override
|
||||
String get backupChangePassword => 'Password ändern';
|
||||
String get backupChangePassword => 'Passwort ändern';
|
||||
|
||||
@override
|
||||
String get backupChangePasswordAuthReason => 'Backup-Passwort ändern';
|
||||
|
||||
@override
|
||||
String get backupChangePasswordAuthFailed =>
|
||||
'Du kannst dein Passwort nur ändern, wenn du dich authentifiziert hast!';
|
||||
|
||||
@override
|
||||
String get twonlySafeRecoverTitle => 'Backup wiederherstellen';
|
||||
|
|
@ -1310,6 +1314,11 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String get addContactQrSheetSubtext =>
|
||||
'Lass einen Freund diesen QR-Code scannen, um dich hinzuzufügen';
|
||||
|
||||
@override
|
||||
String letUserScanQrCode(Object username) {
|
||||
return 'Lass $username diesen QR-Code scannen';
|
||||
}
|
||||
|
||||
@override
|
||||
String get finishSetupCardTitle => 'Profil vervollständigen';
|
||||
|
||||
|
|
@ -1535,7 +1544,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get verificationBadgeGeneralDesc =>
|
||||
'Der Haken *schützt dich vor Betrügern und Angreifern*. Es wird neben einem Kontakt angezeigt, der von dir oder einem Freund *manuell überprüft* wurde.';
|
||||
'Der Haken gibt dir die Sicherheit, dass du mit der *richtigen Person* schreibst und deine *Nachrichten vertraulich* bleiben.';
|
||||
|
||||
@override
|
||||
String get verificationBadgeGreenDesc =>
|
||||
|
|
@ -1552,9 +1561,22 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get scanNow => 'Jetzt scannen';
|
||||
|
||||
@override
|
||||
String get qrScannerVerifyHint =>
|
||||
'Zum Verifizieren muss die andere Person ihren QR-Code öffnen (Chat-Liste > QR-Code-Button unten rechts)';
|
||||
|
||||
@override
|
||||
String qrScannerVerifyUserHint(Object username) {
|
||||
return 'Zum Verifizieren muss $username seinen QR-Code öffnen (Chat-Liste > QR-Code-Button unten rechts)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get openQrCode => 'QR-Code öffnen';
|
||||
|
||||
@override
|
||||
String get letFriendScanQrToVerify =>
|
||||
'Lass einen Freund diesen QR-Code scannen, um euch zu verifizieren';
|
||||
|
||||
@override
|
||||
String get deleteVerificationTitle => 'Verifizierung löschen?';
|
||||
|
||||
|
|
@ -2102,4 +2124,274 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get avatarCustomizeReset => 'Zurücksetzen';
|
||||
|
||||
@override
|
||||
String get passwordlessRecovery => 'Passwortloses Backup';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryNotConfigured => 'Nicht konfiguriert';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryTestPin => 'PIN testen';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryTestPinTitle => 'PIN testen';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryTestPinHint => 'Gib deine PIN ein';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryTestPinCorrect => 'PIN ist korrekt!';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryTestPinIncorrect => 'Falsche PIN.';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryTest => 'Testen';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoverySecondFactorNone => 'Keiner';
|
||||
|
||||
@override
|
||||
String passwordlessRecoverySecondFactorEmailLabel(Object email) {
|
||||
return 'E-Mail ($email)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get passwordlessRecoverySecondFactorPin => 'PIN';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoverySecondFactorEmail => 'E-Mail';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryModify => 'Einstellungen bearbeiten';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryModifyDesc =>
|
||||
'Aktualisiere deine Backupkonfiguration';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoverySecondFactor => 'Zweiter Faktor';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryNoFriendsFound => 'Keine Freunde gefunden.';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryActiveFriends => 'Aktive Freunde';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryActiveFriendsDesc =>
|
||||
'Diese Freunde nutzen twonly aktiv und können dir wahrscheinlich helfen, dein Konto wiederherzustellen.';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryInactiveFriends => 'Inaktive Freunde';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryInactiveFriendsDesc =>
|
||||
'Diese Freunde haben entweder ihren Teil noch nicht erhalten oder nutzen twonly nicht mehr aktiv, was bedeutet, dass sie möglicherweise nicht helfen können.';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryNotEnoughFriends =>
|
||||
'Nicht genügend Freunde ausgewählt. Du brauchst mindestens 3.';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryLoading => 'Wird geladen...';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryEnableSuccess =>
|
||||
'Passwortloses Backup erfolgreich aktiviert!';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryEnableFailed =>
|
||||
'Fehler beim Aktivieren des passwortlosen Backups.';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryNeedAtLeast3 =>
|
||||
'Du brauchst mindestens 3 Freunde.';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryInvalidPin => 'Ungültige PIN';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryEnterPin => 'Bitte gib eine PIN ein.';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryPinMinLength =>
|
||||
'Die PIN muss mindestens 4 Ziffern lang sein.';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryEnterEmail =>
|
||||
'Bitte gib eine E-Mail-Adresse ein.';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryEnableBtn => 'Passwortloses Backup aktivieren';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryRecoverBtn => 'Passwortlos wiederherstellen';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryModifyBtn => 'Passwortloses Backup bearbeiten';
|
||||
|
||||
@override
|
||||
String passwordlessRecoveryStatusEnabled(num count) {
|
||||
return 'Aktiviert • $count Freunde';
|
||||
}
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryInfoHowItWorks => 'Wie es funktioniert';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryInfoHowItWorksDesc =>
|
||||
'Da twonly ohne zentrale Benutzerkonten oder Telefonnummern funktioniert, um deine Privatsphäre zu maximieren, setzen wir auf einen dezentralen Wiederherstellungsmechanismus. Mit Shamir\'s Secret Sharing wird deine kryptografische Identität in unabhängige Teile aufgeteilt und an deine Freunde verteilt. Um den Zugriff wiederherzustellen, muss ein vorher festgelegter Schwellenwert dieser Freunde ihre Teile kombinieren.';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryInfoWhySecondFactor =>
|
||||
'Warum ein zweiter Faktor?';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryInfoWhySecondFactorDesc =>
|
||||
'Der zweite Faktor (E-Mail oder PIN) dient als wichtiger kryptografischer Schutz vor bösartiger Absprache. Wenn sich deine Freunde hinter deinem Rücken absprechen würden, könnten sie deine Identität ohne den Schlüssel für den zweiten Faktor trotzdem nicht entschlüsseln.';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryInfoGotIt => 'Verstanden';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryMethod => 'Methode für den zweiten Faktor';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryMethodNoneDesc =>
|
||||
'Ohne zweiten Faktor könnten sich deine Freunde absprechen, um dein Konto wiederherzustellen. Daher wird empfohlen, einen zweiten Faktor zu konfigurieren.';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryMethodPinHint => 'PIN eingeben';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryMethodEmailHint =>
|
||||
'Wiederherstellungs-E-Mail-Adresse eingeben';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryMethodEmailDesc =>
|
||||
'Deine E-Mail-Adresse wird *niemals auf dem Server gespeichert* und nur im Falle einer Wiederherstellung an ihn gesendet.';
|
||||
|
||||
@override
|
||||
String passwordlessRecoveryThresholdDesc(num count) {
|
||||
return 'Um dein Konto wiederherzustellen, brauchst du $count deiner ausgewählten Freunde.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryThresholdTitle =>
|
||||
'Benötigte Freunde für das Backup';
|
||||
|
||||
@override
|
||||
String passwordlessRecoverySelectFriendsNeeded(num count) {
|
||||
return 'Freunde auswählen ($count weitere benötigt)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get passwordlessRecoverySelectFriends => 'Freunde auswählen';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryNoFriendsSelected =>
|
||||
'Noch keine Freunde ausgewählt';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoverySubtitle =>
|
||||
'Stelle deine Identität ohne Passwort wieder her.';
|
||||
|
||||
@override
|
||||
String get recoverPasswordlessExplanation =>
|
||||
'Wenn du die passwortlose Wiederherstellung aktiviert hast, kannst du dein Konto mithilfe deiner Freunde wiederherstellen.';
|
||||
|
||||
@override
|
||||
String get recoverPasswordlessNotificationCardTitle =>
|
||||
'Benachrichtigungen aktivieren';
|
||||
|
||||
@override
|
||||
String get recoverPasswordlessNotificationCardSubtitle =>
|
||||
'Lass dich benachrichtigen, wenn ein Freund dir geholfen hat, dein Konto wiederherzustellen.';
|
||||
|
||||
@override
|
||||
String get recoverPasswordlessNotificationCardBtn =>
|
||||
'Push-Benachrichtigungen aktivieren';
|
||||
|
||||
@override
|
||||
String get recoverPasswordlessQrInstructions =>
|
||||
'Lass deine Freunde den QR-Code scannen oder teile den Link.';
|
||||
|
||||
@override
|
||||
String get recoverPasswordlessShareBtn => 'Wiederherstellungs-Link teilen';
|
||||
|
||||
@override
|
||||
String get recoverPasswordlessCopyBtn => 'Kopieren';
|
||||
|
||||
@override
|
||||
String get recoverPasswordlessCopiedSnackbar =>
|
||||
'Link in die Zwischenablage kopiert!';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryNoShareStored =>
|
||||
'Kein Wiederherstellungs-Teil für diesen Kontakt gespeichert.';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryShareSent =>
|
||||
'Wiederherstellungs-Teil erfolgreich gesendet!';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryNetworkError =>
|
||||
'Netzwerkfehler, bitte stelle sicher, dass du Internet hast';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryInvalidEmail =>
|
||||
'Die E-Mail-Adresse ist ungültig.';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryResendEmail => 'E-Mail erneut senden';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryHelpAFriend => 'Einem Freund helfen';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoverySelectContactDesc =>
|
||||
'Wähle den Kontakt aus, dem du helfen möchtest.';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoverySearchContacts => 'Kontakte suchen...';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryNoContactsFound => 'Keine Kontakte gefunden';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryCantHelpHim => 'Du kannst ihm nicht helfen';
|
||||
|
||||
@override
|
||||
String passwordlessRecoveryDoesAskedYou(Object username) {
|
||||
return 'Hat $username dich gefragt?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryVerifySourceDesc =>
|
||||
'Bitte stelle sicher, dass du den Link/QR-Code tatsächlich von deinem Freund erhalten hast! Wenn du dir unsicher bist, verifiziere dies bitte erneut und klicke dann noch einmal auf den Link oder kehre zurück.';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryNo => 'Nein';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryYes => 'Ja';
|
||||
|
||||
@override
|
||||
String passwordlessRecoveryYesWithTimer(Object seconds) {
|
||||
return 'Ja (${seconds}s)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recoverPasswordlessBeingRecoveredLabel =>
|
||||
'Konto wird wiederhergestellt';
|
||||
|
||||
@override
|
||||
String recoverPasswordlessSharesReceived(Object received, Object threshold) {
|
||||
return '$received von $threshold Freunden haben geteilt';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recoverPasswordlessRecoverNowBtn => 'Jetzt wiederherstellen';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get onboardingNotProductBody =>
|
||||
'twonly is financed by donations and an optional subscription. Your data will never be sold.';
|
||||
'twonly is financed by an optional subscription. Your data will never be sold.';
|
||||
|
||||
@override
|
||||
String get registerUsernameSlogan => 'Create your account';
|
||||
|
|
@ -265,30 +265,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
return '$len contact(s)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settingsPrivacyProfileSelectionTitle => 'Security Profile';
|
||||
|
||||
@override
|
||||
String get securityProfileTitle => 'Security Profile';
|
||||
|
||||
@override
|
||||
String get securityProfileSubtitle =>
|
||||
'Choose the level of protection that fits your daily use. This can be changed at any time in your settings.';
|
||||
|
||||
@override
|
||||
String get securityProfileNormalTitle => 'Normal Protection';
|
||||
|
||||
@override
|
||||
String get securityProfileNormalDesc =>
|
||||
'Good balance between a convenient mode without bothering you too much.';
|
||||
|
||||
@override
|
||||
String get securityProfileStrictTitle => 'Strict Protection';
|
||||
|
||||
@override
|
||||
String get securityProfileStrictDesc =>
|
||||
'Maximum anti-phishing protection but may be inconvenient.';
|
||||
|
||||
@override
|
||||
String get settingsNotification => 'Notification';
|
||||
|
||||
|
|
@ -431,13 +407,34 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get contactVerifyNumberTitle => 'Verify contacts';
|
||||
|
||||
@override
|
||||
String verifyUserIdentity(Object username) {
|
||||
return 'Verify $username\'s identity';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contactVerifyNumberSubtitle =>
|
||||
'Verify the identity of your contacts to make sure you are texting the right person.';
|
||||
|
||||
@override
|
||||
String get inChatContactNotVerified => 'Contact not verified.';
|
||||
|
||||
@override
|
||||
String groupMembersNotVerified(Object count) {
|
||||
return '$count members are not verified.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get userVerifiedTitle => 'Contact verified';
|
||||
|
||||
@override
|
||||
String scanUserQrCode(Object username) {
|
||||
return 'Scan $username\'s QR code';
|
||||
}
|
||||
|
||||
@override
|
||||
String get openOwnQrCode => 'Open your own QR code';
|
||||
|
||||
@override
|
||||
String contactVerifiedBy(Object username) {
|
||||
return 'Verified by $username';
|
||||
|
|
@ -838,6 +835,13 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get backupChangePassword => 'Change password';
|
||||
|
||||
@override
|
||||
String get backupChangePasswordAuthReason => 'Changing backup password';
|
||||
|
||||
@override
|
||||
String get backupChangePasswordAuthFailed =>
|
||||
'You can only change your password after you have authenticated!';
|
||||
|
||||
@override
|
||||
String get twonlySafeRecoverTitle => 'Restore backup';
|
||||
|
||||
|
|
@ -1302,6 +1306,11 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get addContactQrSheetSubtext =>
|
||||
'Let a friend scan this QR code to add you';
|
||||
|
||||
@override
|
||||
String letUserScanQrCode(Object username) {
|
||||
return 'Let $username scan this QR code';
|
||||
}
|
||||
|
||||
@override
|
||||
String get finishSetupCardTitle => 'Complete your profile';
|
||||
|
||||
|
|
@ -1522,7 +1531,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get verificationBadgeGeneralDesc =>
|
||||
'The badge *protects you from scammers and attackers*. It will be displayed next to a contact that has been *manually verified* by you or a friend.';
|
||||
'The badge gives you the peace of mind that you are messaging the *right person* and your *messages remain confidential*.';
|
||||
|
||||
@override
|
||||
String get verificationBadgeGreenDesc =>
|
||||
|
|
@ -1539,9 +1548,22 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get scanNow => 'Scan now';
|
||||
|
||||
@override
|
||||
String get qrScannerVerifyHint =>
|
||||
'To verify, the other person must open their QR code (Chat Lists > QR code button at the bottom right)';
|
||||
|
||||
@override
|
||||
String qrScannerVerifyUserHint(Object username) {
|
||||
return 'To verify, $username must open their QR code (Chat Lists > QR code button at the bottom right)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get openQrCode => 'Open QR code';
|
||||
|
||||
@override
|
||||
String get letFriendScanQrToVerify =>
|
||||
'Let a friend scan this QR code to verify you';
|
||||
|
||||
@override
|
||||
String get deleteVerificationTitle => 'Delete verification?';
|
||||
|
||||
|
|
@ -2087,4 +2109,270 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get avatarCustomizeReset => 'Reset';
|
||||
|
||||
@override
|
||||
String get passwordlessRecovery => 'Passwordless Recovery';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryNotConfigured => 'Not configured';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryTestPin => 'Test PIN';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryTestPinTitle => 'Test PIN';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryTestPinHint => 'Enter your PIN';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryTestPinCorrect => 'PIN is correct!';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryTestPinIncorrect => 'Incorrect PIN.';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryTest => 'Test';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoverySecondFactorNone => 'None';
|
||||
|
||||
@override
|
||||
String passwordlessRecoverySecondFactorEmailLabel(Object email) {
|
||||
return 'Email ($email)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get passwordlessRecoverySecondFactorPin => 'PIN';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoverySecondFactorEmail => 'Email';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryModify => 'Modify Recovery Settings';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryModifyDesc =>
|
||||
'Update your recovery configuration';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoverySecondFactor => 'Second Factor';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryNoFriendsFound => 'No trusted friends found.';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryActiveFriends => 'Active Friends';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryActiveFriendsDesc =>
|
||||
'These trusted friends are actively using twonly and can probably help you recover your account.';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryInactiveFriends => 'Inactive Friends';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryInactiveFriendsDesc =>
|
||||
'These friends either have not yet received their share or do not use twonly actively anymore, which could mean that they maybe cannot help.';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryNotEnoughFriends =>
|
||||
'Not enough friends selected. You need at least 3.';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryLoading => 'Loading...';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryEnableSuccess =>
|
||||
'Passwordless recovery successfully enabled!';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryEnableFailed =>
|
||||
'Failed to enable passwordless recovery.';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryNeedAtLeast3 =>
|
||||
'You need at least 3 trusted friends.';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryInvalidPin => 'Invalid PIN';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryEnterPin => 'Please enter a PIN.';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryPinMinLength =>
|
||||
'PIN must be at least 4 digits.';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryEnterEmail => 'Please enter an email address.';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryEnableBtn => 'Enable Passwordless Recovery';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryRecoverBtn => 'Recover passwordless';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryModifyBtn => 'Modify Passwordless Recovery';
|
||||
|
||||
@override
|
||||
String passwordlessRecoveryStatusEnabled(num count) {
|
||||
return 'Enabled • $count trusted friends';
|
||||
}
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryInfoHowItWorks => 'How it works';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryInfoHowItWorksDesc =>
|
||||
'Because twonly operates without central user accounts or phone numbers to maximize privacy, we rely on a decentralized recovery mechanism. Using Shamir\'s Secret Sharing, your cryptographic identity is split into independent shares and distributed among your trusted friends. To restore access, a predefined threshold of these friends must combine their shares.';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryInfoWhySecondFactor => 'Why a Second Factor?';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryInfoWhySecondFactorDesc =>
|
||||
'The second factor (Email or PIN) serves as a vital cryptographic safeguard against malicious collusion. If your trusted friends were to coordinate their shares behind your back, they still wouldn\'t be able to decrypt your identity without the second factor key.';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryInfoGotIt => 'Got it';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryMethod => 'Second factor method';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryMethodNoneDesc =>
|
||||
'Without second-factor, your friends could collaborate to recover your account. Therefore, it is recommended to configure a second-factor.';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryMethodPinHint => 'Enter PIN';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryMethodEmailHint =>
|
||||
'Enter recovery email address';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryMethodEmailDesc =>
|
||||
'Your email address is *never stored on the server* and is only sent to it in the event of a recovery.';
|
||||
|
||||
@override
|
||||
String passwordlessRecoveryThresholdDesc(num count) {
|
||||
return 'To recover your account you need $count of your selected trusted friends.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryThresholdTitle =>
|
||||
'Required trusted friends for recovery';
|
||||
|
||||
@override
|
||||
String passwordlessRecoverySelectFriendsNeeded(num count) {
|
||||
return 'Select friends ($count more needed)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get passwordlessRecoverySelectFriends => 'Select trusted friends';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryNoFriendsSelected =>
|
||||
'No trusted friends selected yet';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoverySubtitle =>
|
||||
'Recover your identity without a password.';
|
||||
|
||||
@override
|
||||
String get recoverPasswordlessExplanation =>
|
||||
'If enabled, you can recover your account by asking your selected friends to help you.';
|
||||
|
||||
@override
|
||||
String get recoverPasswordlessNotificationCardTitle => 'Enable notifications';
|
||||
|
||||
@override
|
||||
String get recoverPasswordlessNotificationCardSubtitle =>
|
||||
'Get notified when a friend has helped you recover your account.';
|
||||
|
||||
@override
|
||||
String get recoverPasswordlessNotificationCardBtn =>
|
||||
'Enable push notifications';
|
||||
|
||||
@override
|
||||
String get recoverPasswordlessQrInstructions =>
|
||||
'Let friends scan the QR code or share the link.';
|
||||
|
||||
@override
|
||||
String get recoverPasswordlessShareBtn => 'Share Recovery Link';
|
||||
|
||||
@override
|
||||
String get recoverPasswordlessCopyBtn => 'Copy';
|
||||
|
||||
@override
|
||||
String get recoverPasswordlessCopiedSnackbar => 'Link copied to clipboard!';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryNoShareStored =>
|
||||
'No recovery share stored for this contact.';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryShareSent =>
|
||||
'Recovery share successfully sent!';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryNetworkError =>
|
||||
'Network error, please ensure you have internet';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryInvalidEmail =>
|
||||
'The email address is invalid.';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryResendEmail => 'Resend recovery email';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryHelpAFriend => 'Help a Friend';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoverySelectContactDesc =>
|
||||
'Please select the contact who requested recovery below to send them their recovery key.';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoverySearchContacts => 'Search contacts...';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryNoContactsFound => 'No contacts found';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryCantHelpHim => 'You can\'t help him';
|
||||
|
||||
@override
|
||||
String passwordlessRecoveryDoesAskedYou(Object username) {
|
||||
return 'Does $username has asked you?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryVerifySourceDesc =>
|
||||
'Please ensure that you actualy received the link/qr code from your friend! If you are unsure, please verify again and then click again on the link or come back.';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryNo => 'No';
|
||||
|
||||
@override
|
||||
String get passwordlessRecoveryYes => 'Yes';
|
||||
|
||||
@override
|
||||
String passwordlessRecoveryYesWithTimer(Object seconds) {
|
||||
return 'Yes (${seconds}s)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recoverPasswordlessBeingRecoveredLabel =>
|
||||
'Account being recovered';
|
||||
|
||||
@override
|
||||
String recoverPasswordlessSharesReceived(Object received, Object threshold) {
|
||||
return '$received of $threshold friends have shared';
|
||||
}
|
||||
|
||||
@override
|
||||
String get recoverPasswordlessRecoverNowBtn => 'Recover now';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 80b21d7e566a12d105f573cb7224b6cdfe37048b
|
||||
Subproject commit b7d703c82a5ac0b6776847cb4eb6d637620f6585
|
||||
65
lib/src/model/json/onboarding_state.model.dart
Normal file
65
lib/src/model/json/onboarding_state.model.dart
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'onboarding_state.model.g.dart';
|
||||
|
||||
/// Holds information about a single trusted friend who has already shared their
|
||||
/// recovery part.
|
||||
@JsonSerializable()
|
||||
class ReceivedRecoveryShare {
|
||||
ReceivedRecoveryShare({
|
||||
required this.messageId,
|
||||
required this.trustedFriendDisplayName,
|
||||
required this.myDisplayName,
|
||||
required this.myUserId,
|
||||
required this.myAvatarSvg,
|
||||
required this.threshold,
|
||||
required this.sharedSecretDataBytes,
|
||||
});
|
||||
|
||||
factory ReceivedRecoveryShare.fromJson(Map<String, dynamic> json) =>
|
||||
_$ReceivedRecoveryShareFromJson(json);
|
||||
|
||||
final int messageId;
|
||||
|
||||
final String trustedFriendDisplayName;
|
||||
|
||||
final String myDisplayName;
|
||||
final int myUserId;
|
||||
final List<int>? myAvatarSvg;
|
||||
|
||||
final int threshold;
|
||||
|
||||
final List<int> sharedSecretDataBytes;
|
||||
|
||||
Map<String, dynamic> toJson() => _$ReceivedRecoveryShareToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class OnboardingState {
|
||||
OnboardingState({
|
||||
this.hasOnboardingFinished = false,
|
||||
this.hasStartedPasswordlessRecovery = false,
|
||||
this.notificationId,
|
||||
this.downloadAuthToken,
|
||||
this.serverRegistered = false,
|
||||
this.encryptionKey,
|
||||
this.emailRecoveryRequested = false,
|
||||
});
|
||||
|
||||
factory OnboardingState.fromJson(Map<String, dynamic> json) =>
|
||||
_$OnboardingStateFromJson(json);
|
||||
|
||||
bool hasOnboardingFinished;
|
||||
bool hasStartedPasswordlessRecovery;
|
||||
|
||||
String? notificationId;
|
||||
List<int>? downloadAuthToken;
|
||||
bool serverRegistered;
|
||||
List<int>? encryptionKey;
|
||||
bool emailRecoveryRequested;
|
||||
|
||||
|
||||
List<ReceivedRecoveryShare> receivedShares = [];
|
||||
|
||||
Map<String, dynamic> toJson() => _$OnboardingStateToJson(this);
|
||||
}
|
||||
67
lib/src/model/json/onboarding_state.model.g.dart
Normal file
67
lib/src/model/json/onboarding_state.model.g.dart
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'onboarding_state.model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
ReceivedRecoveryShare _$ReceivedRecoveryShareFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => ReceivedRecoveryShare(
|
||||
messageId: (json['messageId'] as num).toInt(),
|
||||
trustedFriendDisplayName: json['trustedFriendDisplayName'] as String,
|
||||
myDisplayName: json['myDisplayName'] as String,
|
||||
myUserId: (json['myUserId'] as num).toInt(),
|
||||
myAvatarSvg: (json['myAvatarSvg'] as List<dynamic>?)
|
||||
?.map((e) => (e as num).toInt())
|
||||
.toList(),
|
||||
threshold: (json['threshold'] as num).toInt(),
|
||||
sharedSecretDataBytes: (json['sharedSecretDataBytes'] as List<dynamic>)
|
||||
.map((e) => (e as num).toInt())
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ReceivedRecoveryShareToJson(
|
||||
ReceivedRecoveryShare instance,
|
||||
) => <String, dynamic>{
|
||||
'messageId': instance.messageId,
|
||||
'trustedFriendDisplayName': instance.trustedFriendDisplayName,
|
||||
'myDisplayName': instance.myDisplayName,
|
||||
'myUserId': instance.myUserId,
|
||||
'myAvatarSvg': instance.myAvatarSvg,
|
||||
'threshold': instance.threshold,
|
||||
'sharedSecretDataBytes': instance.sharedSecretDataBytes,
|
||||
};
|
||||
|
||||
OnboardingState _$OnboardingStateFromJson(Map<String, dynamic> json) =>
|
||||
OnboardingState(
|
||||
hasOnboardingFinished: json['hasOnboardingFinished'] as bool? ?? false,
|
||||
hasStartedPasswordlessRecovery:
|
||||
json['hasStartedPasswordlessRecovery'] as bool? ?? false,
|
||||
notificationId: json['notificationId'] as String?,
|
||||
downloadAuthToken: (json['downloadAuthToken'] as List<dynamic>?)
|
||||
?.map((e) => (e as num).toInt())
|
||||
.toList(),
|
||||
serverRegistered: json['serverRegistered'] as bool? ?? false,
|
||||
encryptionKey: (json['encryptionKey'] as List<dynamic>?)
|
||||
?.map((e) => (e as num).toInt())
|
||||
.toList(),
|
||||
emailRecoveryRequested:
|
||||
json['emailRecoveryRequested'] as bool? ?? false,
|
||||
)
|
||||
..receivedShares = (json['receivedShares'] as List<dynamic>)
|
||||
.map((e) => ReceivedRecoveryShare.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
Map<String, dynamic> _$OnboardingStateToJson(OnboardingState instance) =>
|
||||
<String, dynamic>{
|
||||
'hasOnboardingFinished': instance.hasOnboardingFinished,
|
||||
'hasStartedPasswordlessRecovery': instance.hasStartedPasswordlessRecovery,
|
||||
'notificationId': instance.notificationId,
|
||||
'downloadAuthToken': instance.downloadAuthToken,
|
||||
'serverRegistered': instance.serverRegistered,
|
||||
'encryptionKey': instance.encryptionKey,
|
||||
'emailRecoveryRequested': instance.emailRecoveryRequested,
|
||||
'receivedShares': instance.receivedShares,
|
||||
};
|
||||
|
|
@ -40,9 +40,6 @@ class UserData {
|
|||
@JsonKey(defaultValue: SetupProfile.standard)
|
||||
SetupProfile setupProfile = SetupProfile.standard;
|
||||
|
||||
@JsonKey(defaultValue: SecurityProfile.normal)
|
||||
SecurityProfile securityProfile = SecurityProfile.normal;
|
||||
|
||||
// --- SUBSCRIPTION DTA ---
|
||||
|
||||
@JsonKey(defaultValue: 'Free')
|
||||
|
|
@ -208,22 +205,31 @@ class TwonlySafeBackup {
|
|||
|
||||
@JsonSerializable()
|
||||
class PasswordLessRecovery {
|
||||
PasswordLessRecovery({
|
||||
this.email,
|
||||
this.pinSeed,
|
||||
this.pinUnlockToken,
|
||||
this.threshold,
|
||||
this.lastHeartbeat,
|
||||
});
|
||||
PasswordLessRecovery(this.threshold);
|
||||
|
||||
factory PasswordLessRecovery.fromJson(Map<String, dynamic> json) =>
|
||||
_$PasswordLessRecoveryFromJson(json);
|
||||
|
||||
// Only stored, so the user can see his deposit email address...
|
||||
String? email;
|
||||
String? pinSeed;
|
||||
String? pinUnlockToken;
|
||||
int? threshold;
|
||||
DateTime? lastHeartbeat;
|
||||
|
||||
// <--
|
||||
// Data shared with trusted friends
|
||||
|
||||
int threshold;
|
||||
// Trusted friends are able to brute-force the pin -> Server delets after X tries
|
||||
List<int>? pinSeed;
|
||||
List<int>? pinUnlockToken;
|
||||
|
||||
// Stored not on the server, so the server is unable to link a email to a user until the actuall recovery or can
|
||||
// brute-force the pin
|
||||
List<int>? encryptedServerKeyNonce;
|
||||
// --->
|
||||
|
||||
// Checking with the server that the server data is valid and not delted throug the pin protection for example.
|
||||
DateTime? lastServerHeartbeat;
|
||||
DateTime? lastContactHeartbeat;
|
||||
List<int>? encryptedServerKey;
|
||||
|
||||
Map<String, dynamic> toJson() => _$PasswordLessRecoveryToJson(this);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,12 +23,6 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
|
|||
..setupProfile =
|
||||
$enumDecodeNullable(_$SetupProfileEnumMap, json['setupProfile']) ??
|
||||
SetupProfile.standard
|
||||
..securityProfile =
|
||||
$enumDecodeNullable(
|
||||
_$SecurityProfileEnumMap,
|
||||
json['securityProfile'],
|
||||
) ??
|
||||
SecurityProfile.normal
|
||||
..subscriptionPlanIdStore = json['subscriptionPlanIdStore'] as String?
|
||||
..lastImageSend = json['lastImageSend'] == null
|
||||
? null
|
||||
|
|
@ -132,7 +126,6 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
|
|||
'isDeveloper': instance.isDeveloper,
|
||||
'deviceId': instance.deviceId,
|
||||
'setupProfile': _$SetupProfileEnumMap[instance.setupProfile]!,
|
||||
'securityProfile': _$SecurityProfileEnumMap[instance.securityProfile]!,
|
||||
'subscriptionPlan': instance.subscriptionPlan,
|
||||
'subscriptionPlanIdStore': instance.subscriptionPlanIdStore,
|
||||
'lastImageSend': instance.lastImageSend?.toIso8601String(),
|
||||
|
|
@ -192,12 +185,6 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
|
|||
const _$SetupProfileEnumMap = {
|
||||
SetupProfile.standard: 'standard',
|
||||
SetupProfile.customized: 'customized',
|
||||
SetupProfile.maximum: 'maximum',
|
||||
};
|
||||
|
||||
const _$SecurityProfileEnumMap = {
|
||||
SecurityProfile.normal: 'normal',
|
||||
SecurityProfile.strict: 'strict',
|
||||
};
|
||||
|
||||
const _$ThemeModeEnumMap = {
|
||||
|
|
@ -243,22 +230,37 @@ const _$LastBackupUploadStateEnumMap = {
|
|||
|
||||
PasswordLessRecovery _$PasswordLessRecoveryFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => PasswordLessRecovery(
|
||||
email: json['email'] as String?,
|
||||
pinSeed: json['pinSeed'] as String?,
|
||||
pinUnlockToken: json['pinUnlockToken'] as String?,
|
||||
threshold: (json['threshold'] as num?)?.toInt(),
|
||||
lastHeartbeat: json['lastHeartbeat'] == null
|
||||
) => PasswordLessRecovery((json['threshold'] as num).toInt())
|
||||
..email = json['email'] as String?
|
||||
..pinSeed = (json['pinSeed'] as List<dynamic>?)
|
||||
?.map((e) => (e as num).toInt())
|
||||
.toList()
|
||||
..pinUnlockToken = (json['pinUnlockToken'] as List<dynamic>?)
|
||||
?.map((e) => (e as num).toInt())
|
||||
.toList()
|
||||
..encryptedServerKeyNonce =
|
||||
(json['encryptedServerKeyNonce'] as List<dynamic>?)
|
||||
?.map((e) => (e as num).toInt())
|
||||
.toList()
|
||||
..lastServerHeartbeat = json['lastServerHeartbeat'] == null
|
||||
? null
|
||||
: DateTime.parse(json['lastHeartbeat'] as String),
|
||||
);
|
||||
: DateTime.parse(json['lastServerHeartbeat'] as String)
|
||||
..lastContactHeartbeat = json['lastContactHeartbeat'] == null
|
||||
? null
|
||||
: DateTime.parse(json['lastContactHeartbeat'] as String)
|
||||
..encryptedServerKey = (json['encryptedServerKey'] as List<dynamic>?)
|
||||
?.map((e) => (e as num).toInt())
|
||||
.toList();
|
||||
|
||||
Map<String, dynamic> _$PasswordLessRecoveryToJson(
|
||||
PasswordLessRecovery instance,
|
||||
) => <String, dynamic>{
|
||||
'email': instance.email,
|
||||
'threshold': instance.threshold,
|
||||
'pinSeed': instance.pinSeed,
|
||||
'pinUnlockToken': instance.pinUnlockToken,
|
||||
'threshold': instance.threshold,
|
||||
'lastHeartbeat': instance.lastHeartbeat?.toIso8601String(),
|
||||
'encryptedServerKeyNonce': instance.encryptedServerKeyNonce,
|
||||
'lastServerHeartbeat': instance.lastServerHeartbeat?.toIso8601String(),
|
||||
'lastContactHeartbeat': instance.lastContactHeartbeat?.toIso8601String(),
|
||||
'encryptedServerKey': instance.encryptedServerKey,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -809,6 +809,301 @@ class Handshake_AuthenticateWithLoginToken extends $pb.GeneratedMessage {
|
|||
void clearInBackground() => $_clearField(5);
|
||||
}
|
||||
|
||||
class Handshake_GetServerKeyForPasswordLessRecovery
|
||||
extends $pb.GeneratedMessage {
|
||||
factory Handshake_GetServerKeyForPasswordLessRecovery({
|
||||
$fixnum.Int64? userId,
|
||||
$core.List<$core.int>? encryptedServerKeyNone,
|
||||
$core.List<$core.int>? pinUnlockToken,
|
||||
$core.List<$core.int>? pinProtectionKey,
|
||||
$core.String? email,
|
||||
}) {
|
||||
final result = create();
|
||||
if (userId != null) result.userId = userId;
|
||||
if (encryptedServerKeyNone != null)
|
||||
result.encryptedServerKeyNone = encryptedServerKeyNone;
|
||||
if (pinUnlockToken != null) result.pinUnlockToken = pinUnlockToken;
|
||||
if (pinProtectionKey != null) result.pinProtectionKey = pinProtectionKey;
|
||||
if (email != null) result.email = email;
|
||||
return result;
|
||||
}
|
||||
|
||||
Handshake_GetServerKeyForPasswordLessRecovery._();
|
||||
|
||||
factory Handshake_GetServerKeyForPasswordLessRecovery.fromBuffer(
|
||||
$core.List<$core.int> data,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(data, registry);
|
||||
factory Handshake_GetServerKeyForPasswordLessRecovery.fromJson(
|
||||
$core.String json,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(json, registry);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
_omitMessageNames ? '' : 'Handshake.GetServerKeyForPasswordLessRecovery',
|
||||
package:
|
||||
const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'),
|
||||
createEmptyInstance: create)
|
||||
..aInt64(1, _omitFieldNames ? '' : 'userId')
|
||||
..a<$core.List<$core.int>>(
|
||||
2, _omitFieldNames ? '' : 'encryptedServerKeyNone', $pb.PbFieldType.OY)
|
||||
..a<$core.List<$core.int>>(
|
||||
3, _omitFieldNames ? '' : 'pinUnlockToken', $pb.PbFieldType.OY)
|
||||
..a<$core.List<$core.int>>(
|
||||
4, _omitFieldNames ? '' : 'pinProtectionKey', $pb.PbFieldType.OY)
|
||||
..aOS(5, _omitFieldNames ? '' : 'email')
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
Handshake_GetServerKeyForPasswordLessRecovery clone() => deepCopy();
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
Handshake_GetServerKeyForPasswordLessRecovery copyWith(
|
||||
void Function(Handshake_GetServerKeyForPasswordLessRecovery)
|
||||
updates) =>
|
||||
super.copyWith((message) =>
|
||||
updates(message as Handshake_GetServerKeyForPasswordLessRecovery))
|
||||
as Handshake_GetServerKeyForPasswordLessRecovery;
|
||||
|
||||
@$core.override
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static Handshake_GetServerKeyForPasswordLessRecovery create() =>
|
||||
Handshake_GetServerKeyForPasswordLessRecovery._();
|
||||
@$core.override
|
||||
Handshake_GetServerKeyForPasswordLessRecovery createEmptyInstance() =>
|
||||
create();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static Handshake_GetServerKeyForPasswordLessRecovery getDefault() =>
|
||||
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<
|
||||
Handshake_GetServerKeyForPasswordLessRecovery>(create);
|
||||
static Handshake_GetServerKeyForPasswordLessRecovery? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$fixnum.Int64 get userId => $_getI64(0);
|
||||
@$pb.TagNumber(1)
|
||||
set userId($fixnum.Int64 value) => $_setInt64(0, value);
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasUserId() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearUserId() => $_clearField(1);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$core.List<$core.int> get encryptedServerKeyNone => $_getN(1);
|
||||
@$pb.TagNumber(2)
|
||||
set encryptedServerKeyNone($core.List<$core.int> value) =>
|
||||
$_setBytes(1, value);
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasEncryptedServerKeyNone() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearEncryptedServerKeyNone() => $_clearField(2);
|
||||
|
||||
@$pb.TagNumber(3)
|
||||
$core.List<$core.int> get pinUnlockToken => $_getN(2);
|
||||
@$pb.TagNumber(3)
|
||||
set pinUnlockToken($core.List<$core.int> value) => $_setBytes(2, value);
|
||||
@$pb.TagNumber(3)
|
||||
$core.bool hasPinUnlockToken() => $_has(2);
|
||||
@$pb.TagNumber(3)
|
||||
void clearPinUnlockToken() => $_clearField(3);
|
||||
|
||||
@$pb.TagNumber(4)
|
||||
$core.List<$core.int> get pinProtectionKey => $_getN(3);
|
||||
@$pb.TagNumber(4)
|
||||
set pinProtectionKey($core.List<$core.int> value) => $_setBytes(3, value);
|
||||
@$pb.TagNumber(4)
|
||||
$core.bool hasPinProtectionKey() => $_has(3);
|
||||
@$pb.TagNumber(4)
|
||||
void clearPinProtectionKey() => $_clearField(4);
|
||||
|
||||
@$pb.TagNumber(5)
|
||||
$core.String get email => $_getSZ(4);
|
||||
@$pb.TagNumber(5)
|
||||
set email($core.String value) => $_setString(4, value);
|
||||
@$pb.TagNumber(5)
|
||||
$core.bool hasEmail() => $_has(4);
|
||||
@$pb.TagNumber(5)
|
||||
void clearEmail() => $_clearField(5);
|
||||
}
|
||||
|
||||
class Handshake_RegisterPasswordlessNotification extends $pb.GeneratedMessage {
|
||||
factory Handshake_RegisterPasswordlessNotification({
|
||||
$core.String? notificationId,
|
||||
$core.List<$core.int>? downloadAuthToken,
|
||||
$core.String? langCode,
|
||||
$core.String? googleFcm,
|
||||
}) {
|
||||
final result = create();
|
||||
if (notificationId != null) result.notificationId = notificationId;
|
||||
if (downloadAuthToken != null) result.downloadAuthToken = downloadAuthToken;
|
||||
if (langCode != null) result.langCode = langCode;
|
||||
if (googleFcm != null) result.googleFcm = googleFcm;
|
||||
return result;
|
||||
}
|
||||
|
||||
Handshake_RegisterPasswordlessNotification._();
|
||||
|
||||
factory Handshake_RegisterPasswordlessNotification.fromBuffer(
|
||||
$core.List<$core.int> data,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(data, registry);
|
||||
factory Handshake_RegisterPasswordlessNotification.fromJson($core.String json,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(json, registry);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
_omitMessageNames ? '' : 'Handshake.RegisterPasswordlessNotification',
|
||||
package:
|
||||
const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'),
|
||||
createEmptyInstance: create)
|
||||
..aOS(1, _omitFieldNames ? '' : 'notificationId')
|
||||
..a<$core.List<$core.int>>(
|
||||
2, _omitFieldNames ? '' : 'downloadAuthToken', $pb.PbFieldType.OY)
|
||||
..aOS(3, _omitFieldNames ? '' : 'langCode')
|
||||
..aOS(4, _omitFieldNames ? '' : 'googleFcm')
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
Handshake_RegisterPasswordlessNotification clone() => deepCopy();
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
Handshake_RegisterPasswordlessNotification copyWith(
|
||||
void Function(Handshake_RegisterPasswordlessNotification) updates) =>
|
||||
super.copyWith((message) =>
|
||||
updates(message as Handshake_RegisterPasswordlessNotification))
|
||||
as Handshake_RegisterPasswordlessNotification;
|
||||
|
||||
@$core.override
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static Handshake_RegisterPasswordlessNotification create() =>
|
||||
Handshake_RegisterPasswordlessNotification._();
|
||||
@$core.override
|
||||
Handshake_RegisterPasswordlessNotification createEmptyInstance() => create();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static Handshake_RegisterPasswordlessNotification getDefault() =>
|
||||
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<
|
||||
Handshake_RegisterPasswordlessNotification>(create);
|
||||
static Handshake_RegisterPasswordlessNotification? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.String get notificationId => $_getSZ(0);
|
||||
@$pb.TagNumber(1)
|
||||
set notificationId($core.String value) => $_setString(0, value);
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasNotificationId() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearNotificationId() => $_clearField(1);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$core.List<$core.int> get downloadAuthToken => $_getN(1);
|
||||
@$pb.TagNumber(2)
|
||||
set downloadAuthToken($core.List<$core.int> value) => $_setBytes(1, value);
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasDownloadAuthToken() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearDownloadAuthToken() => $_clearField(2);
|
||||
|
||||
@$pb.TagNumber(3)
|
||||
$core.String get langCode => $_getSZ(2);
|
||||
@$pb.TagNumber(3)
|
||||
set langCode($core.String value) => $_setString(2, value);
|
||||
@$pb.TagNumber(3)
|
||||
$core.bool hasLangCode() => $_has(2);
|
||||
@$pb.TagNumber(3)
|
||||
void clearLangCode() => $_clearField(3);
|
||||
|
||||
@$pb.TagNumber(4)
|
||||
$core.String get googleFcm => $_getSZ(3);
|
||||
@$pb.TagNumber(4)
|
||||
set googleFcm($core.String value) => $_setString(3, value);
|
||||
@$pb.TagNumber(4)
|
||||
$core.bool hasGoogleFcm() => $_has(3);
|
||||
@$pb.TagNumber(4)
|
||||
void clearGoogleFcm() => $_clearField(4);
|
||||
}
|
||||
|
||||
class Handshake_CheckForPasswordlessNotification extends $pb.GeneratedMessage {
|
||||
factory Handshake_CheckForPasswordlessNotification({
|
||||
$core.String? notificationId,
|
||||
$core.List<$core.int>? downloadAuthToken,
|
||||
$core.Iterable<$fixnum.Int64>? alreadyReceivedMessageIds,
|
||||
}) {
|
||||
final result = create();
|
||||
if (notificationId != null) result.notificationId = notificationId;
|
||||
if (downloadAuthToken != null) result.downloadAuthToken = downloadAuthToken;
|
||||
if (alreadyReceivedMessageIds != null)
|
||||
result.alreadyReceivedMessageIds.addAll(alreadyReceivedMessageIds);
|
||||
return result;
|
||||
}
|
||||
|
||||
Handshake_CheckForPasswordlessNotification._();
|
||||
|
||||
factory Handshake_CheckForPasswordlessNotification.fromBuffer(
|
||||
$core.List<$core.int> data,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(data, registry);
|
||||
factory Handshake_CheckForPasswordlessNotification.fromJson($core.String json,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(json, registry);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
_omitMessageNames ? '' : 'Handshake.CheckForPasswordlessNotification',
|
||||
package:
|
||||
const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'),
|
||||
createEmptyInstance: create)
|
||||
..aOS(1, _omitFieldNames ? '' : 'notificationId')
|
||||
..a<$core.List<$core.int>>(
|
||||
2, _omitFieldNames ? '' : 'downloadAuthToken', $pb.PbFieldType.OY)
|
||||
..p<$fixnum.Int64>(3, _omitFieldNames ? '' : 'alreadyReceivedMessageIds',
|
||||
$pb.PbFieldType.K6)
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
Handshake_CheckForPasswordlessNotification clone() => deepCopy();
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
Handshake_CheckForPasswordlessNotification copyWith(
|
||||
void Function(Handshake_CheckForPasswordlessNotification) updates) =>
|
||||
super.copyWith((message) =>
|
||||
updates(message as Handshake_CheckForPasswordlessNotification))
|
||||
as Handshake_CheckForPasswordlessNotification;
|
||||
|
||||
@$core.override
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static Handshake_CheckForPasswordlessNotification create() =>
|
||||
Handshake_CheckForPasswordlessNotification._();
|
||||
@$core.override
|
||||
Handshake_CheckForPasswordlessNotification createEmptyInstance() => create();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static Handshake_CheckForPasswordlessNotification getDefault() =>
|
||||
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<
|
||||
Handshake_CheckForPasswordlessNotification>(create);
|
||||
static Handshake_CheckForPasswordlessNotification? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.String get notificationId => $_getSZ(0);
|
||||
@$pb.TagNumber(1)
|
||||
set notificationId($core.String value) => $_setString(0, value);
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasNotificationId() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearNotificationId() => $_clearField(1);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$core.List<$core.int> get downloadAuthToken => $_getN(1);
|
||||
@$pb.TagNumber(2)
|
||||
set downloadAuthToken($core.List<$core.int> value) => $_setBytes(1, value);
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasDownloadAuthToken() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearDownloadAuthToken() => $_clearField(2);
|
||||
|
||||
@$pb.TagNumber(3)
|
||||
$pb.PbList<$fixnum.Int64> get alreadyReceivedMessageIds => $_getList(2);
|
||||
}
|
||||
|
||||
enum Handshake_Handshake {
|
||||
register,
|
||||
getAuthChallenge,
|
||||
|
|
@ -817,6 +1112,9 @@ enum Handshake_Handshake {
|
|||
requestPOW,
|
||||
authenticateWithLoginToken,
|
||||
getUseridByUsername,
|
||||
getServerKeyForPasswordlessRecovery,
|
||||
registerPasswordlessNotification,
|
||||
checkForPasswordlessNotification,
|
||||
notSet
|
||||
}
|
||||
|
||||
|
|
@ -829,6 +1127,12 @@ class Handshake extends $pb.GeneratedMessage {
|
|||
Handshake_RequestPOW? requestPOW,
|
||||
Handshake_AuthenticateWithLoginToken? authenticateWithLoginToken,
|
||||
Handshake_GetUserIdByUsername? getUseridByUsername,
|
||||
Handshake_GetServerKeyForPasswordLessRecovery?
|
||||
getServerKeyForPasswordlessRecovery,
|
||||
Handshake_RegisterPasswordlessNotification?
|
||||
registerPasswordlessNotification,
|
||||
Handshake_CheckForPasswordlessNotification?
|
||||
checkForPasswordlessNotification,
|
||||
}) {
|
||||
final result = create();
|
||||
if (register != null) result.register = register;
|
||||
|
|
@ -840,6 +1144,15 @@ class Handshake extends $pb.GeneratedMessage {
|
|||
result.authenticateWithLoginToken = authenticateWithLoginToken;
|
||||
if (getUseridByUsername != null)
|
||||
result.getUseridByUsername = getUseridByUsername;
|
||||
if (getServerKeyForPasswordlessRecovery != null)
|
||||
result.getServerKeyForPasswordlessRecovery =
|
||||
getServerKeyForPasswordlessRecovery;
|
||||
if (registerPasswordlessNotification != null)
|
||||
result.registerPasswordlessNotification =
|
||||
registerPasswordlessNotification;
|
||||
if (checkForPasswordlessNotification != null)
|
||||
result.checkForPasswordlessNotification =
|
||||
checkForPasswordlessNotification;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -861,6 +1174,9 @@ class Handshake extends $pb.GeneratedMessage {
|
|||
5: Handshake_Handshake.requestPOW,
|
||||
6: Handshake_Handshake.authenticateWithLoginToken,
|
||||
7: Handshake_Handshake.getUseridByUsername,
|
||||
8: Handshake_Handshake.getServerKeyForPasswordlessRecovery,
|
||||
9: Handshake_Handshake.registerPasswordlessNotification,
|
||||
10: Handshake_Handshake.checkForPasswordlessNotification,
|
||||
0: Handshake_Handshake.notSet
|
||||
};
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
|
|
@ -868,7 +1184,7 @@ class Handshake extends $pb.GeneratedMessage {
|
|||
package:
|
||||
const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'),
|
||||
createEmptyInstance: create)
|
||||
..oo(0, [1, 2, 3, 4, 5, 6, 7])
|
||||
..oo(0, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
|
||||
..aOM<Handshake_Register>(1, _omitFieldNames ? '' : 'register',
|
||||
subBuilder: Handshake_Register.create)
|
||||
..aOM<Handshake_GetAuthChallenge>(
|
||||
|
|
@ -887,6 +1203,15 @@ class Handshake extends $pb.GeneratedMessage {
|
|||
..aOM<Handshake_GetUserIdByUsername>(
|
||||
7, _omitFieldNames ? '' : 'getUseridByUsername',
|
||||
subBuilder: Handshake_GetUserIdByUsername.create)
|
||||
..aOM<Handshake_GetServerKeyForPasswordLessRecovery>(
|
||||
8, _omitFieldNames ? '' : 'getServerKeyForPasswordlessRecovery',
|
||||
subBuilder: Handshake_GetServerKeyForPasswordLessRecovery.create)
|
||||
..aOM<Handshake_RegisterPasswordlessNotification>(
|
||||
9, _omitFieldNames ? '' : 'registerPasswordlessNotification',
|
||||
subBuilder: Handshake_RegisterPasswordlessNotification.create)
|
||||
..aOM<Handshake_CheckForPasswordlessNotification>(
|
||||
10, _omitFieldNames ? '' : 'checkForPasswordlessNotification',
|
||||
subBuilder: Handshake_CheckForPasswordlessNotification.create)
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
|
|
@ -914,6 +1239,9 @@ class Handshake extends $pb.GeneratedMessage {
|
|||
@$pb.TagNumber(5)
|
||||
@$pb.TagNumber(6)
|
||||
@$pb.TagNumber(7)
|
||||
@$pb.TagNumber(8)
|
||||
@$pb.TagNumber(9)
|
||||
@$pb.TagNumber(10)
|
||||
Handshake_Handshake whichHandshake() =>
|
||||
_Handshake_HandshakeByTag[$_whichOneof(0)]!;
|
||||
@$pb.TagNumber(1)
|
||||
|
|
@ -923,6 +1251,9 @@ class Handshake extends $pb.GeneratedMessage {
|
|||
@$pb.TagNumber(5)
|
||||
@$pb.TagNumber(6)
|
||||
@$pb.TagNumber(7)
|
||||
@$pb.TagNumber(8)
|
||||
@$pb.TagNumber(9)
|
||||
@$pb.TagNumber(10)
|
||||
void clearHandshake() => $_clearField($_whichOneof(0));
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
|
|
@ -1006,6 +1337,51 @@ class Handshake extends $pb.GeneratedMessage {
|
|||
void clearGetUseridByUsername() => $_clearField(7);
|
||||
@$pb.TagNumber(7)
|
||||
Handshake_GetUserIdByUsername ensureGetUseridByUsername() => $_ensure(6);
|
||||
|
||||
@$pb.TagNumber(8)
|
||||
Handshake_GetServerKeyForPasswordLessRecovery
|
||||
get getServerKeyForPasswordlessRecovery => $_getN(7);
|
||||
@$pb.TagNumber(8)
|
||||
set getServerKeyForPasswordlessRecovery(
|
||||
Handshake_GetServerKeyForPasswordLessRecovery value) =>
|
||||
$_setField(8, value);
|
||||
@$pb.TagNumber(8)
|
||||
$core.bool hasGetServerKeyForPasswordlessRecovery() => $_has(7);
|
||||
@$pb.TagNumber(8)
|
||||
void clearGetServerKeyForPasswordlessRecovery() => $_clearField(8);
|
||||
@$pb.TagNumber(8)
|
||||
Handshake_GetServerKeyForPasswordLessRecovery
|
||||
ensureGetServerKeyForPasswordlessRecovery() => $_ensure(7);
|
||||
|
||||
@$pb.TagNumber(9)
|
||||
Handshake_RegisterPasswordlessNotification
|
||||
get registerPasswordlessNotification => $_getN(8);
|
||||
@$pb.TagNumber(9)
|
||||
set registerPasswordlessNotification(
|
||||
Handshake_RegisterPasswordlessNotification value) =>
|
||||
$_setField(9, value);
|
||||
@$pb.TagNumber(9)
|
||||
$core.bool hasRegisterPasswordlessNotification() => $_has(8);
|
||||
@$pb.TagNumber(9)
|
||||
void clearRegisterPasswordlessNotification() => $_clearField(9);
|
||||
@$pb.TagNumber(9)
|
||||
Handshake_RegisterPasswordlessNotification
|
||||
ensureRegisterPasswordlessNotification() => $_ensure(8);
|
||||
|
||||
@$pb.TagNumber(10)
|
||||
Handshake_CheckForPasswordlessNotification
|
||||
get checkForPasswordlessNotification => $_getN(9);
|
||||
@$pb.TagNumber(10)
|
||||
set checkForPasswordlessNotification(
|
||||
Handshake_CheckForPasswordlessNotification value) =>
|
||||
$_setField(10, value);
|
||||
@$pb.TagNumber(10)
|
||||
$core.bool hasCheckForPasswordlessNotification() => $_has(9);
|
||||
@$pb.TagNumber(10)
|
||||
void clearCheckForPasswordlessNotification() => $_clearField(10);
|
||||
@$pb.TagNumber(10)
|
||||
Handshake_CheckForPasswordlessNotification
|
||||
ensureCheckForPasswordlessNotification() => $_ensure(9);
|
||||
}
|
||||
|
||||
class ApplicationData_TextMessage extends $pb.GeneratedMessage {
|
||||
|
|
@ -2182,6 +2558,160 @@ class ApplicationData_Deprecated extends $pb.GeneratedMessage {
|
|||
static ApplicationData_Deprecated? _defaultInstance;
|
||||
}
|
||||
|
||||
class ApplicationData_RegisterPasswordLessRecovery
|
||||
extends $pb.GeneratedMessage {
|
||||
factory ApplicationData_RegisterPasswordLessRecovery({
|
||||
$core.List<$core.int>? encryptedServerKey,
|
||||
$core.List<$core.int>? pinUnlockToken,
|
||||
}) {
|
||||
final result = create();
|
||||
if (encryptedServerKey != null)
|
||||
result.encryptedServerKey = encryptedServerKey;
|
||||
if (pinUnlockToken != null) result.pinUnlockToken = pinUnlockToken;
|
||||
return result;
|
||||
}
|
||||
|
||||
ApplicationData_RegisterPasswordLessRecovery._();
|
||||
|
||||
factory ApplicationData_RegisterPasswordLessRecovery.fromBuffer(
|
||||
$core.List<$core.int> data,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(data, registry);
|
||||
factory ApplicationData_RegisterPasswordLessRecovery.fromJson(
|
||||
$core.String json,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(json, registry);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
_omitMessageNames ? '' : 'ApplicationData.RegisterPasswordLessRecovery',
|
||||
package:
|
||||
const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'),
|
||||
createEmptyInstance: create)
|
||||
..a<$core.List<$core.int>>(
|
||||
1, _omitFieldNames ? '' : 'encryptedServerKey', $pb.PbFieldType.OY,
|
||||
protoName: 'encryptedServerKey')
|
||||
..a<$core.List<$core.int>>(
|
||||
2, _omitFieldNames ? '' : 'pinUnlockToken', $pb.PbFieldType.OY,
|
||||
protoName: 'pinUnlockToken')
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
ApplicationData_RegisterPasswordLessRecovery clone() => deepCopy();
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
ApplicationData_RegisterPasswordLessRecovery copyWith(
|
||||
void Function(ApplicationData_RegisterPasswordLessRecovery)
|
||||
updates) =>
|
||||
super.copyWith((message) =>
|
||||
updates(message as ApplicationData_RegisterPasswordLessRecovery))
|
||||
as ApplicationData_RegisterPasswordLessRecovery;
|
||||
|
||||
@$core.override
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static ApplicationData_RegisterPasswordLessRecovery create() =>
|
||||
ApplicationData_RegisterPasswordLessRecovery._();
|
||||
@$core.override
|
||||
ApplicationData_RegisterPasswordLessRecovery createEmptyInstance() =>
|
||||
create();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static ApplicationData_RegisterPasswordLessRecovery getDefault() =>
|
||||
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<
|
||||
ApplicationData_RegisterPasswordLessRecovery>(create);
|
||||
static ApplicationData_RegisterPasswordLessRecovery? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.List<$core.int> get encryptedServerKey => $_getN(0);
|
||||
@$pb.TagNumber(1)
|
||||
set encryptedServerKey($core.List<$core.int> value) => $_setBytes(0, value);
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasEncryptedServerKey() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearEncryptedServerKey() => $_clearField(1);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$core.List<$core.int> get pinUnlockToken => $_getN(1);
|
||||
@$pb.TagNumber(2)
|
||||
set pinUnlockToken($core.List<$core.int> value) => $_setBytes(1, value);
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasPinUnlockToken() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearPinUnlockToken() => $_clearField(2);
|
||||
}
|
||||
|
||||
class ApplicationData_PasswordlessNotification extends $pb.GeneratedMessage {
|
||||
factory ApplicationData_PasswordlessNotification({
|
||||
$core.String? notificationId,
|
||||
$core.List<$core.int>? encryptedMessage,
|
||||
}) {
|
||||
final result = create();
|
||||
if (notificationId != null) result.notificationId = notificationId;
|
||||
if (encryptedMessage != null) result.encryptedMessage = encryptedMessage;
|
||||
return result;
|
||||
}
|
||||
|
||||
ApplicationData_PasswordlessNotification._();
|
||||
|
||||
factory ApplicationData_PasswordlessNotification.fromBuffer(
|
||||
$core.List<$core.int> data,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(data, registry);
|
||||
factory ApplicationData_PasswordlessNotification.fromJson($core.String json,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(json, registry);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
_omitMessageNames ? '' : 'ApplicationData.PasswordlessNotification',
|
||||
package:
|
||||
const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'),
|
||||
createEmptyInstance: create)
|
||||
..aOS(1, _omitFieldNames ? '' : 'notificationId')
|
||||
..a<$core.List<$core.int>>(
|
||||
2, _omitFieldNames ? '' : 'encryptedMessage', $pb.PbFieldType.OY)
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
ApplicationData_PasswordlessNotification clone() => deepCopy();
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
ApplicationData_PasswordlessNotification copyWith(
|
||||
void Function(ApplicationData_PasswordlessNotification) updates) =>
|
||||
super.copyWith((message) =>
|
||||
updates(message as ApplicationData_PasswordlessNotification))
|
||||
as ApplicationData_PasswordlessNotification;
|
||||
|
||||
@$core.override
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static ApplicationData_PasswordlessNotification create() =>
|
||||
ApplicationData_PasswordlessNotification._();
|
||||
@$core.override
|
||||
ApplicationData_PasswordlessNotification createEmptyInstance() => create();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static ApplicationData_PasswordlessNotification getDefault() =>
|
||||
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<
|
||||
ApplicationData_PasswordlessNotification>(create);
|
||||
static ApplicationData_PasswordlessNotification? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.String get notificationId => $_getSZ(0);
|
||||
@$pb.TagNumber(1)
|
||||
set notificationId($core.String value) => $_setString(0, value);
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasNotificationId() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearNotificationId() => $_clearField(1);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$core.List<$core.int> get encryptedMessage => $_getN(1);
|
||||
@$pb.TagNumber(2)
|
||||
set encryptedMessage($core.List<$core.int> value) => $_setBytes(1, value);
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasEncryptedMessage() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearEncryptedMessage() => $_clearField(2);
|
||||
}
|
||||
|
||||
enum ApplicationData_ApplicationData {
|
||||
textMessage,
|
||||
getUserByUsername,
|
||||
|
|
@ -2209,6 +2739,8 @@ enum ApplicationData_ApplicationData {
|
|||
ipaForceCheck,
|
||||
addAdditionalUser,
|
||||
setLoginToken,
|
||||
registerPasswordlessRecovery,
|
||||
passwordlessNotification,
|
||||
notSet
|
||||
}
|
||||
|
||||
|
|
@ -2240,6 +2772,8 @@ class ApplicationData extends $pb.GeneratedMessage {
|
|||
ApplicationData_IPAForceCheck? ipaForceCheck,
|
||||
ApplicationData_AddAdditionalUser? addAdditionalUser,
|
||||
ApplicationData_SetLoginToken? setLoginToken,
|
||||
ApplicationData_RegisterPasswordLessRecovery? registerPasswordlessRecovery,
|
||||
ApplicationData_PasswordlessNotification? passwordlessNotification,
|
||||
}) {
|
||||
final result = create();
|
||||
if (textMessage != null) result.textMessage = textMessage;
|
||||
|
|
@ -2274,6 +2808,10 @@ class ApplicationData extends $pb.GeneratedMessage {
|
|||
if (ipaForceCheck != null) result.ipaForceCheck = ipaForceCheck;
|
||||
if (addAdditionalUser != null) result.addAdditionalUser = addAdditionalUser;
|
||||
if (setLoginToken != null) result.setLoginToken = setLoginToken;
|
||||
if (registerPasswordlessRecovery != null)
|
||||
result.registerPasswordlessRecovery = registerPasswordlessRecovery;
|
||||
if (passwordlessNotification != null)
|
||||
result.passwordlessNotification = passwordlessNotification;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -2314,6 +2852,8 @@ class ApplicationData extends $pb.GeneratedMessage {
|
|||
28: ApplicationData_ApplicationData.ipaForceCheck,
|
||||
29: ApplicationData_ApplicationData.addAdditionalUser,
|
||||
30: ApplicationData_ApplicationData.setLoginToken,
|
||||
31: ApplicationData_ApplicationData.registerPasswordlessRecovery,
|
||||
32: ApplicationData_ApplicationData.passwordlessNotification,
|
||||
0: ApplicationData_ApplicationData.notSet
|
||||
};
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
|
|
@ -2347,7 +2887,9 @@ class ApplicationData extends $pb.GeneratedMessage {
|
|||
27,
|
||||
28,
|
||||
29,
|
||||
30
|
||||
30,
|
||||
31,
|
||||
32
|
||||
])
|
||||
..aOM<ApplicationData_TextMessage>(1, _omitFieldNames ? '' : 'textMessage',
|
||||
protoName: 'textMessage',
|
||||
|
|
@ -2439,6 +2981,12 @@ class ApplicationData extends $pb.GeneratedMessage {
|
|||
..aOM<ApplicationData_SetLoginToken>(
|
||||
30, _omitFieldNames ? '' : 'setLoginToken',
|
||||
subBuilder: ApplicationData_SetLoginToken.create)
|
||||
..aOM<ApplicationData_RegisterPasswordLessRecovery>(
|
||||
31, _omitFieldNames ? '' : 'registerPasswordlessRecovery',
|
||||
subBuilder: ApplicationData_RegisterPasswordLessRecovery.create)
|
||||
..aOM<ApplicationData_PasswordlessNotification>(
|
||||
32, _omitFieldNames ? '' : 'passwordlessNotification',
|
||||
subBuilder: ApplicationData_PasswordlessNotification.create)
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
|
|
@ -2486,6 +3034,8 @@ class ApplicationData extends $pb.GeneratedMessage {
|
|||
@$pb.TagNumber(28)
|
||||
@$pb.TagNumber(29)
|
||||
@$pb.TagNumber(30)
|
||||
@$pb.TagNumber(31)
|
||||
@$pb.TagNumber(32)
|
||||
ApplicationData_ApplicationData whichApplicationData() =>
|
||||
_ApplicationData_ApplicationDataByTag[$_whichOneof(0)]!;
|
||||
@$pb.TagNumber(1)
|
||||
|
|
@ -2514,6 +3064,8 @@ class ApplicationData extends $pb.GeneratedMessage {
|
|||
@$pb.TagNumber(28)
|
||||
@$pb.TagNumber(29)
|
||||
@$pb.TagNumber(30)
|
||||
@$pb.TagNumber(31)
|
||||
@$pb.TagNumber(32)
|
||||
void clearApplicationData() => $_clearField($_whichOneof(0));
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
|
|
@ -2819,6 +3371,36 @@ class ApplicationData extends $pb.GeneratedMessage {
|
|||
void clearSetLoginToken() => $_clearField(30);
|
||||
@$pb.TagNumber(30)
|
||||
ApplicationData_SetLoginToken ensureSetLoginToken() => $_ensure(25);
|
||||
|
||||
@$pb.TagNumber(31)
|
||||
ApplicationData_RegisterPasswordLessRecovery
|
||||
get registerPasswordlessRecovery => $_getN(26);
|
||||
@$pb.TagNumber(31)
|
||||
set registerPasswordlessRecovery(
|
||||
ApplicationData_RegisterPasswordLessRecovery value) =>
|
||||
$_setField(31, value);
|
||||
@$pb.TagNumber(31)
|
||||
$core.bool hasRegisterPasswordlessRecovery() => $_has(26);
|
||||
@$pb.TagNumber(31)
|
||||
void clearRegisterPasswordlessRecovery() => $_clearField(31);
|
||||
@$pb.TagNumber(31)
|
||||
ApplicationData_RegisterPasswordLessRecovery
|
||||
ensureRegisterPasswordlessRecovery() => $_ensure(26);
|
||||
|
||||
@$pb.TagNumber(32)
|
||||
ApplicationData_PasswordlessNotification get passwordlessNotification =>
|
||||
$_getN(27);
|
||||
@$pb.TagNumber(32)
|
||||
set passwordlessNotification(
|
||||
ApplicationData_PasswordlessNotification value) =>
|
||||
$_setField(32, value);
|
||||
@$pb.TagNumber(32)
|
||||
$core.bool hasPasswordlessNotification() => $_has(27);
|
||||
@$pb.TagNumber(32)
|
||||
void clearPasswordlessNotification() => $_clearField(32);
|
||||
@$pb.TagNumber(32)
|
||||
ApplicationData_PasswordlessNotification ensurePasswordlessNotification() =>
|
||||
$_ensure(27);
|
||||
}
|
||||
|
||||
class Response_PreKey extends $pb.GeneratedMessage {
|
||||
|
|
|
|||
|
|
@ -152,6 +152,33 @@ const Handshake$json = {
|
|||
'9': 0,
|
||||
'10': 'getUseridByUsername'
|
||||
},
|
||||
{
|
||||
'1': 'get_server_key_for_passwordless_recovery',
|
||||
'3': 8,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.client_to_server.Handshake.GetServerKeyForPasswordLessRecovery',
|
||||
'9': 0,
|
||||
'10': 'getServerKeyForPasswordlessRecovery'
|
||||
},
|
||||
{
|
||||
'1': 'register_passwordless_notification',
|
||||
'3': 9,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.client_to_server.Handshake.RegisterPasswordlessNotification',
|
||||
'9': 0,
|
||||
'10': 'registerPasswordlessNotification'
|
||||
},
|
||||
{
|
||||
'1': 'check_for_passwordless_notification',
|
||||
'3': 10,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.client_to_server.Handshake.CheckForPasswordlessNotification',
|
||||
'9': 0,
|
||||
'10': 'checkForPasswordlessNotification'
|
||||
},
|
||||
],
|
||||
'3': [
|
||||
Handshake_RequestPOW$json,
|
||||
|
|
@ -160,7 +187,10 @@ const Handshake$json = {
|
|||
Handshake_GetUserIdByUsername$json,
|
||||
Handshake_GetAuthToken$json,
|
||||
Handshake_Authenticate$json,
|
||||
Handshake_AuthenticateWithLoginToken$json
|
||||
Handshake_AuthenticateWithLoginToken$json,
|
||||
Handshake_GetServerKeyForPasswordLessRecovery$json,
|
||||
Handshake_RegisterPasswordlessNotification$json,
|
||||
Handshake_CheckForPasswordlessNotification$json
|
||||
],
|
||||
'8': [
|
||||
{'1': 'Handshake'},
|
||||
|
|
@ -303,6 +333,95 @@ const Handshake_AuthenticateWithLoginToken$json = {
|
|||
],
|
||||
};
|
||||
|
||||
@$core.Deprecated('Use handshakeDescriptor instead')
|
||||
const Handshake_GetServerKeyForPasswordLessRecovery$json = {
|
||||
'1': 'GetServerKeyForPasswordLessRecovery',
|
||||
'2': [
|
||||
{'1': 'user_id', '3': 1, '4': 1, '5': 3, '10': 'userId'},
|
||||
{
|
||||
'1': 'encrypted_server_key_none',
|
||||
'3': 2,
|
||||
'4': 1,
|
||||
'5': 12,
|
||||
'10': 'encryptedServerKeyNone'
|
||||
},
|
||||
{
|
||||
'1': 'pin_unlock_token',
|
||||
'3': 3,
|
||||
'4': 1,
|
||||
'5': 12,
|
||||
'9': 0,
|
||||
'10': 'pinUnlockToken',
|
||||
'17': true
|
||||
},
|
||||
{
|
||||
'1': 'pin_protection_key',
|
||||
'3': 4,
|
||||
'4': 1,
|
||||
'5': 12,
|
||||
'9': 1,
|
||||
'10': 'pinProtectionKey',
|
||||
'17': true
|
||||
},
|
||||
{'1': 'email', '3': 5, '4': 1, '5': 9, '9': 2, '10': 'email', '17': true},
|
||||
],
|
||||
'8': [
|
||||
{'1': '_pin_unlock_token'},
|
||||
{'1': '_pin_protection_key'},
|
||||
{'1': '_email'},
|
||||
],
|
||||
};
|
||||
|
||||
@$core.Deprecated('Use handshakeDescriptor instead')
|
||||
const Handshake_RegisterPasswordlessNotification$json = {
|
||||
'1': 'RegisterPasswordlessNotification',
|
||||
'2': [
|
||||
{'1': 'notification_id', '3': 1, '4': 1, '5': 9, '10': 'notificationId'},
|
||||
{
|
||||
'1': 'download_auth_token',
|
||||
'3': 2,
|
||||
'4': 1,
|
||||
'5': 12,
|
||||
'10': 'downloadAuthToken'
|
||||
},
|
||||
{'1': 'lang_code', '3': 3, '4': 1, '5': 9, '10': 'langCode'},
|
||||
{
|
||||
'1': 'google_fcm',
|
||||
'3': 4,
|
||||
'4': 1,
|
||||
'5': 9,
|
||||
'9': 0,
|
||||
'10': 'googleFcm',
|
||||
'17': true
|
||||
},
|
||||
],
|
||||
'8': [
|
||||
{'1': '_google_fcm'},
|
||||
],
|
||||
};
|
||||
|
||||
@$core.Deprecated('Use handshakeDescriptor instead')
|
||||
const Handshake_CheckForPasswordlessNotification$json = {
|
||||
'1': 'CheckForPasswordlessNotification',
|
||||
'2': [
|
||||
{'1': 'notification_id', '3': 1, '4': 1, '5': 9, '10': 'notificationId'},
|
||||
{
|
||||
'1': 'download_auth_token',
|
||||
'3': 2,
|
||||
'4': 1,
|
||||
'5': 12,
|
||||
'10': 'downloadAuthToken'
|
||||
},
|
||||
{
|
||||
'1': 'already_received_message_ids',
|
||||
'3': 3,
|
||||
'4': 3,
|
||||
'5': 3,
|
||||
'10': 'alreadyReceivedMessageIds'
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `Handshake`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List handshakeDescriptor = $convert.base64Decode(
|
||||
'CglIYW5kc2hha2USQgoIcmVnaXN0ZXIYASABKAsyJC5jbGllbnRfdG9fc2VydmVyLkhhbmRzaG'
|
||||
|
|
@ -316,26 +435,47 @@ final $typed_data.Uint8List handshakeDescriptor = $convert.base64Decode(
|
|||
'MjYuY2xpZW50X3RvX3NlcnZlci5IYW5kc2hha2UuQXV0aGVudGljYXRlV2l0aExvZ2luVG9rZW'
|
||||
'5IAFIaYXV0aGVudGljYXRlV2l0aExvZ2luVG9rZW4SZgoWZ2V0X3VzZXJpZF9ieV91c2VybmFt'
|
||||
'ZRgHIAEoCzIvLmNsaWVudF90b19zZXJ2ZXIuSGFuZHNoYWtlLkdldFVzZXJJZEJ5VXNlcm5hbW'
|
||||
'VIAFITZ2V0VXNlcmlkQnlVc2VybmFtZRoMCgpSZXF1ZXN0UE9XGsoDCghSZWdpc3RlchIaCgh1'
|
||||
'c2VybmFtZRgBIAEoCVIIdXNlcm5hbWUSJAoLaW52aXRlX2NvZGUYAiABKAlIAFIKaW52aXRlQ2'
|
||||
'9kZYgBARIuChNwdWJsaWNfaWRlbnRpdHlfa2V5GAMgASgMUhFwdWJsaWNJZGVudGl0eUtleRIj'
|
||||
'Cg1zaWduZWRfcHJla2V5GAQgASgMUgxzaWduZWRQcmVrZXkSNgoXc2lnbmVkX3ByZWtleV9zaW'
|
||||
'duYXR1cmUYBSABKAxSFXNpZ25lZFByZWtleVNpZ25hdHVyZRIoChBzaWduZWRfcHJla2V5X2lk'
|
||||
'GAYgASgDUg5zaWduZWRQcmVrZXlJZBInCg9yZWdpc3RyYXRpb25faWQYByABKANSDnJlZ2lzdH'
|
||||
'JhdGlvbklkEhUKBmlzX2lvcxgIIAEoCFIFaXNJb3MSGwoJbGFuZ19jb2RlGAkgASgJUghsYW5n'
|
||||
'Q29kZRIiCg1wcm9vZl9vZl93b3JrGAogASgDUgtwcm9vZk9mV29yaxIkCgtsb2dpbl90b2tlbh'
|
||||
'gLIAEoDEgBUgpsb2dpblRva2VuiAEBQg4KDF9pbnZpdGVfY29kZUIOCgxfbG9naW5fdG9rZW4a'
|
||||
'EgoQR2V0QXV0aENoYWxsZW5nZRoxChNHZXRVc2VySWRCeVVzZXJuYW1lEhoKCHVzZXJuYW1lGA'
|
||||
'EgASgJUgh1c2VybmFtZRpDCgxHZXRBdXRoVG9rZW4SFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklk'
|
||||
'EhoKCHJlc3BvbnNlGAIgASgMUghyZXNwb25zZRroAQoMQXV0aGVudGljYXRlEhcKB3VzZXJfaW'
|
||||
'QYASABKANSBnVzZXJJZBIdCgphdXRoX3Rva2VuGAIgASgMUglhdXRoVG9rZW4SJAoLYXBwX3Zl'
|
||||
'cnNpb24YAyABKAlIAFIKYXBwVmVyc2lvbogBARIgCglkZXZpY2VfaWQYBCABKANIAVIIZGV2aW'
|
||||
'NlSWSIAQESKAoNaW5fYmFja2dyb3VuZBgFIAEoCEgCUgxpbkJhY2tncm91bmSIAQFCDgoMX2Fw'
|
||||
'cF92ZXJzaW9uQgwKCl9kZXZpY2VfaWRCEAoOX2luX2JhY2tncm91bmQaxgEKGkF1dGhlbnRpY2'
|
||||
'F0ZVdpdGhMb2dpblRva2VuEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBIsChJzZWNyZXRfbG9n'
|
||||
'aW5fdG9rZW4YAiABKAxSEHNlY3JldExvZ2luVG9rZW4SHwoLYXBwX3ZlcnNpb24YAyABKAlSCm'
|
||||
'FwcFZlcnNpb24SGwoJZGV2aWNlX2lkGAQgASgDUghkZXZpY2VJZBIjCg1pbl9iYWNrZ3JvdW5k'
|
||||
'GAUgASgIUgxpbkJhY2tncm91bmRCCwoJSGFuZHNoYWtl');
|
||||
'VIAFITZ2V0VXNlcmlkQnlVc2VybmFtZRKYAQooZ2V0X3NlcnZlcl9rZXlfZm9yX3Bhc3N3b3Jk'
|
||||
'bGVzc19yZWNvdmVyeRgIIAEoCzI/LmNsaWVudF90b19zZXJ2ZXIuSGFuZHNoYWtlLkdldFNlcn'
|
||||
'ZlcktleUZvclBhc3N3b3JkTGVzc1JlY292ZXJ5SABSI2dldFNlcnZlcktleUZvclBhc3N3b3Jk'
|
||||
'bGVzc1JlY292ZXJ5EowBCiJyZWdpc3Rlcl9wYXNzd29yZGxlc3Nfbm90aWZpY2F0aW9uGAkgAS'
|
||||
'gLMjwuY2xpZW50X3RvX3NlcnZlci5IYW5kc2hha2UuUmVnaXN0ZXJQYXNzd29yZGxlc3NOb3Rp'
|
||||
'ZmljYXRpb25IAFIgcmVnaXN0ZXJQYXNzd29yZGxlc3NOb3RpZmljYXRpb24SjQEKI2NoZWNrX2'
|
||||
'Zvcl9wYXNzd29yZGxlc3Nfbm90aWZpY2F0aW9uGAogASgLMjwuY2xpZW50X3RvX3NlcnZlci5I'
|
||||
'YW5kc2hha2UuQ2hlY2tGb3JQYXNzd29yZGxlc3NOb3RpZmljYXRpb25IAFIgY2hlY2tGb3JQYX'
|
||||
'Nzd29yZGxlc3NOb3RpZmljYXRpb24aDAoKUmVxdWVzdFBPVxrKAwoIUmVnaXN0ZXISGgoIdXNl'
|
||||
'cm5hbWUYASABKAlSCHVzZXJuYW1lEiQKC2ludml0ZV9jb2RlGAIgASgJSABSCmludml0ZUNvZG'
|
||||
'WIAQESLgoTcHVibGljX2lkZW50aXR5X2tleRgDIAEoDFIRcHVibGljSWRlbnRpdHlLZXkSIwoN'
|
||||
'c2lnbmVkX3ByZWtleRgEIAEoDFIMc2lnbmVkUHJla2V5EjYKF3NpZ25lZF9wcmVrZXlfc2lnbm'
|
||||
'F0dXJlGAUgASgMUhVzaWduZWRQcmVrZXlTaWduYXR1cmUSKAoQc2lnbmVkX3ByZWtleV9pZBgG'
|
||||
'IAEoA1IOc2lnbmVkUHJla2V5SWQSJwoPcmVnaXN0cmF0aW9uX2lkGAcgASgDUg5yZWdpc3RyYX'
|
||||
'Rpb25JZBIVCgZpc19pb3MYCCABKAhSBWlzSW9zEhsKCWxhbmdfY29kZRgJIAEoCVIIbGFuZ0Nv'
|
||||
'ZGUSIgoNcHJvb2Zfb2Zfd29yaxgKIAEoA1ILcHJvb2ZPZldvcmsSJAoLbG9naW5fdG9rZW4YCy'
|
||||
'ABKAxIAVIKbG9naW5Ub2tlbogBAUIOCgxfaW52aXRlX2NvZGVCDgoMX2xvZ2luX3Rva2VuGhIK'
|
||||
'EEdldEF1dGhDaGFsbGVuZ2UaMQoTR2V0VXNlcklkQnlVc2VybmFtZRIaCgh1c2VybmFtZRgBIA'
|
||||
'EoCVIIdXNlcm5hbWUaQwoMR2V0QXV0aFRva2VuEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBIa'
|
||||
'CghyZXNwb25zZRgCIAEoDFIIcmVzcG9uc2Ua6AEKDEF1dGhlbnRpY2F0ZRIXCgd1c2VyX2lkGA'
|
||||
'EgASgDUgZ1c2VySWQSHQoKYXV0aF90b2tlbhgCIAEoDFIJYXV0aFRva2VuEiQKC2FwcF92ZXJz'
|
||||
'aW9uGAMgASgJSABSCmFwcFZlcnNpb26IAQESIAoJZGV2aWNlX2lkGAQgASgDSAFSCGRldmljZU'
|
||||
'lkiAEBEigKDWluX2JhY2tncm91bmQYBSABKAhIAlIMaW5CYWNrZ3JvdW5kiAEBQg4KDF9hcHBf'
|
||||
'dmVyc2lvbkIMCgpfZGV2aWNlX2lkQhAKDl9pbl9iYWNrZ3JvdW5kGsYBChpBdXRoZW50aWNhdG'
|
||||
'VXaXRoTG9naW5Ub2tlbhIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQSLAoSc2VjcmV0X2xvZ2lu'
|
||||
'X3Rva2VuGAIgASgMUhBzZWNyZXRMb2dpblRva2VuEh8KC2FwcF92ZXJzaW9uGAMgASgJUgphcH'
|
||||
'BWZXJzaW9uEhsKCWRldmljZV9pZBgEIAEoA1IIZGV2aWNlSWQSIwoNaW5fYmFja2dyb3VuZBgF'
|
||||
'IAEoCFIMaW5CYWNrZ3JvdW5kGqwCCiNHZXRTZXJ2ZXJLZXlGb3JQYXNzd29yZExlc3NSZWNvdm'
|
||||
'VyeRIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQSOQoZZW5jcnlwdGVkX3NlcnZlcl9rZXlfbm9u'
|
||||
'ZRgCIAEoDFIWZW5jcnlwdGVkU2VydmVyS2V5Tm9uZRItChBwaW5fdW5sb2NrX3Rva2VuGAMgAS'
|
||||
'gMSABSDnBpblVubG9ja1Rva2VuiAEBEjEKEnBpbl9wcm90ZWN0aW9uX2tleRgEIAEoDEgBUhBw'
|
||||
'aW5Qcm90ZWN0aW9uS2V5iAEBEhkKBWVtYWlsGAUgASgJSAJSBWVtYWlsiAEBQhMKEV9waW5fdW'
|
||||
'5sb2NrX3Rva2VuQhUKE19waW5fcHJvdGVjdGlvbl9rZXlCCAoGX2VtYWlsGssBCiBSZWdpc3Rl'
|
||||
'clBhc3N3b3JkbGVzc05vdGlmaWNhdGlvbhInCg9ub3RpZmljYXRpb25faWQYASABKAlSDm5vdG'
|
||||
'lmaWNhdGlvbklkEi4KE2Rvd25sb2FkX2F1dGhfdG9rZW4YAiABKAxSEWRvd25sb2FkQXV0aFRv'
|
||||
'a2VuEhsKCWxhbmdfY29kZRgDIAEoCVIIbGFuZ0NvZGUSIgoKZ29vZ2xlX2ZjbRgEIAEoCUgAUg'
|
||||
'lnb29nbGVGY22IAQFCDQoLX2dvb2dsZV9mY20avAEKIENoZWNrRm9yUGFzc3dvcmRsZXNzTm90'
|
||||
'aWZpY2F0aW9uEicKD25vdGlmaWNhdGlvbl9pZBgBIAEoCVIObm90aWZpY2F0aW9uSWQSLgoTZG'
|
||||
'93bmxvYWRfYXV0aF90b2tlbhgCIAEoDFIRZG93bmxvYWRBdXRoVG9rZW4SPwocYWxyZWFkeV9y'
|
||||
'ZWNlaXZlZF9tZXNzYWdlX2lkcxgDIAMoA1IZYWxyZWFkeVJlY2VpdmVkTWVzc2FnZUlkc0ILCg'
|
||||
'lIYW5kc2hha2U=');
|
||||
|
||||
@$core.Deprecated('Use applicationDataDescriptor instead')
|
||||
const ApplicationData$json = {
|
||||
|
|
@ -575,6 +715,24 @@ const ApplicationData$json = {
|
|||
'9': 0,
|
||||
'10': 'setLoginToken'
|
||||
},
|
||||
{
|
||||
'1': 'register_passwordless_recovery',
|
||||
'3': 31,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.client_to_server.ApplicationData.RegisterPasswordLessRecovery',
|
||||
'9': 0,
|
||||
'10': 'registerPasswordlessRecovery'
|
||||
},
|
||||
{
|
||||
'1': 'passwordless_notification',
|
||||
'3': 32,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.client_to_server.ApplicationData.PasswordlessNotification',
|
||||
'9': 0,
|
||||
'10': 'passwordlessNotification'
|
||||
},
|
||||
],
|
||||
'3': [
|
||||
ApplicationData_TextMessage$json,
|
||||
|
|
@ -596,7 +754,9 @@ const ApplicationData$json = {
|
|||
ApplicationData_DeleteAccount$json,
|
||||
ApplicationData_AddAdditionalUser$json,
|
||||
ApplicationData_SetLoginToken$json,
|
||||
ApplicationData_Deprecated$json
|
||||
ApplicationData_Deprecated$json,
|
||||
ApplicationData_RegisterPasswordLessRecovery$json,
|
||||
ApplicationData_PasswordlessNotification$json
|
||||
],
|
||||
'8': [
|
||||
{'1': 'ApplicationData'},
|
||||
|
|
@ -775,6 +935,47 @@ const ApplicationData_Deprecated$json = {
|
|||
'1': 'Deprecated',
|
||||
};
|
||||
|
||||
@$core.Deprecated('Use applicationDataDescriptor instead')
|
||||
const ApplicationData_RegisterPasswordLessRecovery$json = {
|
||||
'1': 'RegisterPasswordLessRecovery',
|
||||
'2': [
|
||||
{
|
||||
'1': 'encryptedServerKey',
|
||||
'3': 1,
|
||||
'4': 1,
|
||||
'5': 12,
|
||||
'10': 'encryptedServerKey'
|
||||
},
|
||||
{
|
||||
'1': 'pinUnlockToken',
|
||||
'3': 2,
|
||||
'4': 1,
|
||||
'5': 12,
|
||||
'9': 0,
|
||||
'10': 'pinUnlockToken',
|
||||
'17': true
|
||||
},
|
||||
],
|
||||
'8': [
|
||||
{'1': '_pinUnlockToken'},
|
||||
],
|
||||
};
|
||||
|
||||
@$core.Deprecated('Use applicationDataDescriptor instead')
|
||||
const ApplicationData_PasswordlessNotification$json = {
|
||||
'1': 'PasswordlessNotification',
|
||||
'2': [
|
||||
{'1': 'notification_id', '3': 1, '4': 1, '5': 9, '10': 'notificationId'},
|
||||
{
|
||||
'1': 'encrypted_message',
|
||||
'3': 2,
|
||||
'4': 1,
|
||||
'5': 12,
|
||||
'10': 'encryptedMessage'
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `ApplicationData`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List applicationDataDescriptor = $convert.base64Decode(
|
||||
'Cg9BcHBsaWNhdGlvbkRhdGESUQoLdGV4dE1lc3NhZ2UYASABKAsyLS5jbGllbnRfdG9fc2Vydm'
|
||||
|
|
@ -820,27 +1021,36 @@ final $typed_data.Uint8List applicationDataDescriptor = $convert.base64Decode(
|
|||
'dmVBZGRpdGlvbmFsVXNlchJjChFhZGRBZGRpdGlvbmFsVXNlchgdIAEoCzIzLmNsaWVudF90b1'
|
||||
'9zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkFkZEFkZGl0aW9uYWxVc2VySABSEWFkZEFkZGl0aW9u'
|
||||
'YWxVc2VyElkKD3NldF9sb2dpbl90b2tlbhgeIAEoCzIvLmNsaWVudF90b19zZXJ2ZXIuQXBwbG'
|
||||
'ljYXRpb25EYXRhLlNldExvZ2luVG9rZW5IAFINc2V0TG9naW5Ub2tlbhpqCgtUZXh0TWVzc2Fn'
|
||||
'ZRIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQSEgoEYm9keRgDIAEoDFIEYm9keRIgCglwdXNoX2'
|
||||
'RhdGEYBCABKAxIAFIIcHVzaERhdGGIAQFCDAoKX3B1c2hfZGF0YRovChFHZXRVc2VyQnlVc2Vy'
|
||||
'bmFtZRIaCgh1c2VybmFtZRgBIAEoCVIIdXNlcm5hbWUaLAoOQ2hhbmdlVXNlcm5hbWUSGgoIdX'
|
||||
'Nlcm5hbWUYASABKAlSCHVzZXJuYW1lGjUKFFVwZGF0ZUdvb2dsZUZjbVRva2VuEh0KCmdvb2ds'
|
||||
'ZV9mY20YASABKAlSCWdvb2dsZUZjbRomCgtHZXRVc2VyQnlJZBIXCgd1c2VyX2lkGAEgASgDUg'
|
||||
'Z1c2VySWQaEwoRR2V0QXZhaWxhYmxlUGxhbnMaFwoVR2V0QWRkQWNjb3VudHNJbnZpdGVzGhUK'
|
||||
'E0dldEN1cnJlbnRQbGFuSW5mb3MaLwoUUmVtb3ZlQWRkaXRpb25hbFVzZXISFwoHdXNlcl9pZB'
|
||||
'gBIAEoA1IGdXNlcklkGi0KEkdldFByZWtleXNCeVVzZXJJZBIXCgd1c2VyX2lkGAEgASgDUgZ1'
|
||||
'c2VySWQaMgoXR2V0U2lnbmVkUHJlS2V5QnlVc2VySWQSFwoHdXNlcl9pZBgBIAEoA1IGdXNlck'
|
||||
'lkGpsBChJVcGRhdGVTaWduZWRQcmVLZXkSKAoQc2lnbmVkX3ByZWtleV9pZBgBIAEoA1IOc2ln'
|
||||
'bmVkUHJla2V5SWQSIwoNc2lnbmVkX3ByZWtleRgCIAEoDFIMc2lnbmVkUHJla2V5EjYKF3NpZ2'
|
||||
'5lZF9wcmVrZXlfc2lnbmF0dXJlGAMgASgMUhVzaWduZWRQcmVrZXlTaWduYXR1cmUaNQoMRG93'
|
||||
'bmxvYWREb25lEiUKDmRvd25sb2FkX3Rva2VuGAEgASgMUg1kb3dubG9hZFRva2VuGk4KClJlcG'
|
||||
'9ydFVzZXISKAoQcmVwb3J0ZWRfdXNlcl9pZBgBIAEoA1IOcmVwb3J0ZWRVc2VySWQSFgoGcmVh'
|
||||
'c29uGAIgASgJUgZyZWFzb24acQoLSVBBUHVyY2hhc2USHQoKcHJvZHVjdF9pZBgBIAEoCVIJcH'
|
||||
'JvZHVjdElkEhYKBnNvdXJjZRgCIAEoCVIGc291cmNlEisKEXZlcmlmaWNhdGlvbl9kYXRhGAMg'
|
||||
'ASgJUhB2ZXJpZmljYXRpb25EYXRhGg8KDUlQQUZvcmNlQ2hlY2saDwoNRGVsZXRlQWNjb3VudB'
|
||||
'osChFBZGRBZGRpdGlvbmFsVXNlchIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQaMAoNU2V0TG9n'
|
||||
'aW5Ub2tlbhIfCgtsb2dpbl90b2tlbhgBIAEoDFIKbG9naW5Ub2tlbhoMCgpEZXByZWNhdGVkQh'
|
||||
'EKD0FwcGxpY2F0aW9uRGF0YQ==');
|
||||
'ljYXRpb25EYXRhLlNldExvZ2luVG9rZW5IAFINc2V0TG9naW5Ub2tlbhKGAQoecmVnaXN0ZXJf'
|
||||
'cGFzc3dvcmRsZXNzX3JlY292ZXJ5GB8gASgLMj4uY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdG'
|
||||
'lvbkRhdGEuUmVnaXN0ZXJQYXNzd29yZExlc3NSZWNvdmVyeUgAUhxyZWdpc3RlclBhc3N3b3Jk'
|
||||
'bGVzc1JlY292ZXJ5EnkKGXBhc3N3b3JkbGVzc19ub3RpZmljYXRpb24YICABKAsyOi5jbGllbn'
|
||||
'RfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5QYXNzd29yZGxlc3NOb3RpZmljYXRpb25IAFIY'
|
||||
'cGFzc3dvcmRsZXNzTm90aWZpY2F0aW9uGmoKC1RleHRNZXNzYWdlEhcKB3VzZXJfaWQYASABKA'
|
||||
'NSBnVzZXJJZBISCgRib2R5GAMgASgMUgRib2R5EiAKCXB1c2hfZGF0YRgEIAEoDEgAUghwdXNo'
|
||||
'RGF0YYgBAUIMCgpfcHVzaF9kYXRhGi8KEUdldFVzZXJCeVVzZXJuYW1lEhoKCHVzZXJuYW1lGA'
|
||||
'EgASgJUgh1c2VybmFtZRosCg5DaGFuZ2VVc2VybmFtZRIaCgh1c2VybmFtZRgBIAEoCVIIdXNl'
|
||||
'cm5hbWUaNQoUVXBkYXRlR29vZ2xlRmNtVG9rZW4SHQoKZ29vZ2xlX2ZjbRgBIAEoCVIJZ29vZ2'
|
||||
'xlRmNtGiYKC0dldFVzZXJCeUlkEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBoTChFHZXRBdmFp'
|
||||
'bGFibGVQbGFucxoXChVHZXRBZGRBY2NvdW50c0ludml0ZXMaFQoTR2V0Q3VycmVudFBsYW5Jbm'
|
||||
'ZvcxovChRSZW1vdmVBZGRpdGlvbmFsVXNlchIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQaLQoS'
|
||||
'R2V0UHJla2V5c0J5VXNlcklkEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBoyChdHZXRTaWduZW'
|
||||
'RQcmVLZXlCeVVzZXJJZBIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQamwEKElVwZGF0ZVNpZ25l'
|
||||
'ZFByZUtleRIoChBzaWduZWRfcHJla2V5X2lkGAEgASgDUg5zaWduZWRQcmVrZXlJZBIjCg1zaW'
|
||||
'duZWRfcHJla2V5GAIgASgMUgxzaWduZWRQcmVrZXkSNgoXc2lnbmVkX3ByZWtleV9zaWduYXR1'
|
||||
'cmUYAyABKAxSFXNpZ25lZFByZWtleVNpZ25hdHVyZRo1CgxEb3dubG9hZERvbmUSJQoOZG93bm'
|
||||
'xvYWRfdG9rZW4YASABKAxSDWRvd25sb2FkVG9rZW4aTgoKUmVwb3J0VXNlchIoChByZXBvcnRl'
|
||||
'ZF91c2VyX2lkGAEgASgDUg5yZXBvcnRlZFVzZXJJZBIWCgZyZWFzb24YAiABKAlSBnJlYXNvbh'
|
||||
'pxCgtJUEFQdXJjaGFzZRIdCgpwcm9kdWN0X2lkGAEgASgJUglwcm9kdWN0SWQSFgoGc291cmNl'
|
||||
'GAIgASgJUgZzb3VyY2USKwoRdmVyaWZpY2F0aW9uX2RhdGEYAyABKAlSEHZlcmlmaWNhdGlvbk'
|
||||
'RhdGEaDwoNSVBBRm9yY2VDaGVjaxoPCg1EZWxldGVBY2NvdW50GiwKEUFkZEFkZGl0aW9uYWxV'
|
||||
'c2VyEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBowCg1TZXRMb2dpblRva2VuEh8KC2xvZ2luX3'
|
||||
'Rva2VuGAEgASgMUgpsb2dpblRva2VuGgwKCkRlcHJlY2F0ZWQajgEKHFJlZ2lzdGVyUGFzc3dv'
|
||||
'cmRMZXNzUmVjb3ZlcnkSLgoSZW5jcnlwdGVkU2VydmVyS2V5GAEgASgMUhJlbmNyeXB0ZWRTZX'
|
||||
'J2ZXJLZXkSKwoOcGluVW5sb2NrVG9rZW4YAiABKAxIAFIOcGluVW5sb2NrVG9rZW6IAQFCEQoP'
|
||||
'X3BpblVubG9ja1Rva2VuGnAKGFBhc3N3b3JkbGVzc05vdGlmaWNhdGlvbhInCg9ub3RpZmljYX'
|
||||
'Rpb25faWQYASABKAlSDm5vdGlmaWNhdGlvbklkEisKEWVuY3J5cHRlZF9tZXNzYWdlGAIgASgM'
|
||||
'UhBlbmNyeXB0ZWRNZXNzYWdlQhEKD0FwcGxpY2F0aW9uRGF0YQ==');
|
||||
|
||||
@$core.Deprecated('Use responseDescriptor instead')
|
||||
const Response$json = {
|
||||
|
|
|
|||
|
|
@ -93,6 +93,10 @@ class ErrorCode extends $pb.ProtobufEnum {
|
|||
ErrorCode._(1035, _omitEnumNames ? '' : 'UserIsNotInFreePlan');
|
||||
static const ErrorCode ForegroundSessionConnected =
|
||||
ErrorCode._(1036, _omitEnumNames ? '' : 'ForegroundSessionConnected');
|
||||
static const ErrorCode NoRecoveryData =
|
||||
ErrorCode._(1037, _omitEnumNames ? '' : 'NoRecoveryData');
|
||||
static const ErrorCode InvalidRecoveryData =
|
||||
ErrorCode._(1038, _omitEnumNames ? '' : 'InvalidRecoveryData');
|
||||
|
||||
static const $core.List<ErrorCode> values = <ErrorCode>[
|
||||
Unknown,
|
||||
|
|
@ -134,6 +138,8 @@ class ErrorCode extends $pb.ProtobufEnum {
|
|||
IPAPaymentExpired,
|
||||
UserIsNotInFreePlan,
|
||||
ForegroundSessionConnected,
|
||||
NoRecoveryData,
|
||||
InvalidRecoveryData,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, ErrorCode> _byValue =
|
||||
|
|
|
|||
|
|
@ -58,6 +58,8 @@ const ErrorCode$json = {
|
|||
{'1': 'IPAPaymentExpired', '2': 1034},
|
||||
{'1': 'UserIsNotInFreePlan', '2': 1035},
|
||||
{'1': 'ForegroundSessionConnected', '2': 1036},
|
||||
{'1': 'NoRecoveryData', '2': 1037},
|
||||
{'1': 'InvalidRecoveryData', '2': 1038},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
@ -80,4 +82,5 @@ final $typed_data.Uint8List errorCodeDescriptor = $convert.base64Decode(
|
|||
'EIUIEhcKEkFwcFZlcnNpb25PdXRkYXRlZBCGCBIYChNOZXdEZXZpY2VSZWdpc3RlcmVkEIcIEh'
|
||||
'cKEkludmFsaWRQcm9vZk9mV29yaxCICBIZChRSZWdpc3RyYXRpb25EaXNhYmxlZBCJCBIWChFJ'
|
||||
'UEFQYXltZW50RXhwaXJlZBCKCBIYChNVc2VySXNOb3RJbkZyZWVQbGFuEIsIEh8KGkZvcmVncm'
|
||||
'91bmRTZXNzaW9uQ29ubmVjdGVkEIwI');
|
||||
'91bmRTZXNzaW9uQ29ubmVjdGVkEIwIEhMKDk5vUmVjb3ZlcnlEYXRhEI0IEhgKE0ludmFsaWRS'
|
||||
'ZWNvdmVyeURhdGEQjgg=');
|
||||
|
|
|
|||
|
|
@ -1505,6 +1505,136 @@ class Response_ProofOfWork extends $pb.GeneratedMessage {
|
|||
void clearDifficulty() => $_clearField(2);
|
||||
}
|
||||
|
||||
class Response_PasswordlessNotificationMessage extends $pb.GeneratedMessage {
|
||||
factory Response_PasswordlessNotificationMessage({
|
||||
$fixnum.Int64? id,
|
||||
$core.List<$core.int>? encryptedMessage,
|
||||
}) {
|
||||
final result = create();
|
||||
if (id != null) result.id = id;
|
||||
if (encryptedMessage != null) result.encryptedMessage = encryptedMessage;
|
||||
return result;
|
||||
}
|
||||
|
||||
Response_PasswordlessNotificationMessage._();
|
||||
|
||||
factory Response_PasswordlessNotificationMessage.fromBuffer(
|
||||
$core.List<$core.int> data,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(data, registry);
|
||||
factory Response_PasswordlessNotificationMessage.fromJson($core.String json,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(json, registry);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
_omitMessageNames ? '' : 'Response.PasswordlessNotificationMessage',
|
||||
package:
|
||||
const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'),
|
||||
createEmptyInstance: create)
|
||||
..aInt64(1, _omitFieldNames ? '' : 'id')
|
||||
..a<$core.List<$core.int>>(
|
||||
2, _omitFieldNames ? '' : 'encryptedMessage', $pb.PbFieldType.OY)
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
Response_PasswordlessNotificationMessage clone() => deepCopy();
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
Response_PasswordlessNotificationMessage copyWith(
|
||||
void Function(Response_PasswordlessNotificationMessage) updates) =>
|
||||
super.copyWith((message) =>
|
||||
updates(message as Response_PasswordlessNotificationMessage))
|
||||
as Response_PasswordlessNotificationMessage;
|
||||
|
||||
@$core.override
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static Response_PasswordlessNotificationMessage create() =>
|
||||
Response_PasswordlessNotificationMessage._();
|
||||
@$core.override
|
||||
Response_PasswordlessNotificationMessage createEmptyInstance() => create();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static Response_PasswordlessNotificationMessage getDefault() =>
|
||||
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<
|
||||
Response_PasswordlessNotificationMessage>(create);
|
||||
static Response_PasswordlessNotificationMessage? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$fixnum.Int64 get id => $_getI64(0);
|
||||
@$pb.TagNumber(1)
|
||||
set id($fixnum.Int64 value) => $_setInt64(0, value);
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasId() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearId() => $_clearField(1);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$core.List<$core.int> get encryptedMessage => $_getN(1);
|
||||
@$pb.TagNumber(2)
|
||||
set encryptedMessage($core.List<$core.int> value) => $_setBytes(1, value);
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasEncryptedMessage() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearEncryptedMessage() => $_clearField(2);
|
||||
}
|
||||
|
||||
class Response_PasswordlessNotificationMessages extends $pb.GeneratedMessage {
|
||||
factory Response_PasswordlessNotificationMessages({
|
||||
$core.Iterable<Response_PasswordlessNotificationMessage>? messages,
|
||||
}) {
|
||||
final result = create();
|
||||
if (messages != null) result.messages.addAll(messages);
|
||||
return result;
|
||||
}
|
||||
|
||||
Response_PasswordlessNotificationMessages._();
|
||||
|
||||
factory Response_PasswordlessNotificationMessages.fromBuffer(
|
||||
$core.List<$core.int> data,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(data, registry);
|
||||
factory Response_PasswordlessNotificationMessages.fromJson($core.String json,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(json, registry);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
_omitMessageNames ? '' : 'Response.PasswordlessNotificationMessages',
|
||||
package:
|
||||
const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'),
|
||||
createEmptyInstance: create)
|
||||
..pPM<Response_PasswordlessNotificationMessage>(
|
||||
1, _omitFieldNames ? '' : 'messages',
|
||||
subBuilder: Response_PasswordlessNotificationMessage.create)
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
Response_PasswordlessNotificationMessages clone() => deepCopy();
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
Response_PasswordlessNotificationMessages copyWith(
|
||||
void Function(Response_PasswordlessNotificationMessages) updates) =>
|
||||
super.copyWith((message) =>
|
||||
updates(message as Response_PasswordlessNotificationMessages))
|
||||
as Response_PasswordlessNotificationMessages;
|
||||
|
||||
@$core.override
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static Response_PasswordlessNotificationMessages create() =>
|
||||
Response_PasswordlessNotificationMessages._();
|
||||
@$core.override
|
||||
Response_PasswordlessNotificationMessages createEmptyInstance() => create();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static Response_PasswordlessNotificationMessages getDefault() =>
|
||||
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<
|
||||
Response_PasswordlessNotificationMessages>(create);
|
||||
static Response_PasswordlessNotificationMessages? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$pb.PbList<Response_PasswordlessNotificationMessage> get messages =>
|
||||
$_getList(0);
|
||||
}
|
||||
|
||||
enum Response_Ok_Ok {
|
||||
none,
|
||||
userid,
|
||||
|
|
@ -1521,6 +1651,8 @@ enum Response_Ok_Ok {
|
|||
downloadtokens,
|
||||
signedprekey,
|
||||
proofOfWork,
|
||||
passwordlessRecoveryServerKey,
|
||||
passwordlessNotificationMessages,
|
||||
notSet
|
||||
}
|
||||
|
||||
|
|
@ -1541,6 +1673,8 @@ class Response_Ok extends $pb.GeneratedMessage {
|
|||
Response_DownloadTokens? downloadtokens,
|
||||
Response_SignedPreKey? signedprekey,
|
||||
Response_ProofOfWork? proofOfWork,
|
||||
$core.List<$core.int>? passwordlessRecoveryServerKey,
|
||||
Response_PasswordlessNotificationMessages? passwordlessNotificationMessages,
|
||||
}) {
|
||||
final result = create();
|
||||
if (none != null) result.none = none;
|
||||
|
|
@ -1559,6 +1693,11 @@ class Response_Ok extends $pb.GeneratedMessage {
|
|||
if (downloadtokens != null) result.downloadtokens = downloadtokens;
|
||||
if (signedprekey != null) result.signedprekey = signedprekey;
|
||||
if (proofOfWork != null) result.proofOfWork = proofOfWork;
|
||||
if (passwordlessRecoveryServerKey != null)
|
||||
result.passwordlessRecoveryServerKey = passwordlessRecoveryServerKey;
|
||||
if (passwordlessNotificationMessages != null)
|
||||
result.passwordlessNotificationMessages =
|
||||
passwordlessNotificationMessages;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -1587,6 +1726,8 @@ class Response_Ok extends $pb.GeneratedMessage {
|
|||
13: Response_Ok_Ok.downloadtokens,
|
||||
14: Response_Ok_Ok.signedprekey,
|
||||
15: Response_Ok_Ok.proofOfWork,
|
||||
16: Response_Ok_Ok.passwordlessRecoveryServerKey,
|
||||
17: Response_Ok_Ok.passwordlessNotificationMessages,
|
||||
0: Response_Ok_Ok.notSet
|
||||
};
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
|
|
@ -1594,7 +1735,7 @@ class Response_Ok extends $pb.GeneratedMessage {
|
|||
package:
|
||||
const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'),
|
||||
createEmptyInstance: create)
|
||||
..oo(0, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15])
|
||||
..oo(0, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17])
|
||||
..aOB(1, _omitFieldNames ? '' : 'None', protoName: 'None')
|
||||
..aInt64(2, _omitFieldNames ? '' : 'userid')
|
||||
..a<$core.List<$core.int>>(
|
||||
|
|
@ -1624,6 +1765,13 @@ class Response_Ok extends $pb.GeneratedMessage {
|
|||
subBuilder: Response_SignedPreKey.create)
|
||||
..aOM<Response_ProofOfWork>(15, _omitFieldNames ? '' : 'proofOfWork',
|
||||
protoName: 'proofOfWork', subBuilder: Response_ProofOfWork.create)
|
||||
..a<$core.List<$core.int>>(
|
||||
16,
|
||||
_omitFieldNames ? '' : 'passwordlessRecoveryServerKey',
|
||||
$pb.PbFieldType.OY)
|
||||
..aOM<Response_PasswordlessNotificationMessages>(
|
||||
17, _omitFieldNames ? '' : 'passwordlessNotificationMessages',
|
||||
subBuilder: Response_PasswordlessNotificationMessages.create)
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
|
|
@ -1660,6 +1808,8 @@ class Response_Ok extends $pb.GeneratedMessage {
|
|||
@$pb.TagNumber(13)
|
||||
@$pb.TagNumber(14)
|
||||
@$pb.TagNumber(15)
|
||||
@$pb.TagNumber(16)
|
||||
@$pb.TagNumber(17)
|
||||
Response_Ok_Ok whichOk() => _Response_Ok_OkByTag[$_whichOneof(0)]!;
|
||||
@$pb.TagNumber(1)
|
||||
@$pb.TagNumber(2)
|
||||
|
|
@ -1676,6 +1826,8 @@ class Response_Ok extends $pb.GeneratedMessage {
|
|||
@$pb.TagNumber(13)
|
||||
@$pb.TagNumber(14)
|
||||
@$pb.TagNumber(15)
|
||||
@$pb.TagNumber(16)
|
||||
@$pb.TagNumber(17)
|
||||
void clearOk() => $_clearField($_whichOneof(0));
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
|
|
@ -1835,6 +1987,31 @@ class Response_Ok extends $pb.GeneratedMessage {
|
|||
void clearProofOfWork() => $_clearField(15);
|
||||
@$pb.TagNumber(15)
|
||||
Response_ProofOfWork ensureProofOfWork() => $_ensure(14);
|
||||
|
||||
@$pb.TagNumber(16)
|
||||
$core.List<$core.int> get passwordlessRecoveryServerKey => $_getN(15);
|
||||
@$pb.TagNumber(16)
|
||||
set passwordlessRecoveryServerKey($core.List<$core.int> value) =>
|
||||
$_setBytes(15, value);
|
||||
@$pb.TagNumber(16)
|
||||
$core.bool hasPasswordlessRecoveryServerKey() => $_has(15);
|
||||
@$pb.TagNumber(16)
|
||||
void clearPasswordlessRecoveryServerKey() => $_clearField(16);
|
||||
|
||||
@$pb.TagNumber(17)
|
||||
Response_PasswordlessNotificationMessages
|
||||
get passwordlessNotificationMessages => $_getN(16);
|
||||
@$pb.TagNumber(17)
|
||||
set passwordlessNotificationMessages(
|
||||
Response_PasswordlessNotificationMessages value) =>
|
||||
$_setField(17, value);
|
||||
@$pb.TagNumber(17)
|
||||
$core.bool hasPasswordlessNotificationMessages() => $_has(16);
|
||||
@$pb.TagNumber(17)
|
||||
void clearPasswordlessNotificationMessages() => $_clearField(17);
|
||||
@$pb.TagNumber(17)
|
||||
Response_PasswordlessNotificationMessages
|
||||
ensurePasswordlessNotificationMessages() => $_ensure(16);
|
||||
}
|
||||
|
||||
enum Response_Response { ok, error, notSet }
|
||||
|
|
|
|||
|
|
@ -176,6 +176,8 @@ const Response$json = {
|
|||
Response_UploadToken$json,
|
||||
Response_DownloadTokens$json,
|
||||
Response_ProofOfWork$json,
|
||||
Response_PasswordlessNotificationMessage$json,
|
||||
Response_PasswordlessNotificationMessages$json,
|
||||
Response_Ok$json
|
||||
],
|
||||
'8': [
|
||||
|
|
@ -509,6 +511,36 @@ const Response_ProofOfWork$json = {
|
|||
],
|
||||
};
|
||||
|
||||
@$core.Deprecated('Use responseDescriptor instead')
|
||||
const Response_PasswordlessNotificationMessage$json = {
|
||||
'1': 'PasswordlessNotificationMessage',
|
||||
'2': [
|
||||
{'1': 'id', '3': 1, '4': 1, '5': 3, '10': 'id'},
|
||||
{
|
||||
'1': 'encrypted_message',
|
||||
'3': 2,
|
||||
'4': 1,
|
||||
'5': 12,
|
||||
'10': 'encryptedMessage'
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@$core.Deprecated('Use responseDescriptor instead')
|
||||
const Response_PasswordlessNotificationMessages$json = {
|
||||
'1': 'PasswordlessNotificationMessages',
|
||||
'2': [
|
||||
{
|
||||
'1': 'messages',
|
||||
'3': 1,
|
||||
'4': 3,
|
||||
'5': 11,
|
||||
'6': '.server_to_client.Response.PasswordlessNotificationMessage',
|
||||
'10': 'messages'
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@$core.Deprecated('Use responseDescriptor instead')
|
||||
const Response_Ok$json = {
|
||||
'1': 'Ok',
|
||||
|
|
@ -623,6 +655,23 @@ const Response_Ok$json = {
|
|||
'9': 0,
|
||||
'10': 'proofOfWork'
|
||||
},
|
||||
{
|
||||
'1': 'passwordless_recovery_server_key',
|
||||
'3': 16,
|
||||
'4': 1,
|
||||
'5': 12,
|
||||
'9': 0,
|
||||
'10': 'passwordlessRecoveryServerKey'
|
||||
},
|
||||
{
|
||||
'1': 'passwordless_notification_messages',
|
||||
'3': 17,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.server_to_client.Response.PasswordlessNotificationMessages',
|
||||
'9': 0,
|
||||
'10': 'passwordlessNotificationMessages'
|
||||
},
|
||||
],
|
||||
'8': [
|
||||
{'1': 'Ok'},
|
||||
|
|
@ -677,22 +726,29 @@ final $typed_data.Uint8List responseDescriptor = $convert.base64Decode(
|
|||
'cKD2Rvd25sb2FkX3Rva2VucxgCIAMoDFIOZG93bmxvYWRUb2tlbnMaOQoORG93bmxvYWRUb2tl'
|
||||
'bnMSJwoPZG93bmxvYWRfdG9rZW5zGAEgAygMUg5kb3dubG9hZFRva2VucxpFCgtQcm9vZk9mV2'
|
||||
'9yaxIWCgZwcmVmaXgYASABKAlSBnByZWZpeBIeCgpkaWZmaWN1bHR5GAIgASgDUgpkaWZmaWN1'
|
||||
'bHR5GtcHCgJPaxIUCgROb25lGAEgASgISABSBE5vbmUSGAoGdXNlcmlkGAIgASgDSABSBnVzZX'
|
||||
'JpZBImCg1hdXRoY2hhbGxlbmdlGAMgASgMSABSDWF1dGhjaGFsbGVuZ2USSgoLdXBsb2FkdG9r'
|
||||
'ZW4YBCABKAsyJi5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlVwbG9hZFRva2VuSABSC3VwbG'
|
||||
'9hZHRva2VuEkEKCHVzZXJkYXRhGAUgASgLMiMuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5V'
|
||||
'c2VyRGF0YUgAUgh1c2VyZGF0YRIeCglhdXRodG9rZW4YBiABKAxIAFIJYXV0aHRva2VuEkoKDG'
|
||||
'RlcHJlY2F0ZWRfNxgHIAEoCzIlLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuRGVwcmVjYXRl'
|
||||
'ZEgAUgtkZXByZWNhdGVkNxJQCg1hdXRoZW50aWNhdGVkGAggASgLMiguc2VydmVyX3RvX2NsaW'
|
||||
'VudC5SZXNwb25zZS5BdXRoZW50aWNhdGVkSABSDWF1dGhlbnRpY2F0ZWQSOAoFcGxhbnMYCSAB'
|
||||
'KAsyIC5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlBsYW5zSABSBXBsYW5zEk0KDHBsYW5iYW'
|
||||
'xsYW5jZRgKIAEoCzInLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuUGxhbkJhbGxhbmNlSABS'
|
||||
'DHBsYW5iYWxsYW5jZRJMCg1kZXByZWNhdGVkXzExGAsgASgLMiUuc2VydmVyX3RvX2NsaWVudC'
|
||||
'5SZXNwb25zZS5EZXByZWNhdGVkSABSDGRlcHJlY2F0ZWQxMRJfChJhZGRhY2NvdW50c2ludml0'
|
||||
'ZXMYDCABKAsyLS5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLkFkZEFjY291bnRzSW52aXRlc0'
|
||||
'gAUhJhZGRhY2NvdW50c2ludml0ZXMSUwoOZG93bmxvYWR0b2tlbnMYDSABKAsyKS5zZXJ2ZXJf'
|
||||
'dG9fY2xpZW50LlJlc3BvbnNlLkRvd25sb2FkVG9rZW5zSABSDmRvd25sb2FkdG9rZW5zEk0KDH'
|
||||
'NpZ25lZHByZWtleRgOIAEoCzInLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuU2lnbmVkUHJl'
|
||||
'S2V5SABSDHNpZ25lZHByZWtleRJKCgtwcm9vZk9mV29yaxgPIAEoCzImLnNlcnZlcl90b19jbG'
|
||||
'llbnQuUmVzcG9uc2UuUHJvb2ZPZldvcmtIAFILcHJvb2ZPZldvcmtCBAoCT2tCCgoIUmVzcG9u'
|
||||
'c2U=');
|
||||
'bHR5Gl4KH1Bhc3N3b3JkbGVzc05vdGlmaWNhdGlvbk1lc3NhZ2USDgoCaWQYASABKANSAmlkEi'
|
||||
'sKEWVuY3J5cHRlZF9tZXNzYWdlGAIgASgMUhBlbmNyeXB0ZWRNZXNzYWdlGnoKIFBhc3N3b3Jk'
|
||||
'bGVzc05vdGlmaWNhdGlvbk1lc3NhZ2VzElYKCG1lc3NhZ2VzGAEgAygLMjouc2VydmVyX3RvX2'
|
||||
'NsaWVudC5SZXNwb25zZS5QYXNzd29yZGxlc3NOb3RpZmljYXRpb25NZXNzYWdlUghtZXNzYWdl'
|
||||
'cxqwCQoCT2sSFAoETm9uZRgBIAEoCEgAUgROb25lEhgKBnVzZXJpZBgCIAEoA0gAUgZ1c2VyaW'
|
||||
'QSJgoNYXV0aGNoYWxsZW5nZRgDIAEoDEgAUg1hdXRoY2hhbGxlbmdlEkoKC3VwbG9hZHRva2Vu'
|
||||
'GAQgASgLMiYuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5VcGxvYWRUb2tlbkgAUgt1cGxvYW'
|
||||
'R0b2tlbhJBCgh1c2VyZGF0YRgFIAEoCzIjLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuVXNl'
|
||||
'ckRhdGFIAFIIdXNlcmRhdGESHgoJYXV0aHRva2VuGAYgASgMSABSCWF1dGh0b2tlbhJKCgxkZX'
|
||||
'ByZWNhdGVkXzcYByABKAsyJS5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLkRlcHJlY2F0ZWRI'
|
||||
'AFILZGVwcmVjYXRlZDcSUAoNYXV0aGVudGljYXRlZBgIIAEoCzIoLnNlcnZlcl90b19jbGllbn'
|
||||
'QuUmVzcG9uc2UuQXV0aGVudGljYXRlZEgAUg1hdXRoZW50aWNhdGVkEjgKBXBsYW5zGAkgASgL'
|
||||
'MiAuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5QbGFuc0gAUgVwbGFucxJNCgxwbGFuYmFsbG'
|
||||
'FuY2UYCiABKAsyJy5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlBsYW5CYWxsYW5jZUgAUgxw'
|
||||
'bGFuYmFsbGFuY2USTAoNZGVwcmVjYXRlZF8xMRgLIAEoCzIlLnNlcnZlcl90b19jbGllbnQuUm'
|
||||
'VzcG9uc2UuRGVwcmVjYXRlZEgAUgxkZXByZWNhdGVkMTESXwoSYWRkYWNjb3VudHNpbnZpdGVz'
|
||||
'GAwgASgLMi0uc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5BZGRBY2NvdW50c0ludml0ZXNIAF'
|
||||
'ISYWRkYWNjb3VudHNpbnZpdGVzElMKDmRvd25sb2FkdG9rZW5zGA0gASgLMikuc2VydmVyX3Rv'
|
||||
'X2NsaWVudC5SZXNwb25zZS5Eb3dubG9hZFRva2Vuc0gAUg5kb3dubG9hZHRva2VucxJNCgxzaW'
|
||||
'duZWRwcmVrZXkYDiABKAsyJy5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlNpZ25lZFByZUtl'
|
||||
'eUgAUgxzaWduZWRwcmVrZXkSSgoLcHJvb2ZPZldvcmsYDyABKAsyJi5zZXJ2ZXJfdG9fY2xpZW'
|
||||
'50LlJlc3BvbnNlLlByb29mT2ZXb3JrSABSC3Byb29mT2ZXb3JrEkkKIHBhc3N3b3JkbGVzc19y'
|
||||
'ZWNvdmVyeV9zZXJ2ZXJfa2V5GBAgASgMSABSHXBhc3N3b3JkbGVzc1JlY292ZXJ5U2VydmVyS2'
|
||||
'V5EosBCiJwYXNzd29yZGxlc3Nfbm90aWZpY2F0aW9uX21lc3NhZ2VzGBEgASgLMjsuc2VydmVy'
|
||||
'X3RvX2NsaWVudC5SZXNwb25zZS5QYXNzd29yZGxlc3NOb3RpZmljYXRpb25NZXNzYWdlc0gAUi'
|
||||
'BwYXNzd29yZGxlc3NOb3RpZmljYXRpb25NZXNzYWdlc0IECgJPa0IKCghSZXNwb25zZQ==');
|
||||
|
|
|
|||
|
|
@ -1839,6 +1839,154 @@ class EncryptedContent_KeyVerificationProof extends $pb.GeneratedMessage {
|
|||
void clearCalculatedMac() => $_clearField(1);
|
||||
}
|
||||
|
||||
class EncryptedContent_PasswordLessRecovery extends $pb.GeneratedMessage {
|
||||
factory EncryptedContent_PasswordLessRecovery({
|
||||
$core.List<$core.int>? recoverySecretShare,
|
||||
$core.bool? delete,
|
||||
$fixnum.Int64? threshold,
|
||||
}) {
|
||||
final result = create();
|
||||
if (recoverySecretShare != null)
|
||||
result.recoverySecretShare = recoverySecretShare;
|
||||
if (delete != null) result.delete = delete;
|
||||
if (threshold != null) result.threshold = threshold;
|
||||
return result;
|
||||
}
|
||||
|
||||
EncryptedContent_PasswordLessRecovery._();
|
||||
|
||||
factory EncryptedContent_PasswordLessRecovery.fromBuffer(
|
||||
$core.List<$core.int> data,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(data, registry);
|
||||
factory EncryptedContent_PasswordLessRecovery.fromJson($core.String json,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(json, registry);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
_omitMessageNames ? '' : 'EncryptedContent.PasswordLessRecovery',
|
||||
createEmptyInstance: create)
|
||||
..a<$core.List<$core.int>>(
|
||||
1, _omitFieldNames ? '' : 'recoverySecretShare', $pb.PbFieldType.OY,
|
||||
protoName: 'recoverySecretShare')
|
||||
..aOB(2, _omitFieldNames ? '' : 'delete')
|
||||
..aInt64(3, _omitFieldNames ? '' : 'threshold')
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
EncryptedContent_PasswordLessRecovery clone() => deepCopy();
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
EncryptedContent_PasswordLessRecovery copyWith(
|
||||
void Function(EncryptedContent_PasswordLessRecovery) updates) =>
|
||||
super.copyWith((message) =>
|
||||
updates(message as EncryptedContent_PasswordLessRecovery))
|
||||
as EncryptedContent_PasswordLessRecovery;
|
||||
|
||||
@$core.override
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static EncryptedContent_PasswordLessRecovery create() =>
|
||||
EncryptedContent_PasswordLessRecovery._();
|
||||
@$core.override
|
||||
EncryptedContent_PasswordLessRecovery createEmptyInstance() => create();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static EncryptedContent_PasswordLessRecovery getDefault() =>
|
||||
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<
|
||||
EncryptedContent_PasswordLessRecovery>(create);
|
||||
static EncryptedContent_PasswordLessRecovery? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.List<$core.int> get recoverySecretShare => $_getN(0);
|
||||
@$pb.TagNumber(1)
|
||||
set recoverySecretShare($core.List<$core.int> value) => $_setBytes(0, value);
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasRecoverySecretShare() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearRecoverySecretShare() => $_clearField(1);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool get delete => $_getBF(1);
|
||||
@$pb.TagNumber(2)
|
||||
set delete($core.bool value) => $_setBool(1, value);
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasDelete() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearDelete() => $_clearField(2);
|
||||
|
||||
@$pb.TagNumber(3)
|
||||
$fixnum.Int64 get threshold => $_getI64(2);
|
||||
@$pb.TagNumber(3)
|
||||
set threshold($fixnum.Int64 value) => $_setInt64(2, value);
|
||||
@$pb.TagNumber(3)
|
||||
$core.bool hasThreshold() => $_has(2);
|
||||
@$pb.TagNumber(3)
|
||||
void clearThreshold() => $_clearField(3);
|
||||
}
|
||||
|
||||
class EncryptedContent_PasswordLessRecoveryHeartbeat
|
||||
extends $pb.GeneratedMessage {
|
||||
factory EncryptedContent_PasswordLessRecoveryHeartbeat({
|
||||
$core.List<$core.int>? hash,
|
||||
}) {
|
||||
final result = create();
|
||||
if (hash != null) result.hash = hash;
|
||||
return result;
|
||||
}
|
||||
|
||||
EncryptedContent_PasswordLessRecoveryHeartbeat._();
|
||||
|
||||
factory EncryptedContent_PasswordLessRecoveryHeartbeat.fromBuffer(
|
||||
$core.List<$core.int> data,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(data, registry);
|
||||
factory EncryptedContent_PasswordLessRecoveryHeartbeat.fromJson(
|
||||
$core.String json,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(json, registry);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
_omitMessageNames ? '' : 'EncryptedContent.PasswordLessRecoveryHeartbeat',
|
||||
createEmptyInstance: create)
|
||||
..a<$core.List<$core.int>>(
|
||||
1, _omitFieldNames ? '' : 'hash', $pb.PbFieldType.OY)
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
EncryptedContent_PasswordLessRecoveryHeartbeat clone() => deepCopy();
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
EncryptedContent_PasswordLessRecoveryHeartbeat copyWith(
|
||||
void Function(EncryptedContent_PasswordLessRecoveryHeartbeat)
|
||||
updates) =>
|
||||
super.copyWith((message) => updates(
|
||||
message as EncryptedContent_PasswordLessRecoveryHeartbeat))
|
||||
as EncryptedContent_PasswordLessRecoveryHeartbeat;
|
||||
|
||||
@$core.override
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static EncryptedContent_PasswordLessRecoveryHeartbeat create() =>
|
||||
EncryptedContent_PasswordLessRecoveryHeartbeat._();
|
||||
@$core.override
|
||||
EncryptedContent_PasswordLessRecoveryHeartbeat createEmptyInstance() =>
|
||||
create();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static EncryptedContent_PasswordLessRecoveryHeartbeat getDefault() =>
|
||||
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<
|
||||
EncryptedContent_PasswordLessRecoveryHeartbeat>(create);
|
||||
static EncryptedContent_PasswordLessRecoveryHeartbeat? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.List<$core.int> get hash => $_getN(0);
|
||||
@$pb.TagNumber(1)
|
||||
set hash($core.List<$core.int> value) => $_setBytes(0, value);
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasHash() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearHash() => $_clearField(1);
|
||||
}
|
||||
|
||||
class EncryptedContent extends $pb.GeneratedMessage {
|
||||
factory EncryptedContent({
|
||||
$core.String? groupId,
|
||||
|
|
@ -1865,6 +2013,9 @@ class EncryptedContent extends $pb.GeneratedMessage {
|
|||
EncryptedContent_UserDiscoveryUpdate? userDiscoveryUpdate,
|
||||
EncryptedContent_KeyVerificationProof? keyVerificationProof,
|
||||
$core.bool? askForFriendPromotions,
|
||||
EncryptedContent_PasswordLessRecovery? passwordlessRecovery,
|
||||
EncryptedContent_PasswordLessRecoveryHeartbeat?
|
||||
passwordlessRecoveryHeartbeat,
|
||||
}) {
|
||||
final result = create();
|
||||
if (groupId != null) result.groupId = groupId;
|
||||
|
|
@ -1899,6 +2050,10 @@ class EncryptedContent extends $pb.GeneratedMessage {
|
|||
result.keyVerificationProof = keyVerificationProof;
|
||||
if (askForFriendPromotions != null)
|
||||
result.askForFriendPromotions = askForFriendPromotions;
|
||||
if (passwordlessRecovery != null)
|
||||
result.passwordlessRecovery = passwordlessRecovery;
|
||||
if (passwordlessRecoveryHeartbeat != null)
|
||||
result.passwordlessRecoveryHeartbeat = passwordlessRecoveryHeartbeat;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -1971,6 +2126,12 @@ class EncryptedContent extends $pb.GeneratedMessage {
|
|||
24, _omitFieldNames ? '' : 'keyVerificationProof',
|
||||
subBuilder: EncryptedContent_KeyVerificationProof.create)
|
||||
..aOB(25, _omitFieldNames ? '' : 'askForFriendPromotions')
|
||||
..aOM<EncryptedContent_PasswordLessRecovery>(
|
||||
26, _omitFieldNames ? '' : 'passwordlessRecovery',
|
||||
subBuilder: EncryptedContent_PasswordLessRecovery.create)
|
||||
..aOM<EncryptedContent_PasswordLessRecoveryHeartbeat>(
|
||||
27, _omitFieldNames ? '' : 'passwordlessRecoveryHeartbeat',
|
||||
subBuilder: EncryptedContent_PasswordLessRecoveryHeartbeat.create)
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
|
|
@ -2263,6 +2424,34 @@ class EncryptedContent extends $pb.GeneratedMessage {
|
|||
$core.bool hasAskForFriendPromotions() => $_has(23);
|
||||
@$pb.TagNumber(25)
|
||||
void clearAskForFriendPromotions() => $_clearField(25);
|
||||
|
||||
@$pb.TagNumber(26)
|
||||
EncryptedContent_PasswordLessRecovery get passwordlessRecovery => $_getN(24);
|
||||
@$pb.TagNumber(26)
|
||||
set passwordlessRecovery(EncryptedContent_PasswordLessRecovery value) =>
|
||||
$_setField(26, value);
|
||||
@$pb.TagNumber(26)
|
||||
$core.bool hasPasswordlessRecovery() => $_has(24);
|
||||
@$pb.TagNumber(26)
|
||||
void clearPasswordlessRecovery() => $_clearField(26);
|
||||
@$pb.TagNumber(26)
|
||||
EncryptedContent_PasswordLessRecovery ensurePasswordlessRecovery() =>
|
||||
$_ensure(24);
|
||||
|
||||
@$pb.TagNumber(27)
|
||||
EncryptedContent_PasswordLessRecoveryHeartbeat
|
||||
get passwordlessRecoveryHeartbeat => $_getN(25);
|
||||
@$pb.TagNumber(27)
|
||||
set passwordlessRecoveryHeartbeat(
|
||||
EncryptedContent_PasswordLessRecoveryHeartbeat value) =>
|
||||
$_setField(27, value);
|
||||
@$pb.TagNumber(27)
|
||||
$core.bool hasPasswordlessRecoveryHeartbeat() => $_has(25);
|
||||
@$pb.TagNumber(27)
|
||||
void clearPasswordlessRecoveryHeartbeat() => $_clearField(27);
|
||||
@$pb.TagNumber(27)
|
||||
EncryptedContent_PasswordLessRecoveryHeartbeat
|
||||
ensurePasswordlessRecoveryHeartbeat() => $_ensure(25);
|
||||
}
|
||||
|
||||
const $core.bool _omitFieldNames =
|
||||
|
|
|
|||
|
|
@ -385,6 +385,26 @@ const EncryptedContent$json = {
|
|||
'10': 'keyVerificationProof',
|
||||
'17': true
|
||||
},
|
||||
{
|
||||
'1': 'passwordless_recovery',
|
||||
'3': 26,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.EncryptedContent.PasswordLessRecovery',
|
||||
'9': 24,
|
||||
'10': 'passwordlessRecovery',
|
||||
'17': true
|
||||
},
|
||||
{
|
||||
'1': 'passwordless_recovery_heartbeat',
|
||||
'3': 27,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.EncryptedContent.PasswordLessRecoveryHeartbeat',
|
||||
'9': 25,
|
||||
'10': 'passwordlessRecoveryHeartbeat',
|
||||
'17': true
|
||||
},
|
||||
],
|
||||
'3': [
|
||||
EncryptedContent_ErrorMessages$json,
|
||||
|
|
@ -405,7 +425,9 @@ const EncryptedContent$json = {
|
|||
EncryptedContent_TypingIndicator$json,
|
||||
EncryptedContent_UserDiscoveryRequest$json,
|
||||
EncryptedContent_UserDiscoveryUpdate$json,
|
||||
EncryptedContent_KeyVerificationProof$json
|
||||
EncryptedContent_KeyVerificationProof$json,
|
||||
EncryptedContent_PasswordLessRecovery$json,
|
||||
EncryptedContent_PasswordLessRecoveryHeartbeat$json
|
||||
],
|
||||
'8': [
|
||||
{'1': '_group_id'},
|
||||
|
|
@ -432,6 +454,8 @@ const EncryptedContent$json = {
|
|||
{'1': '_user_discovery_request'},
|
||||
{'1': '_user_discovery_update'},
|
||||
{'1': '_key_verification_proof'},
|
||||
{'1': '_passwordless_recovery'},
|
||||
{'1': '_passwordless_recovery_heartbeat'},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
@ -955,6 +979,35 @@ const EncryptedContent_KeyVerificationProof$json = {
|
|||
],
|
||||
};
|
||||
|
||||
@$core.Deprecated('Use encryptedContentDescriptor instead')
|
||||
const EncryptedContent_PasswordLessRecovery$json = {
|
||||
'1': 'PasswordLessRecovery',
|
||||
'2': [
|
||||
{
|
||||
'1': 'recoverySecretShare',
|
||||
'3': 1,
|
||||
'4': 1,
|
||||
'5': 12,
|
||||
'9': 0,
|
||||
'10': 'recoverySecretShare',
|
||||
'17': true
|
||||
},
|
||||
{'1': 'delete', '3': 2, '4': 1, '5': 8, '10': 'delete'},
|
||||
{'1': 'threshold', '3': 3, '4': 1, '5': 3, '10': 'threshold'},
|
||||
],
|
||||
'8': [
|
||||
{'1': '_recoverySecretShare'},
|
||||
],
|
||||
};
|
||||
|
||||
@$core.Deprecated('Use encryptedContentDescriptor instead')
|
||||
const EncryptedContent_PasswordLessRecoveryHeartbeat$json = {
|
||||
'1': 'PasswordLessRecoveryHeartbeat',
|
||||
'2': [
|
||||
{'1': 'hash', '3': 1, '4': 1, '5': 12, '10': 'hash'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `EncryptedContent`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode(
|
||||
'ChBFbmNyeXB0ZWRDb250ZW50Eh4KCGdyb3VwX2lkGAIgASgJSABSB2dyb3VwSWSIAQESKQoOaX'
|
||||
|
|
@ -988,79 +1041,88 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode(
|
|||
'aXNjb3ZlcnlSZXF1ZXN0iAEBEl4KFXVzZXJfZGlzY292ZXJ5X3VwZGF0ZRgXIAEoCzIlLkVuY3'
|
||||
'J5cHRlZENvbnRlbnQuVXNlckRpc2NvdmVyeVVwZGF0ZUgWUhN1c2VyRGlzY292ZXJ5VXBkYXRl'
|
||||
'iAEBEmEKFmtleV92ZXJpZmljYXRpb25fcHJvb2YYGCABKAsyJi5FbmNyeXB0ZWRDb250ZW50Lk'
|
||||
'tleVZlcmlmaWNhdGlvblByb29mSBdSFGtleVZlcmlmaWNhdGlvblByb29miAEBGpYCCg1FcnJv'
|
||||
'ck1lc3NhZ2VzEjgKBHR5cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50LkVycm9yTWVzc2FnZX'
|
||||
'MuVHlwZVIEdHlwZRIsChJyZWxhdGVkX3JlY2VpcHRfaWQYAiABKAlSEHJlbGF0ZWRSZWNlaXB0'
|
||||
'SWQinAEKBFR5cGUSPAo4RVJST1JfUFJPQ0VTU0lOR19NRVNTQUdFX0NSRUFURURfQUNDT1VOVF'
|
||||
'9SRVFVRVNUX0lOU1RFQUQQABIYChRVTktOT1dOX01FU1NBR0VfVFlQRRACEhcKE1NFU1NJT05f'
|
||||
'T1VUX09GX1NZTkMQAxIjCh9HUk9VUF9OT1RfRk9VTkRfT1JfTk9UX0FfTUVNQkVSEAQahwEKC0'
|
||||
'dyb3VwQ3JlYXRlEhsKCXN0YXRlX2tleRgDIAEoDFIIc3RhdGVLZXkSKAoQZ3JvdXBfcHVibGlj'
|
||||
'X2tleRgEIAEoDFIOZ3JvdXBQdWJsaWNLZXkSIgoKZ3JvdXBfbmFtZRgFIAEoCUgAUglncm91cE'
|
||||
'5hbWWIAQFCDQoLX2dyb3VwX25hbWUaNQoJR3JvdXBKb2luEigKEGdyb3VwX3B1YmxpY19rZXkY'
|
||||
'ASABKAxSDmdyb3VwUHVibGljS2V5GhYKFFJlc2VuZEdyb3VwUHVibGljS2V5GsgCCgtHcm91cF'
|
||||
'VwZGF0ZRIqChFncm91cF9hY3Rpb25fdHlwZRgBIAEoCVIPZ3JvdXBBY3Rpb25UeXBlEjMKE2Fm'
|
||||
'ZmVjdGVkX2NvbnRhY3RfaWQYAiABKANIAFIRYWZmZWN0ZWRDb250YWN0SWSIAQESKQoObmV3X2'
|
||||
'dyb3VwX25hbWUYAyABKAlIAVIMbmV3R3JvdXBOYW1liAEBElcKJm5ld19kZWxldGVfbWVzc2Fn'
|
||||
'ZXNfYWZ0ZXJfbWlsbGlzZWNvbmRzGAQgASgDSAJSIm5ld0RlbGV0ZU1lc3NhZ2VzQWZ0ZXJNaW'
|
||||
'xsaXNlY29uZHOIAQFCFgoUX2FmZmVjdGVkX2NvbnRhY3RfaWRCEQoPX25ld19ncm91cF9uYW1l'
|
||||
'QikKJ19uZXdfZGVsZXRlX21lc3NhZ2VzX2FmdGVyX21pbGxpc2Vjb25kcxqvAQoLVGV4dE1lc3'
|
||||
'NhZ2USKgoRc2VuZGVyX21lc3NhZ2VfaWQYASABKAlSD3NlbmRlck1lc3NhZ2VJZBISCgR0ZXh0'
|
||||
'GAIgASgJUgR0ZXh0EhwKCXRpbWVzdGFtcBgDIAEoA1IJdGltZXN0YW1wEi0KEHF1b3RlX21lc3'
|
||||
'NhZ2VfaWQYBCABKAlIAFIOcXVvdGVNZXNzYWdlSWSIAQFCEwoRX3F1b3RlX21lc3NhZ2VfaWQa'
|
||||
'zgEKFUFkZGl0aW9uYWxEYXRhTWVzc2FnZRIqChFzZW5kZXJfbWVzc2FnZV9pZBgBIAEoCVIPc2'
|
||||
'VuZGVyTWVzc2FnZUlkEhwKCXRpbWVzdGFtcBgCIAEoA1IJdGltZXN0YW1wEhIKBHR5cGUYAyAB'
|
||||
'KAlSBHR5cGUSOwoXYWRkaXRpb25hbF9tZXNzYWdlX2RhdGEYBCABKAxIAFIVYWRkaXRpb25hbE'
|
||||
'1lc3NhZ2VEYXRhiAEBQhoKGF9hZGRpdGlvbmFsX21lc3NhZ2VfZGF0YRpkCghSZWFjdGlvbhIq'
|
||||
'ChF0YXJnZXRfbWVzc2FnZV9pZBgBIAEoCVIPdGFyZ2V0TWVzc2FnZUlkEhQKBWVtb2ppGAIgAS'
|
||||
'gJUgVlbW9qaRIWCgZyZW1vdmUYAyABKAhSBnJlbW92ZRq+AgoNTWVzc2FnZVVwZGF0ZRI4CgR0'
|
||||
'eXBlGAEgASgOMiQuRW5jcnlwdGVkQ29udGVudC5NZXNzYWdlVXBkYXRlLlR5cGVSBHR5cGUSLw'
|
||||
'oRc2VuZGVyX21lc3NhZ2VfaWQYAiABKAlIAFIPc2VuZGVyTWVzc2FnZUlkiAEBEj0KG211bHRp'
|
||||
'cGxlX3RhcmdldF9tZXNzYWdlX2lkcxgDIAMoCVIYbXVsdGlwbGVUYXJnZXRNZXNzYWdlSWRzEh'
|
||||
'cKBHRleHQYBCABKAlIAVIEdGV4dIgBARIcCgl0aW1lc3RhbXAYBSABKANSCXRpbWVzdGFtcCIt'
|
||||
'CgRUeXBlEgoKBkRFTEVURRAAEg0KCUVESVRfVEVYVBABEgoKBk9QRU5FRBACQhQKEl9zZW5kZX'
|
||||
'JfbWVzc2FnZV9pZEIHCgVfdGV4dBqFBgoFTWVkaWESKgoRc2VuZGVyX21lc3NhZ2VfaWQYASAB'
|
||||
'KAlSD3NlbmRlck1lc3NhZ2VJZBIwCgR0eXBlGAIgASgOMhwuRW5jcnlwdGVkQ29udGVudC5NZW'
|
||||
'RpYS5UeXBlUgR0eXBlEkYKHWRpc3BsYXlfbGltaXRfaW5fbWlsbGlzZWNvbmRzGAMgASgDSABS'
|
||||
'GmRpc3BsYXlMaW1pdEluTWlsbGlzZWNvbmRziAEBEjcKF3JlcXVpcmVzX2F1dGhlbnRpY2F0aW'
|
||||
'9uGAQgASgIUhZyZXF1aXJlc0F1dGhlbnRpY2F0aW9uEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGlt'
|
||||
'ZXN0YW1wEi0KEHF1b3RlX21lc3NhZ2VfaWQYBiABKAlIAVIOcXVvdGVNZXNzYWdlSWSIAQESKg'
|
||||
'oOZG93bmxvYWRfdG9rZW4YByABKAxIAlINZG93bmxvYWRUb2tlbogBARIqCg5lbmNyeXB0aW9u'
|
||||
'X2tleRgIIAEoDEgDUg1lbmNyeXB0aW9uS2V5iAEBEioKDmVuY3J5cHRpb25fbWFjGAkgASgMSA'
|
||||
'RSDWVuY3J5cHRpb25NYWOIAQESLgoQZW5jcnlwdGlvbl9ub25jZRgKIAEoDEgFUg9lbmNyeXB0'
|
||||
'aW9uTm9uY2WIAQESOwoXYWRkaXRpb25hbF9tZXNzYWdlX2RhdGEYCyABKAxIBlIVYWRkaXRpb2'
|
||||
'5hbE1lc3NhZ2VEYXRhiAEBIj4KBFR5cGUSDAoIUkVVUExPQUQQABIJCgVJTUFHRRABEgkKBVZJ'
|
||||
'REVPEAISBwoDR0lGEAMSCQoFQVVESU8QBEIgCh5fZGlzcGxheV9saW1pdF9pbl9taWxsaXNlY2'
|
||||
'9uZHNCEwoRX3F1b3RlX21lc3NhZ2VfaWRCEQoPX2Rvd25sb2FkX3Rva2VuQhEKD19lbmNyeXB0'
|
||||
'aW9uX2tleUIRCg9fZW5jcnlwdGlvbl9tYWNCEwoRX2VuY3J5cHRpb25fbm9uY2VCGgoYX2FkZG'
|
||||
'l0aW9uYWxfbWVzc2FnZV9kYXRhGqkBCgtNZWRpYVVwZGF0ZRI2CgR0eXBlGAEgASgOMiIuRW5j'
|
||||
'cnlwdGVkQ29udGVudC5NZWRpYVVwZGF0ZS5UeXBlUgR0eXBlEioKEXRhcmdldF9tZXNzYWdlX2'
|
||||
'lkGAIgASgJUg90YXJnZXRNZXNzYWdlSWQiNgoEVHlwZRIMCghSRU9QRU5FRBAAEgoKBlNUT1JF'
|
||||
'RBABEhQKEERFQ1JZUFRJT05fRVJST1IQAhp4Cg5Db250YWN0UmVxdWVzdBI5CgR0eXBlGAEgAS'
|
||||
'gOMiUuRW5jcnlwdGVkQ29udGVudC5Db250YWN0UmVxdWVzdC5UeXBlUgR0eXBlIisKBFR5cGUS'
|
||||
'CwoHUkVRVUVTVBAAEgoKBlJFSkVDVBABEgoKBkFDQ0VQVBACGqQCCg1Db250YWN0VXBkYXRlEj'
|
||||
'gKBHR5cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50LkNvbnRhY3RVcGRhdGUuVHlwZVIEdHlw'
|
||||
'ZRI3ChVhdmF0YXJfc3ZnX2NvbXByZXNzZWQYAiABKAxIAFITYXZhdGFyU3ZnQ29tcHJlc3NlZI'
|
||||
'gBARIfCgh1c2VybmFtZRgDIAEoCUgBUgh1c2VybmFtZYgBARImCgxkaXNwbGF5X25hbWUYBCAB'
|
||||
'KAlIAlILZGlzcGxheU5hbWWIAQEiHwoEVHlwZRILCgdSRVFVRVNUEAASCgoGVVBEQVRFEAFCGA'
|
||||
'oWX2F2YXRhcl9zdmdfY29tcHJlc3NlZEILCglfdXNlcm5hbWVCDwoNX2Rpc3BsYXlfbmFtZRrZ'
|
||||
'AQoIUHVzaEtleXMSMwoEdHlwZRgBIAEoDjIfLkVuY3J5cHRlZENvbnRlbnQuUHVzaEtleXMuVH'
|
||||
'lwZVIEdHlwZRIaCgZrZXlfaWQYAiABKANIAFIFa2V5SWSIAQESFQoDa2V5GAMgASgMSAFSA2tl'
|
||||
'eYgBARIiCgpjcmVhdGVkX2F0GAQgASgDSAJSCWNyZWF0ZWRBdIgBASIfCgRUeXBlEgsKB1JFUV'
|
||||
'VFU1QQABIKCgZVUERBVEUQAUIJCgdfa2V5X2lkQgYKBF9rZXlCDQoLX2NyZWF0ZWRfYXQarwEK'
|
||||
'CUZsYW1lU3luYxIjCg1mbGFtZV9jb3VudGVyGAEgASgDUgxmbGFtZUNvdW50ZXISOQoZbGFzdF'
|
||||
'9mbGFtZV9jb3VudGVyX2NoYW5nZRgCIAEoA1IWbGFzdEZsYW1lQ291bnRlckNoYW5nZRIfCgti'
|
||||
'ZXN0X2ZyaWVuZBgDIAEoCFIKYmVzdEZyaWVuZBIhCgxmb3JjZV91cGRhdGUYBCABKAhSC2Zvcm'
|
||||
'NlVXBkYXRlGk0KD1R5cGluZ0luZGljYXRvchIbCglpc190eXBpbmcYASABKAhSCGlzVHlwaW5n'
|
||||
'Eh0KCmNyZWF0ZWRfYXQYAiABKANSCWNyZWF0ZWRBdBo/ChRVc2VyRGlzY292ZXJ5UmVxdWVzdB'
|
||||
'InCg9jdXJyZW50X3ZlcnNpb24YASABKAxSDmN1cnJlbnRWZXJzaW9uGjEKE1VzZXJEaXNjb3Zl'
|
||||
'cnlVcGRhdGUSGgoIbWVzc2FnZXMYASADKAxSCG1lc3NhZ2VzGj0KFEtleVZlcmlmaWNhdGlvbl'
|
||||
'Byb29mEiUKDmNhbGN1bGF0ZWRfbWFjGAEgASgMUg1jYWxjdWxhdGVkTWFjQgsKCV9ncm91cF9p'
|
||||
'ZEIRCg9faXNfZGlyZWN0X2NoYXRCGQoXX3NlbmRlcl9wcm9maWxlX2NvdW50ZXJCIAoeX3Nlbm'
|
||||
'Rlcl91c2VyX2Rpc2NvdmVyeV92ZXJzaW9uQhwKGl9hc2tfZm9yX2ZyaWVuZF9wcm9tb3Rpb25z'
|
||||
'QhEKD19tZXNzYWdlX3VwZGF0ZUIICgZfbWVkaWFCDwoNX21lZGlhX3VwZGF0ZUIRCg9fY29udG'
|
||||
'FjdF91cGRhdGVCEgoQX2NvbnRhY3RfcmVxdWVzdEINCgtfZmxhbWVfc3luY0IMCgpfcHVzaF9r'
|
||||
'ZXlzQgsKCV9yZWFjdGlvbkIPCg1fdGV4dF9tZXNzYWdlQg8KDV9ncm91cF9jcmVhdGVCDQoLX2'
|
||||
'dyb3VwX2pvaW5CDwoNX2dyb3VwX3VwZGF0ZUIaChhfcmVzZW5kX2dyb3VwX3B1YmxpY19rZXlC'
|
||||
'EQoPX2Vycm9yX21lc3NhZ2VzQhoKGF9hZGRpdGlvbmFsX2RhdGFfbWVzc2FnZUITChFfdHlwaW'
|
||||
'5nX2luZGljYXRvckIZChdfdXNlcl9kaXNjb3ZlcnlfcmVxdWVzdEIYChZfdXNlcl9kaXNjb3Zl'
|
||||
'cnlfdXBkYXRlQhkKF19rZXlfdmVyaWZpY2F0aW9uX3Byb29m');
|
||||
'tleVZlcmlmaWNhdGlvblByb29mSBdSFGtleVZlcmlmaWNhdGlvblByb29miAEBEmAKFXBhc3N3'
|
||||
'b3JkbGVzc19yZWNvdmVyeRgaIAEoCzImLkVuY3J5cHRlZENvbnRlbnQuUGFzc3dvcmRMZXNzUm'
|
||||
'Vjb3ZlcnlIGFIUcGFzc3dvcmRsZXNzUmVjb3ZlcnmIAQESfAofcGFzc3dvcmRsZXNzX3JlY292'
|
||||
'ZXJ5X2hlYXJ0YmVhdBgbIAEoCzIvLkVuY3J5cHRlZENvbnRlbnQuUGFzc3dvcmRMZXNzUmVjb3'
|
||||
'ZlcnlIZWFydGJlYXRIGVIdcGFzc3dvcmRsZXNzUmVjb3ZlcnlIZWFydGJlYXSIAQEalgIKDUVy'
|
||||
'cm9yTWVzc2FnZXMSOAoEdHlwZRgBIAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuRXJyb3JNZXNzYW'
|
||||
'dlcy5UeXBlUgR0eXBlEiwKEnJlbGF0ZWRfcmVjZWlwdF9pZBgCIAEoCVIQcmVsYXRlZFJlY2Vp'
|
||||
'cHRJZCKcAQoEVHlwZRI8CjhFUlJPUl9QUk9DRVNTSU5HX01FU1NBR0VfQ1JFQVRFRF9BQ0NPVU'
|
||||
'5UX1JFUVVFU1RfSU5TVEVBRBAAEhgKFFVOS05PV05fTUVTU0FHRV9UWVBFEAISFwoTU0VTU0lP'
|
||||
'Tl9PVVRfT0ZfU1lOQxADEiMKH0dST1VQX05PVF9GT1VORF9PUl9OT1RfQV9NRU1CRVIQBBqHAQ'
|
||||
'oLR3JvdXBDcmVhdGUSGwoJc3RhdGVfa2V5GAMgASgMUghzdGF0ZUtleRIoChBncm91cF9wdWJs'
|
||||
'aWNfa2V5GAQgASgMUg5ncm91cFB1YmxpY0tleRIiCgpncm91cF9uYW1lGAUgASgJSABSCWdyb3'
|
||||
'VwTmFtZYgBAUINCgtfZ3JvdXBfbmFtZRo1CglHcm91cEpvaW4SKAoQZ3JvdXBfcHVibGljX2tl'
|
||||
'eRgBIAEoDFIOZ3JvdXBQdWJsaWNLZXkaFgoUUmVzZW5kR3JvdXBQdWJsaWNLZXkayAIKC0dyb3'
|
||||
'VwVXBkYXRlEioKEWdyb3VwX2FjdGlvbl90eXBlGAEgASgJUg9ncm91cEFjdGlvblR5cGUSMwoT'
|
||||
'YWZmZWN0ZWRfY29udGFjdF9pZBgCIAEoA0gAUhFhZmZlY3RlZENvbnRhY3RJZIgBARIpCg5uZX'
|
||||
'dfZ3JvdXBfbmFtZRgDIAEoCUgBUgxuZXdHcm91cE5hbWWIAQESVwombmV3X2RlbGV0ZV9tZXNz'
|
||||
'YWdlc19hZnRlcl9taWxsaXNlY29uZHMYBCABKANIAlIibmV3RGVsZXRlTWVzc2FnZXNBZnRlck'
|
||||
'1pbGxpc2Vjb25kc4gBAUIWChRfYWZmZWN0ZWRfY29udGFjdF9pZEIRCg9fbmV3X2dyb3VwX25h'
|
||||
'bWVCKQonX25ld19kZWxldGVfbWVzc2FnZXNfYWZ0ZXJfbWlsbGlzZWNvbmRzGq8BCgtUZXh0TW'
|
||||
'Vzc2FnZRIqChFzZW5kZXJfbWVzc2FnZV9pZBgBIAEoCVIPc2VuZGVyTWVzc2FnZUlkEhIKBHRl'
|
||||
'eHQYAiABKAlSBHRleHQSHAoJdGltZXN0YW1wGAMgASgDUgl0aW1lc3RhbXASLQoQcXVvdGVfbW'
|
||||
'Vzc2FnZV9pZBgEIAEoCUgAUg5xdW90ZU1lc3NhZ2VJZIgBAUITChFfcXVvdGVfbWVzc2FnZV9p'
|
||||
'ZBrOAQoVQWRkaXRpb25hbERhdGFNZXNzYWdlEioKEXNlbmRlcl9tZXNzYWdlX2lkGAEgASgJUg'
|
||||
'9zZW5kZXJNZXNzYWdlSWQSHAoJdGltZXN0YW1wGAIgASgDUgl0aW1lc3RhbXASEgoEdHlwZRgD'
|
||||
'IAEoCVIEdHlwZRI7ChdhZGRpdGlvbmFsX21lc3NhZ2VfZGF0YRgEIAEoDEgAUhVhZGRpdGlvbm'
|
||||
'FsTWVzc2FnZURhdGGIAQFCGgoYX2FkZGl0aW9uYWxfbWVzc2FnZV9kYXRhGmQKCFJlYWN0aW9u'
|
||||
'EioKEXRhcmdldF9tZXNzYWdlX2lkGAEgASgJUg90YXJnZXRNZXNzYWdlSWQSFAoFZW1vamkYAi'
|
||||
'ABKAlSBWVtb2ppEhYKBnJlbW92ZRgDIAEoCFIGcmVtb3ZlGr4CCg1NZXNzYWdlVXBkYXRlEjgK'
|
||||
'BHR5cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50Lk1lc3NhZ2VVcGRhdGUuVHlwZVIEdHlwZR'
|
||||
'IvChFzZW5kZXJfbWVzc2FnZV9pZBgCIAEoCUgAUg9zZW5kZXJNZXNzYWdlSWSIAQESPQobbXVs'
|
||||
'dGlwbGVfdGFyZ2V0X21lc3NhZ2VfaWRzGAMgAygJUhhtdWx0aXBsZVRhcmdldE1lc3NhZ2VJZH'
|
||||
'MSFwoEdGV4dBgEIAEoCUgBUgR0ZXh0iAEBEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1w'
|
||||
'Ii0KBFR5cGUSCgoGREVMRVRFEAASDQoJRURJVF9URVhUEAESCgoGT1BFTkVEEAJCFAoSX3Nlbm'
|
||||
'Rlcl9tZXNzYWdlX2lkQgcKBV90ZXh0GoUGCgVNZWRpYRIqChFzZW5kZXJfbWVzc2FnZV9pZBgB'
|
||||
'IAEoCVIPc2VuZGVyTWVzc2FnZUlkEjAKBHR5cGUYAiABKA4yHC5FbmNyeXB0ZWRDb250ZW50Lk'
|
||||
'1lZGlhLlR5cGVSBHR5cGUSRgodZGlzcGxheV9saW1pdF9pbl9taWxsaXNlY29uZHMYAyABKANI'
|
||||
'AFIaZGlzcGxheUxpbWl0SW5NaWxsaXNlY29uZHOIAQESNwoXcmVxdWlyZXNfYXV0aGVudGljYX'
|
||||
'Rpb24YBCABKAhSFnJlcXVpcmVzQXV0aGVudGljYXRpb24SHAoJdGltZXN0YW1wGAUgASgDUgl0'
|
||||
'aW1lc3RhbXASLQoQcXVvdGVfbWVzc2FnZV9pZBgGIAEoCUgBUg5xdW90ZU1lc3NhZ2VJZIgBAR'
|
||||
'IqCg5kb3dubG9hZF90b2tlbhgHIAEoDEgCUg1kb3dubG9hZFRva2VuiAEBEioKDmVuY3J5cHRp'
|
||||
'b25fa2V5GAggASgMSANSDWVuY3J5cHRpb25LZXmIAQESKgoOZW5jcnlwdGlvbl9tYWMYCSABKA'
|
||||
'xIBFINZW5jcnlwdGlvbk1hY4gBARIuChBlbmNyeXB0aW9uX25vbmNlGAogASgMSAVSD2VuY3J5'
|
||||
'cHRpb25Ob25jZYgBARI7ChdhZGRpdGlvbmFsX21lc3NhZ2VfZGF0YRgLIAEoDEgGUhVhZGRpdG'
|
||||
'lvbmFsTWVzc2FnZURhdGGIAQEiPgoEVHlwZRIMCghSRVVQTE9BRBAAEgkKBUlNQUdFEAESCQoF'
|
||||
'VklERU8QAhIHCgNHSUYQAxIJCgVBVURJTxAEQiAKHl9kaXNwbGF5X2xpbWl0X2luX21pbGxpc2'
|
||||
'Vjb25kc0ITChFfcXVvdGVfbWVzc2FnZV9pZEIRCg9fZG93bmxvYWRfdG9rZW5CEQoPX2VuY3J5'
|
||||
'cHRpb25fa2V5QhEKD19lbmNyeXB0aW9uX21hY0ITChFfZW5jcnlwdGlvbl9ub25jZUIaChhfYW'
|
||||
'RkaXRpb25hbF9tZXNzYWdlX2RhdGEaqQEKC01lZGlhVXBkYXRlEjYKBHR5cGUYASABKA4yIi5F'
|
||||
'bmNyeXB0ZWRDb250ZW50Lk1lZGlhVXBkYXRlLlR5cGVSBHR5cGUSKgoRdGFyZ2V0X21lc3NhZ2'
|
||||
'VfaWQYAiABKAlSD3RhcmdldE1lc3NhZ2VJZCI2CgRUeXBlEgwKCFJFT1BFTkVEEAASCgoGU1RP'
|
||||
'UkVEEAESFAoQREVDUllQVElPTl9FUlJPUhACGngKDkNvbnRhY3RSZXF1ZXN0EjkKBHR5cGUYAS'
|
||||
'ABKA4yJS5FbmNyeXB0ZWRDb250ZW50LkNvbnRhY3RSZXF1ZXN0LlR5cGVSBHR5cGUiKwoEVHlw'
|
||||
'ZRILCgdSRVFVRVNUEAASCgoGUkVKRUNUEAESCgoGQUNDRVBUEAIapAIKDUNvbnRhY3RVcGRhdG'
|
||||
'USOAoEdHlwZRgBIAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFVwZGF0ZS5UeXBlUgR0'
|
||||
'eXBlEjcKFWF2YXRhcl9zdmdfY29tcHJlc3NlZBgCIAEoDEgAUhNhdmF0YXJTdmdDb21wcmVzc2'
|
||||
'VkiAEBEh8KCHVzZXJuYW1lGAMgASgJSAFSCHVzZXJuYW1liAEBEiYKDGRpc3BsYXlfbmFtZRgE'
|
||||
'IAEoCUgCUgtkaXNwbGF5TmFtZYgBASIfCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAU'
|
||||
'IYChZfYXZhdGFyX3N2Z19jb21wcmVzc2VkQgsKCV91c2VybmFtZUIPCg1fZGlzcGxheV9uYW1l'
|
||||
'GtkBCghQdXNoS2V5cxIzCgR0eXBlGAEgASgOMh8uRW5jcnlwdGVkQ29udGVudC5QdXNoS2V5cy'
|
||||
'5UeXBlUgR0eXBlEhoKBmtleV9pZBgCIAEoA0gAUgVrZXlJZIgBARIVCgNrZXkYAyABKAxIAVID'
|
||||
'a2V5iAEBEiIKCmNyZWF0ZWRfYXQYBCABKANIAlIJY3JlYXRlZEF0iAEBIh8KBFR5cGUSCwoHUk'
|
||||
'VRVUVTVBAAEgoKBlVQREFURRABQgkKB19rZXlfaWRCBgoEX2tleUINCgtfY3JlYXRlZF9hdBqv'
|
||||
'AQoJRmxhbWVTeW5jEiMKDWZsYW1lX2NvdW50ZXIYASABKANSDGZsYW1lQ291bnRlchI5ChlsYX'
|
||||
'N0X2ZsYW1lX2NvdW50ZXJfY2hhbmdlGAIgASgDUhZsYXN0RmxhbWVDb3VudGVyQ2hhbmdlEh8K'
|
||||
'C2Jlc3RfZnJpZW5kGAMgASgIUgpiZXN0RnJpZW5kEiEKDGZvcmNlX3VwZGF0ZRgEIAEoCFILZm'
|
||||
'9yY2VVcGRhdGUaTQoPVHlwaW5nSW5kaWNhdG9yEhsKCWlzX3R5cGluZxgBIAEoCFIIaXNUeXBp'
|
||||
'bmcSHQoKY3JlYXRlZF9hdBgCIAEoA1IJY3JlYXRlZEF0Gj8KFFVzZXJEaXNjb3ZlcnlSZXF1ZX'
|
||||
'N0EicKD2N1cnJlbnRfdmVyc2lvbhgBIAEoDFIOY3VycmVudFZlcnNpb24aMQoTVXNlckRpc2Nv'
|
||||
'dmVyeVVwZGF0ZRIaCghtZXNzYWdlcxgBIAMoDFIIbWVzc2FnZXMaPQoUS2V5VmVyaWZpY2F0aW'
|
||||
'9uUHJvb2YSJQoOY2FsY3VsYXRlZF9tYWMYASABKAxSDWNhbGN1bGF0ZWRNYWMamwEKFFBhc3N3'
|
||||
'b3JkTGVzc1JlY292ZXJ5EjUKE3JlY292ZXJ5U2VjcmV0U2hhcmUYASABKAxIAFITcmVjb3Zlcn'
|
||||
'lTZWNyZXRTaGFyZYgBARIWCgZkZWxldGUYAiABKAhSBmRlbGV0ZRIcCgl0aHJlc2hvbGQYAyAB'
|
||||
'KANSCXRocmVzaG9sZEIWChRfcmVjb3ZlcnlTZWNyZXRTaGFyZRozCh1QYXNzd29yZExlc3NSZW'
|
||||
'NvdmVyeUhlYXJ0YmVhdBISCgRoYXNoGAEgASgMUgRoYXNoQgsKCV9ncm91cF9pZEIRCg9faXNf'
|
||||
'ZGlyZWN0X2NoYXRCGQoXX3NlbmRlcl9wcm9maWxlX2NvdW50ZXJCIAoeX3NlbmRlcl91c2VyX2'
|
||||
'Rpc2NvdmVyeV92ZXJzaW9uQhwKGl9hc2tfZm9yX2ZyaWVuZF9wcm9tb3Rpb25zQhEKD19tZXNz'
|
||||
'YWdlX3VwZGF0ZUIICgZfbWVkaWFCDwoNX21lZGlhX3VwZGF0ZUIRCg9fY29udGFjdF91cGRhdG'
|
||||
'VCEgoQX2NvbnRhY3RfcmVxdWVzdEINCgtfZmxhbWVfc3luY0IMCgpfcHVzaF9rZXlzQgsKCV9y'
|
||||
'ZWFjdGlvbkIPCg1fdGV4dF9tZXNzYWdlQg8KDV9ncm91cF9jcmVhdGVCDQoLX2dyb3VwX2pvaW'
|
||||
'5CDwoNX2dyb3VwX3VwZGF0ZUIaChhfcmVzZW5kX2dyb3VwX3B1YmxpY19rZXlCEQoPX2Vycm9y'
|
||||
'X21lc3NhZ2VzQhoKGF9hZGRpdGlvbmFsX2RhdGFfbWVzc2FnZUITChFfdHlwaW5nX2luZGljYX'
|
||||
'RvckIZChdfdXNlcl9kaXNjb3ZlcnlfcmVxdWVzdEIYChZfdXNlcl9kaXNjb3ZlcnlfdXBkYXRl'
|
||||
'QhkKF19rZXlfdmVyaWZpY2F0aW9uX3Byb29mQhgKFl9wYXNzd29yZGxlc3NfcmVjb3ZlcnlCIg'
|
||||
'ogX3Bhc3N3b3JkbGVzc19yZWNvdmVyeV9oZWFydGJlYXQ=');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,544 @@
|
|||
// This is a generated file - do not edit.
|
||||
//
|
||||
// Generated from passwordless_recovery.proto.
|
||||
|
||||
// @dart = 3.3
|
||||
|
||||
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// 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:fixnum/fixnum.dart' as $fixnum;
|
||||
import 'package:protobuf/protobuf.dart' as $pb;
|
||||
|
||||
export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions;
|
||||
|
||||
/// Send from the person who tries to recover their account.
|
||||
/// This can be done via a link, which will then be opend in the app of the contact.
|
||||
/// The contact than has to manualy select from which user he got the request.
|
||||
/// -> Using this phishing is harder, as the user has to manualy select the user to recovery
|
||||
/// -> The user who wants to recover his account does not need to remember her old username
|
||||
class RecoveryRequest extends $pb.GeneratedMessage {
|
||||
factory RecoveryRequest({
|
||||
$core.String? notificationId,
|
||||
$core.List<$core.int>? publicKey,
|
||||
}) {
|
||||
final result = create();
|
||||
if (notificationId != null) result.notificationId = notificationId;
|
||||
if (publicKey != null) result.publicKey = publicKey;
|
||||
return result;
|
||||
}
|
||||
|
||||
RecoveryRequest._();
|
||||
|
||||
factory RecoveryRequest.fromBuffer($core.List<$core.int> data,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(data, registry);
|
||||
factory RecoveryRequest.fromJson($core.String json,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(json, registry);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
_omitMessageNames ? '' : 'RecoveryRequest',
|
||||
package: const $pb.PackageName(
|
||||
_omitMessageNames ? '' : 'passwordless_recovery'),
|
||||
createEmptyInstance: create)
|
||||
..aOS(1, _omitFieldNames ? '' : 'notificationId')
|
||||
..a<$core.List<$core.int>>(
|
||||
2, _omitFieldNames ? '' : 'publicKey', $pb.PbFieldType.OY)
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
RecoveryRequest clone() => deepCopy();
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
RecoveryRequest copyWith(void Function(RecoveryRequest) updates) =>
|
||||
super.copyWith((message) => updates(message as RecoveryRequest))
|
||||
as RecoveryRequest;
|
||||
|
||||
@$core.override
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static RecoveryRequest create() => RecoveryRequest._();
|
||||
@$core.override
|
||||
RecoveryRequest createEmptyInstance() => create();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static RecoveryRequest getDefault() => _defaultInstance ??=
|
||||
$pb.GeneratedMessage.$_defaultFor<RecoveryRequest>(create);
|
||||
static RecoveryRequest? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.String get notificationId => $_getSZ(0);
|
||||
@$pb.TagNumber(1)
|
||||
set notificationId($core.String value) => $_setString(0, value);
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasNotificationId() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearNotificationId() => $_clearField(1);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$core.List<$core.int> get publicKey => $_getN(1);
|
||||
@$pb.TagNumber(2)
|
||||
set publicKey($core.List<$core.int> value) => $_setBytes(1, value);
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasPublicKey() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearPublicKey() => $_clearField(2);
|
||||
}
|
||||
|
||||
/// Used as envelope for TrustedFriendShare and RecoveryData
|
||||
class EncryptedEnvelope extends $pb.GeneratedMessage {
|
||||
factory EncryptedEnvelope({
|
||||
$core.List<$core.int>? encryptedData,
|
||||
$core.List<$core.int>? iv,
|
||||
$core.List<$core.int>? mac,
|
||||
}) {
|
||||
final result = create();
|
||||
if (encryptedData != null) result.encryptedData = encryptedData;
|
||||
if (iv != null) result.iv = iv;
|
||||
if (mac != null) result.mac = mac;
|
||||
return result;
|
||||
}
|
||||
|
||||
EncryptedEnvelope._();
|
||||
|
||||
factory EncryptedEnvelope.fromBuffer($core.List<$core.int> data,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(data, registry);
|
||||
factory EncryptedEnvelope.fromJson($core.String json,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(json, registry);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
_omitMessageNames ? '' : 'EncryptedEnvelope',
|
||||
package: const $pb.PackageName(
|
||||
_omitMessageNames ? '' : 'passwordless_recovery'),
|
||||
createEmptyInstance: create)
|
||||
..a<$core.List<$core.int>>(
|
||||
1, _omitFieldNames ? '' : 'encryptedData', $pb.PbFieldType.OY)
|
||||
..a<$core.List<$core.int>>(
|
||||
2, _omitFieldNames ? '' : 'iv', $pb.PbFieldType.OY)
|
||||
..a<$core.List<$core.int>>(
|
||||
3, _omitFieldNames ? '' : 'mac', $pb.PbFieldType.OY)
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
EncryptedEnvelope clone() => deepCopy();
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
EncryptedEnvelope copyWith(void Function(EncryptedEnvelope) updates) =>
|
||||
super.copyWith((message) => updates(message as EncryptedEnvelope))
|
||||
as EncryptedEnvelope;
|
||||
|
||||
@$core.override
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static EncryptedEnvelope create() => EncryptedEnvelope._();
|
||||
@$core.override
|
||||
EncryptedEnvelope createEmptyInstance() => create();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static EncryptedEnvelope getDefault() => _defaultInstance ??=
|
||||
$pb.GeneratedMessage.$_defaultFor<EncryptedEnvelope>(create);
|
||||
static EncryptedEnvelope? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.List<$core.int> get encryptedData => $_getN(0);
|
||||
@$pb.TagNumber(1)
|
||||
set encryptedData($core.List<$core.int> value) => $_setBytes(0, value);
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasEncryptedData() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearEncryptedData() => $_clearField(1);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$core.List<$core.int> get iv => $_getN(1);
|
||||
@$pb.TagNumber(2)
|
||||
set iv($core.List<$core.int> value) => $_setBytes(1, value);
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasIv() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearIv() => $_clearField(2);
|
||||
|
||||
@$pb.TagNumber(3)
|
||||
$core.List<$core.int> get mac => $_getN(2);
|
||||
@$pb.TagNumber(3)
|
||||
set mac($core.List<$core.int> value) => $_setBytes(2, value);
|
||||
@$pb.TagNumber(3)
|
||||
$core.bool hasMac() => $_has(2);
|
||||
@$pb.TagNumber(3)
|
||||
void clearMac() => $_clearField(3);
|
||||
}
|
||||
|
||||
class TrustedFriendShare_User extends $pb.GeneratedMessage {
|
||||
factory TrustedFriendShare_User({
|
||||
$fixnum.Int64? userId,
|
||||
$core.String? displayName,
|
||||
$core.List<$core.int>? avatar,
|
||||
}) {
|
||||
final result = create();
|
||||
if (userId != null) result.userId = userId;
|
||||
if (displayName != null) result.displayName = displayName;
|
||||
if (avatar != null) result.avatar = avatar;
|
||||
return result;
|
||||
}
|
||||
|
||||
TrustedFriendShare_User._();
|
||||
|
||||
factory TrustedFriendShare_User.fromBuffer($core.List<$core.int> data,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(data, registry);
|
||||
factory TrustedFriendShare_User.fromJson($core.String json,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(json, registry);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
_omitMessageNames ? '' : 'TrustedFriendShare.User',
|
||||
package: const $pb.PackageName(
|
||||
_omitMessageNames ? '' : 'passwordless_recovery'),
|
||||
createEmptyInstance: create)
|
||||
..aInt64(1, _omitFieldNames ? '' : 'userId')
|
||||
..aOS(2, _omitFieldNames ? '' : 'displayName')
|
||||
..a<$core.List<$core.int>>(
|
||||
3, _omitFieldNames ? '' : 'avatar', $pb.PbFieldType.OY)
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
TrustedFriendShare_User clone() => deepCopy();
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
TrustedFriendShare_User copyWith(
|
||||
void Function(TrustedFriendShare_User) updates) =>
|
||||
super.copyWith((message) => updates(message as TrustedFriendShare_User))
|
||||
as TrustedFriendShare_User;
|
||||
|
||||
@$core.override
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static TrustedFriendShare_User create() => TrustedFriendShare_User._();
|
||||
@$core.override
|
||||
TrustedFriendShare_User createEmptyInstance() => create();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static TrustedFriendShare_User getDefault() => _defaultInstance ??=
|
||||
$pb.GeneratedMessage.$_defaultFor<TrustedFriendShare_User>(create);
|
||||
static TrustedFriendShare_User? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$fixnum.Int64 get userId => $_getI64(0);
|
||||
@$pb.TagNumber(1)
|
||||
set userId($fixnum.Int64 value) => $_setInt64(0, value);
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasUserId() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearUserId() => $_clearField(1);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$core.String get displayName => $_getSZ(1);
|
||||
@$pb.TagNumber(2)
|
||||
set displayName($core.String value) => $_setString(1, value);
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasDisplayName() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearDisplayName() => $_clearField(2);
|
||||
|
||||
@$pb.TagNumber(3)
|
||||
$core.List<$core.int> get avatar => $_getN(2);
|
||||
@$pb.TagNumber(3)
|
||||
set avatar($core.List<$core.int> value) => $_setBytes(2, value);
|
||||
@$pb.TagNumber(3)
|
||||
$core.bool hasAvatar() => $_has(2);
|
||||
@$pb.TagNumber(3)
|
||||
void clearAvatar() => $_clearField(3);
|
||||
}
|
||||
|
||||
/// Send from the trusted friend to
|
||||
/// This is encrypted with the received public key.
|
||||
class TrustedFriendShare extends $pb.GeneratedMessage {
|
||||
factory TrustedFriendShare({
|
||||
TrustedFriendShare_User? trustedFriend,
|
||||
TrustedFriendShare_User? shareUser,
|
||||
$core.int? threshold,
|
||||
$core.List<$core.int>? sharedSecretData,
|
||||
}) {
|
||||
final result = create();
|
||||
if (trustedFriend != null) result.trustedFriend = trustedFriend;
|
||||
if (shareUser != null) result.shareUser = shareUser;
|
||||
if (threshold != null) result.threshold = threshold;
|
||||
if (sharedSecretData != null) result.sharedSecretData = sharedSecretData;
|
||||
return result;
|
||||
}
|
||||
|
||||
TrustedFriendShare._();
|
||||
|
||||
factory TrustedFriendShare.fromBuffer($core.List<$core.int> data,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(data, registry);
|
||||
factory TrustedFriendShare.fromJson($core.String json,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(json, registry);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
_omitMessageNames ? '' : 'TrustedFriendShare',
|
||||
package: const $pb.PackageName(
|
||||
_omitMessageNames ? '' : 'passwordless_recovery'),
|
||||
createEmptyInstance: create)
|
||||
..aOM<TrustedFriendShare_User>(1, _omitFieldNames ? '' : 'trustedFriend',
|
||||
subBuilder: TrustedFriendShare_User.create)
|
||||
..aOM<TrustedFriendShare_User>(2, _omitFieldNames ? '' : 'shareUser',
|
||||
subBuilder: TrustedFriendShare_User.create)
|
||||
..aI(3, _omitFieldNames ? '' : 'threshold')
|
||||
..a<$core.List<$core.int>>(
|
||||
4, _omitFieldNames ? '' : 'sharedSecretData', $pb.PbFieldType.OY)
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
TrustedFriendShare clone() => deepCopy();
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
TrustedFriendShare copyWith(void Function(TrustedFriendShare) updates) =>
|
||||
super.copyWith((message) => updates(message as TrustedFriendShare))
|
||||
as TrustedFriendShare;
|
||||
|
||||
@$core.override
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static TrustedFriendShare create() => TrustedFriendShare._();
|
||||
@$core.override
|
||||
TrustedFriendShare createEmptyInstance() => create();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static TrustedFriendShare getDefault() => _defaultInstance ??=
|
||||
$pb.GeneratedMessage.$_defaultFor<TrustedFriendShare>(create);
|
||||
static TrustedFriendShare? _defaultInstance;
|
||||
|
||||
/// This allows to display the user which user has send him his recovery data.
|
||||
@$pb.TagNumber(1)
|
||||
TrustedFriendShare_User get trustedFriend => $_getN(0);
|
||||
@$pb.TagNumber(1)
|
||||
set trustedFriend(TrustedFriendShare_User value) => $_setField(1, value);
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasTrustedFriend() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearTrustedFriend() => $_clearField(1);
|
||||
@$pb.TagNumber(1)
|
||||
TrustedFriendShare_User ensureTrustedFriend() => $_ensure(0);
|
||||
|
||||
/// This allows to display the userdata, showing that he is recovering the correct person.
|
||||
@$pb.TagNumber(2)
|
||||
TrustedFriendShare_User get shareUser => $_getN(1);
|
||||
@$pb.TagNumber(2)
|
||||
set shareUser(TrustedFriendShare_User value) => $_setField(2, value);
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasShareUser() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearShareUser() => $_clearField(2);
|
||||
@$pb.TagNumber(2)
|
||||
TrustedFriendShare_User ensureShareUser() => $_ensure(1);
|
||||
|
||||
/// The minimum threshold required to decrypte the shares.
|
||||
@$pb.TagNumber(3)
|
||||
$core.int get threshold => $_getIZ(2);
|
||||
@$pb.TagNumber(3)
|
||||
set threshold($core.int value) => $_setSignedInt32(2, value);
|
||||
@$pb.TagNumber(3)
|
||||
$core.bool hasThreshold() => $_has(2);
|
||||
@$pb.TagNumber(3)
|
||||
void clearThreshold() => $_clearField(3);
|
||||
|
||||
/// The actual share which will become: SharedSecretData
|
||||
@$pb.TagNumber(4)
|
||||
$core.List<$core.int> get sharedSecretData => $_getN(3);
|
||||
@$pb.TagNumber(4)
|
||||
set sharedSecretData($core.List<$core.int> value) => $_setBytes(3, value);
|
||||
@$pb.TagNumber(4)
|
||||
$core.bool hasSharedSecretData() => $_has(3);
|
||||
@$pb.TagNumber(4)
|
||||
void clearSharedSecretData() => $_clearField(4);
|
||||
}
|
||||
|
||||
/// The user identity keys. Can be used to restore the data backup.
|
||||
class RecoveryData extends $pb.GeneratedMessage {
|
||||
factory RecoveryData({
|
||||
$fixnum.Int64? userId,
|
||||
$core.List<$core.int>? keyManager,
|
||||
}) {
|
||||
final result = create();
|
||||
if (userId != null) result.userId = userId;
|
||||
if (keyManager != null) result.keyManager = keyManager;
|
||||
return result;
|
||||
}
|
||||
|
||||
RecoveryData._();
|
||||
|
||||
factory RecoveryData.fromBuffer($core.List<$core.int> data,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(data, registry);
|
||||
factory RecoveryData.fromJson($core.String json,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(json, registry);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
_omitMessageNames ? '' : 'RecoveryData',
|
||||
package: const $pb.PackageName(
|
||||
_omitMessageNames ? '' : 'passwordless_recovery'),
|
||||
createEmptyInstance: create)
|
||||
..aInt64(1, _omitFieldNames ? '' : 'userId')
|
||||
..a<$core.List<$core.int>>(
|
||||
3, _omitFieldNames ? '' : 'keyManager', $pb.PbFieldType.OY)
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
RecoveryData clone() => deepCopy();
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
RecoveryData copyWith(void Function(RecoveryData) updates) =>
|
||||
super.copyWith((message) => updates(message as RecoveryData))
|
||||
as RecoveryData;
|
||||
|
||||
@$core.override
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static RecoveryData create() => RecoveryData._();
|
||||
@$core.override
|
||||
RecoveryData createEmptyInstance() => create();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static RecoveryData getDefault() => _defaultInstance ??=
|
||||
$pb.GeneratedMessage.$_defaultFor<RecoveryData>(create);
|
||||
static RecoveryData? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$fixnum.Int64 get userId => $_getI64(0);
|
||||
@$pb.TagNumber(1)
|
||||
set userId($fixnum.Int64 value) => $_setInt64(0, value);
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasUserId() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearUserId() => $_clearField(1);
|
||||
|
||||
@$pb.TagNumber(3)
|
||||
$core.List<$core.int> get keyManager => $_getN(1);
|
||||
@$pb.TagNumber(3)
|
||||
set keyManager($core.List<$core.int> value) => $_setBytes(1, value);
|
||||
@$pb.TagNumber(3)
|
||||
$core.bool hasKeyManager() => $_has(1);
|
||||
@$pb.TagNumber(3)
|
||||
void clearKeyManager() => $_clearField(3);
|
||||
}
|
||||
|
||||
/// After received all shares this is decrypted by the user restoring its own
|
||||
class SharedSecretData extends $pb.GeneratedMessage {
|
||||
factory SharedSecretData({
|
||||
$core.List<$core.int>? recoveryData,
|
||||
$core.List<$core.int>? encryptedServerKeyNonce,
|
||||
$core.List<$core.int>? pinSeed,
|
||||
$core.List<$core.int>? pinUnlockToken,
|
||||
$core.String? emailHint,
|
||||
}) {
|
||||
final result = create();
|
||||
if (recoveryData != null) result.recoveryData = recoveryData;
|
||||
if (encryptedServerKeyNonce != null)
|
||||
result.encryptedServerKeyNonce = encryptedServerKeyNonce;
|
||||
if (pinSeed != null) result.pinSeed = pinSeed;
|
||||
if (pinUnlockToken != null) result.pinUnlockToken = pinUnlockToken;
|
||||
if (emailHint != null) result.emailHint = emailHint;
|
||||
return result;
|
||||
}
|
||||
|
||||
SharedSecretData._();
|
||||
|
||||
factory SharedSecretData.fromBuffer($core.List<$core.int> data,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(data, registry);
|
||||
factory SharedSecretData.fromJson($core.String json,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(json, registry);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
_omitMessageNames ? '' : 'SharedSecretData',
|
||||
package: const $pb.PackageName(
|
||||
_omitMessageNames ? '' : 'passwordless_recovery'),
|
||||
createEmptyInstance: create)
|
||||
..a<$core.List<$core.int>>(
|
||||
1, _omitFieldNames ? '' : 'recoveryData', $pb.PbFieldType.OY)
|
||||
..a<$core.List<$core.int>>(
|
||||
3, _omitFieldNames ? '' : 'encryptedServerKeyNonce', $pb.PbFieldType.OY)
|
||||
..a<$core.List<$core.int>>(
|
||||
4, _omitFieldNames ? '' : 'pinSeed', $pb.PbFieldType.OY)
|
||||
..a<$core.List<$core.int>>(
|
||||
5, _omitFieldNames ? '' : 'pinUnlockToken', $pb.PbFieldType.OY)
|
||||
..aOS(6, _omitFieldNames ? '' : 'emailHint')
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
SharedSecretData clone() => deepCopy();
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
SharedSecretData copyWith(void Function(SharedSecretData) updates) =>
|
||||
super.copyWith((message) => updates(message as SharedSecretData))
|
||||
as SharedSecretData;
|
||||
|
||||
@$core.override
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static SharedSecretData create() => SharedSecretData._();
|
||||
@$core.override
|
||||
SharedSecretData createEmptyInstance() => create();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static SharedSecretData getDefault() => _defaultInstance ??=
|
||||
$pb.GeneratedMessage.$_defaultFor<SharedSecretData>(create);
|
||||
static SharedSecretData? _defaultInstance;
|
||||
|
||||
/// The recovery data is encrypted in case a second factor was chosen.
|
||||
@$pb.TagNumber(1)
|
||||
$core.List<$core.int> get recoveryData => $_getN(0);
|
||||
@$pb.TagNumber(1)
|
||||
set recoveryData($core.List<$core.int> value) => $_setBytes(0, value);
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasRecoveryData() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearRecoveryData() => $_clearField(1);
|
||||
|
||||
@$pb.TagNumber(3)
|
||||
$core.List<$core.int> get encryptedServerKeyNonce => $_getN(1);
|
||||
@$pb.TagNumber(3)
|
||||
set encryptedServerKeyNonce($core.List<$core.int> value) =>
|
||||
$_setBytes(1, value);
|
||||
@$pb.TagNumber(3)
|
||||
$core.bool hasEncryptedServerKeyNonce() => $_has(1);
|
||||
@$pb.TagNumber(3)
|
||||
void clearEncryptedServerKeyNonce() => $_clearField(3);
|
||||
|
||||
@$pb.TagNumber(4)
|
||||
$core.List<$core.int> get pinSeed => $_getN(2);
|
||||
@$pb.TagNumber(4)
|
||||
set pinSeed($core.List<$core.int> value) => $_setBytes(2, value);
|
||||
@$pb.TagNumber(4)
|
||||
$core.bool hasPinSeed() => $_has(2);
|
||||
@$pb.TagNumber(4)
|
||||
void clearPinSeed() => $_clearField(4);
|
||||
|
||||
@$pb.TagNumber(5)
|
||||
$core.List<$core.int> get pinUnlockToken => $_getN(3);
|
||||
@$pb.TagNumber(5)
|
||||
set pinUnlockToken($core.List<$core.int> value) => $_setBytes(3, value);
|
||||
@$pb.TagNumber(5)
|
||||
$core.bool hasPinUnlockToken() => $_has(3);
|
||||
@$pb.TagNumber(5)
|
||||
void clearPinUnlockToken() => $_clearField(5);
|
||||
|
||||
@$pb.TagNumber(6)
|
||||
$core.String get emailHint => $_getSZ(4);
|
||||
@$pb.TagNumber(6)
|
||||
set emailHint($core.String value) => $_setString(4, value);
|
||||
@$pb.TagNumber(6)
|
||||
$core.bool hasEmailHint() => $_has(4);
|
||||
@$pb.TagNumber(6)
|
||||
void clearEmailHint() => $_clearField(6);
|
||||
}
|
||||
|
||||
const $core.bool _omitFieldNames =
|
||||
$core.bool.fromEnvironment('protobuf.omit_field_names');
|
||||
const $core.bool _omitMessageNames =
|
||||
$core.bool.fromEnvironment('protobuf.omit_message_names');
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
// This is a generated file - do not edit.
|
||||
//
|
||||
// Generated from passwordless_recovery.proto.
|
||||
|
||||
// @dart = 3.3
|
||||
|
||||
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// 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
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
// This is a generated file - do not edit.
|
||||
//
|
||||
// Generated from passwordless_recovery.proto.
|
||||
|
||||
// @dart = 3.3
|
||||
|
||||
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// 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
|
||||
// ignore_for_file: unused_import
|
||||
|
||||
import 'dart:convert' as $convert;
|
||||
import 'dart:core' as $core;
|
||||
import 'dart:typed_data' as $typed_data;
|
||||
|
||||
@$core.Deprecated('Use recoveryRequestDescriptor instead')
|
||||
const RecoveryRequest$json = {
|
||||
'1': 'RecoveryRequest',
|
||||
'2': [
|
||||
{'1': 'notification_id', '3': 1, '4': 1, '5': 9, '10': 'notificationId'},
|
||||
{'1': 'public_key', '3': 2, '4': 1, '5': 12, '10': 'publicKey'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `RecoveryRequest`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List recoveryRequestDescriptor = $convert.base64Decode(
|
||||
'Cg9SZWNvdmVyeVJlcXVlc3QSJwoPbm90aWZpY2F0aW9uX2lkGAEgASgJUg5ub3RpZmljYXRpb2'
|
||||
'5JZBIdCgpwdWJsaWNfa2V5GAIgASgMUglwdWJsaWNLZXk=');
|
||||
|
||||
@$core.Deprecated('Use encryptedEnvelopeDescriptor instead')
|
||||
const EncryptedEnvelope$json = {
|
||||
'1': 'EncryptedEnvelope',
|
||||
'2': [
|
||||
{'1': 'encrypted_data', '3': 1, '4': 1, '5': 12, '10': 'encryptedData'},
|
||||
{'1': 'iv', '3': 2, '4': 1, '5': 12, '10': 'iv'},
|
||||
{'1': 'mac', '3': 3, '4': 1, '5': 12, '10': 'mac'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `EncryptedEnvelope`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List encryptedEnvelopeDescriptor = $convert.base64Decode(
|
||||
'ChFFbmNyeXB0ZWRFbnZlbG9wZRIlCg5lbmNyeXB0ZWRfZGF0YRgBIAEoDFINZW5jcnlwdGVkRG'
|
||||
'F0YRIOCgJpdhgCIAEoDFICaXYSEAoDbWFjGAMgASgMUgNtYWM=');
|
||||
|
||||
@$core.Deprecated('Use trustedFriendShareDescriptor instead')
|
||||
const TrustedFriendShare$json = {
|
||||
'1': 'TrustedFriendShare',
|
||||
'2': [
|
||||
{
|
||||
'1': 'trusted_friend',
|
||||
'3': 1,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.passwordless_recovery.TrustedFriendShare.User',
|
||||
'10': 'trustedFriend'
|
||||
},
|
||||
{
|
||||
'1': 'share_user',
|
||||
'3': 2,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.passwordless_recovery.TrustedFriendShare.User',
|
||||
'10': 'shareUser'
|
||||
},
|
||||
{'1': 'threshold', '3': 3, '4': 1, '5': 5, '10': 'threshold'},
|
||||
{
|
||||
'1': 'shared_secret_data',
|
||||
'3': 4,
|
||||
'4': 1,
|
||||
'5': 12,
|
||||
'10': 'sharedSecretData'
|
||||
},
|
||||
],
|
||||
'3': [TrustedFriendShare_User$json],
|
||||
};
|
||||
|
||||
@$core.Deprecated('Use trustedFriendShareDescriptor instead')
|
||||
const TrustedFriendShare_User$json = {
|
||||
'1': 'User',
|
||||
'2': [
|
||||
{'1': 'user_id', '3': 1, '4': 1, '5': 3, '10': 'userId'},
|
||||
{'1': 'display_name', '3': 2, '4': 1, '5': 9, '10': 'displayName'},
|
||||
{'1': 'avatar', '3': 3, '4': 1, '5': 12, '10': 'avatar'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `TrustedFriendShare`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List trustedFriendShareDescriptor = $convert.base64Decode(
|
||||
'ChJUcnVzdGVkRnJpZW5kU2hhcmUSVQoOdHJ1c3RlZF9mcmllbmQYASABKAsyLi5wYXNzd29yZG'
|
||||
'xlc3NfcmVjb3ZlcnkuVHJ1c3RlZEZyaWVuZFNoYXJlLlVzZXJSDXRydXN0ZWRGcmllbmQSTQoK'
|
||||
'c2hhcmVfdXNlchgCIAEoCzIuLnBhc3N3b3JkbGVzc19yZWNvdmVyeS5UcnVzdGVkRnJpZW5kU2'
|
||||
'hhcmUuVXNlclIJc2hhcmVVc2VyEhwKCXRocmVzaG9sZBgDIAEoBVIJdGhyZXNob2xkEiwKEnNo'
|
||||
'YXJlZF9zZWNyZXRfZGF0YRgEIAEoDFIQc2hhcmVkU2VjcmV0RGF0YRpaCgRVc2VyEhcKB3VzZX'
|
||||
'JfaWQYASABKANSBnVzZXJJZBIhCgxkaXNwbGF5X25hbWUYAiABKAlSC2Rpc3BsYXlOYW1lEhYK'
|
||||
'BmF2YXRhchgDIAEoDFIGYXZhdGFy');
|
||||
|
||||
@$core.Deprecated('Use recoveryDataDescriptor instead')
|
||||
const RecoveryData$json = {
|
||||
'1': 'RecoveryData',
|
||||
'2': [
|
||||
{'1': 'user_id', '3': 1, '4': 1, '5': 3, '10': 'userId'},
|
||||
{'1': 'key_manager', '3': 3, '4': 1, '5': 12, '10': 'keyManager'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `RecoveryData`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List recoveryDataDescriptor = $convert.base64Decode(
|
||||
'CgxSZWNvdmVyeURhdGESFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklkEh8KC2tleV9tYW5hZ2VyGA'
|
||||
'MgASgMUgprZXlNYW5hZ2Vy');
|
||||
|
||||
@$core.Deprecated('Use sharedSecretDataDescriptor instead')
|
||||
const SharedSecretData$json = {
|
||||
'1': 'SharedSecretData',
|
||||
'2': [
|
||||
{'1': 'recovery_data', '3': 1, '4': 1, '5': 12, '10': 'recoveryData'},
|
||||
{
|
||||
'1': 'encrypted_server_key_nonce',
|
||||
'3': 3,
|
||||
'4': 1,
|
||||
'5': 12,
|
||||
'9': 0,
|
||||
'10': 'encryptedServerKeyNonce',
|
||||
'17': true
|
||||
},
|
||||
{
|
||||
'1': 'pin_seed',
|
||||
'3': 4,
|
||||
'4': 1,
|
||||
'5': 12,
|
||||
'9': 1,
|
||||
'10': 'pinSeed',
|
||||
'17': true
|
||||
},
|
||||
{
|
||||
'1': 'pin_unlock_token',
|
||||
'3': 5,
|
||||
'4': 1,
|
||||
'5': 12,
|
||||
'9': 2,
|
||||
'10': 'pinUnlockToken',
|
||||
'17': true
|
||||
},
|
||||
{
|
||||
'1': 'email_hint',
|
||||
'3': 6,
|
||||
'4': 1,
|
||||
'5': 9,
|
||||
'9': 3,
|
||||
'10': 'emailHint',
|
||||
'17': true
|
||||
},
|
||||
],
|
||||
'8': [
|
||||
{'1': '_encrypted_server_key_nonce'},
|
||||
{'1': '_pin_seed'},
|
||||
{'1': '_pin_unlock_token'},
|
||||
{'1': '_email_hint'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `SharedSecretData`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List sharedSecretDataDescriptor = $convert.base64Decode(
|
||||
'ChBTaGFyZWRTZWNyZXREYXRhEiMKDXJlY292ZXJ5X2RhdGEYASABKAxSDHJlY292ZXJ5RGF0YR'
|
||||
'JAChplbmNyeXB0ZWRfc2VydmVyX2tleV9ub25jZRgDIAEoDEgAUhdlbmNyeXB0ZWRTZXJ2ZXJL'
|
||||
'ZXlOb25jZYgBARIeCghwaW5fc2VlZBgEIAEoDEgBUgdwaW5TZWVkiAEBEi0KEHBpbl91bmxvY2'
|
||||
'tfdG9rZW4YBSABKAxIAlIOcGluVW5sb2NrVG9rZW6IAQESIgoKZW1haWxfaGludBgGIAEoCUgD'
|
||||
'UgllbWFpbEhpbnSIAQFCHQobX2VuY3J5cHRlZF9zZXJ2ZXJfa2V5X25vbmNlQgsKCV9waW5fc2'
|
||||
'VlZEITChFfcGluX3VubG9ja190b2tlbkINCgtfZW1haWxfaGludA==');
|
||||
|
|
@ -58,6 +58,8 @@ message EncryptedContent {
|
|||
optional UserDiscoveryRequest user_discovery_request = 22;
|
||||
optional UserDiscoveryUpdate user_discovery_update = 23;
|
||||
optional KeyVerificationProof key_verification_proof = 24;
|
||||
optional PasswordLessRecovery passwordless_recovery = 26;
|
||||
optional PasswordLessRecoveryHeartbeat passwordless_recovery_heartbeat = 27;
|
||||
|
||||
message ErrorMessages {
|
||||
enum Type {
|
||||
|
|
@ -82,9 +84,7 @@ message EncryptedContent {
|
|||
bytes group_public_key = 1;
|
||||
}
|
||||
|
||||
message ResendGroupPublicKey {
|
||||
|
||||
}
|
||||
message ResendGroupPublicKey {}
|
||||
|
||||
message GroupUpdate {
|
||||
string group_action_type = 1; // GroupActionType.name
|
||||
|
|
@ -217,4 +217,14 @@ message EncryptedContent {
|
|||
bytes calculated_mac = 1;
|
||||
}
|
||||
|
||||
message PasswordLessRecovery {
|
||||
optional bytes recoverySecretShare = 1;
|
||||
bool delete = 2;
|
||||
int64 threshold = 3;
|
||||
}
|
||||
|
||||
message PasswordLessRecoveryHeartbeat {
|
||||
bytes hash = 1;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ package passwordless_recovery;
|
|||
// -> Using this phishing is harder, as the user has to manualy select the user to recovery
|
||||
// -> The user who wants to recover his account does not need to remember her old username
|
||||
message RecoveryRequest {
|
||||
int64 temp_id = 1;
|
||||
string notification_id = 1;
|
||||
bytes public_key = 2;
|
||||
}
|
||||
|
||||
|
|
@ -46,40 +46,24 @@ message TrustedFriendShare {
|
|||
}
|
||||
}
|
||||
|
||||
// After received all shares this is decrypted by the user restoring its own
|
||||
message SharedSecretData {
|
||||
|
||||
// No second factor was selected
|
||||
optional RecoveryData recovery_data = 1;
|
||||
|
||||
|
||||
// Server has
|
||||
optional SecondFactorMail second_factor_mail = 2;
|
||||
optional SecondFactorPin second_factor_pin = 3;
|
||||
|
||||
// The recovery data in case a second factor was used
|
||||
// The decryption key is loaded from the server either using the PIN or the MAIL
|
||||
optional bytes recovery_data_encrypted = 4;
|
||||
|
||||
|
||||
message SecondFactorPin {
|
||||
// Required to try the PIN to get the share from the server.
|
||||
// This prevents that someone else can lock the pin, as the server only
|
||||
// allows 3 tries then after 1 day again 3 tries until the key is deleted.
|
||||
bytes unlock_token = 1;
|
||||
// This never is send to the server but used to hash the pin before sending it to the server.
|
||||
// This prevents that the server every knows the shot 4-diget PIN.
|
||||
bytes pin_seed = 2;
|
||||
}
|
||||
|
||||
message SecondFactorMail {}
|
||||
|
||||
}
|
||||
|
||||
// The data which is recovered at the end.
|
||||
// The backup_master_key allows to recover the actual backup uploaded in the background to the server.
|
||||
// In case the backup is not available any more the user can use its user_id and his private_key to requister as a new user.
|
||||
// The user identity keys. Can be used to restore the data backup.
|
||||
message RecoveryData {
|
||||
int64 user_id = 1;
|
||||
bytes key_manager = 3;
|
||||
}
|
||||
|
||||
// After received all shares this is decrypted by the user restoring its own
|
||||
message SharedSecretData {
|
||||
|
||||
// The recovery data is encrypted in case a second factor was chosen.
|
||||
bytes recovery_data = 1;
|
||||
|
||||
optional bytes encrypted_server_key_nonce = 3;
|
||||
|
||||
optional bytes pin_seed = 4;
|
||||
optional bytes pin_unlock_token = 5;
|
||||
|
||||
optional string email_hint = 6;
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -13,7 +13,8 @@ import 'package:twonly/src/visual/views/contact/add_new_contact.view.dart';
|
|||
import 'package:twonly/src/visual/views/contact/contact.view.dart';
|
||||
import 'package:twonly/src/visual/views/groups/group.view.dart';
|
||||
import 'package:twonly/src/visual/views/groups/group_create_select_members.view.dart';
|
||||
import 'package:twonly/src/visual/views/onboarding/recover.view.dart';
|
||||
import 'package:twonly/src/visual/views/onboarding/recover_password.view.dart';
|
||||
import 'package:twonly/src/visual/views/onboarding/recover_passwordless.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';
|
||||
|
|
@ -39,7 +40,6 @@ import 'package:twonly/src/visual/views/settings/help/help.view.dart';
|
|||
import 'package:twonly/src/visual/views/settings/notification.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/privacy.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/privacy/block_users.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/privacy/profile_selection.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/privacy/user_discovery.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/profile/modify_avatar.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/profile/profile.view.dart';
|
||||
|
|
@ -58,6 +58,13 @@ final routerProvider = GoRouter(
|
|||
path: Routes.home,
|
||||
builder: (context, state) => const AppMainWidget(initialPage: 1),
|
||||
),
|
||||
GoRoute(
|
||||
path: Routes.recoverPasswordless,
|
||||
builder: (context, state) {
|
||||
final token = state.extra as String?;
|
||||
return RecoverPasswordless(initialEmailToken: token);
|
||||
},
|
||||
),
|
||||
|
||||
// Chats
|
||||
GoRoute(
|
||||
|
|
@ -135,7 +142,13 @@ final routerProvider = GoRouter(
|
|||
GoRoute(
|
||||
path: Routes.cameraQRScanner,
|
||||
builder: (context, state) {
|
||||
return const QrCodeScannerView();
|
||||
final extra = state.extra as Map<String, dynamic>?;
|
||||
final contact = extra?['contact'] as Contact?;
|
||||
final openToVerify = extra?['openToVerify'] as bool? ?? false;
|
||||
return QrCodeScannerView(
|
||||
contact: contact,
|
||||
openToVerify: openToVerify,
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
|
|
@ -206,10 +219,6 @@ final routerProvider = GoRouter(
|
|||
path: 'user_discovery',
|
||||
builder: (context, state) => const UserDiscoverySettingsView(),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'profile_selection',
|
||||
builder: (context, state) => const ProfileSelectionSettingsView(),
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
|
|
@ -240,7 +249,9 @@ final routerProvider = GoRouter(
|
|||
routes: [
|
||||
GoRoute(
|
||||
path: 'verifybadge',
|
||||
builder: (context, state) => const VerificationBadeFaqView(),
|
||||
builder: (context, state) => VerificationBadeFaqView(
|
||||
contact: state.extra as Contact?,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import 'package:twonly/src/services/flame.service.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/passwordless_recovery.service.dart';
|
||||
import 'package:twonly/src/services/signal/identity.signal.dart';
|
||||
import 'package:twonly/src/services/signal/protocol_state.signal.dart';
|
||||
import 'package:twonly/src/services/signal/utils.signal.dart';
|
||||
|
|
@ -139,6 +140,7 @@ class ApiService {
|
|||
unawaited(fetchGroupStatesForUnjoinedGroups());
|
||||
unawaited(fetchMissingGroupPublicKey());
|
||||
unawaited(checkForDeletedUsernames());
|
||||
unawaited(PasswordlessRecoveryService.performHeartbeat());
|
||||
|
||||
unawaited(UserDiscoveryService.checkForNewAnnouncedUsers());
|
||||
|
||||
|
|
@ -684,11 +686,11 @@ class ApiService {
|
|||
final req = createClientToServerFromHandshake(handshake);
|
||||
final result = await sendRequestSync(req, authenticated: false);
|
||||
if (result.isError) {
|
||||
Log.error('could not request proof of work params', result);
|
||||
Log.error('could not request proof of work params', error: result);
|
||||
if (result.error == ErrorCode.RegistrationDisabled) {
|
||||
return (null, true);
|
||||
}
|
||||
Log.error('could not request proof of work params', result);
|
||||
Log.error('could not request proof of work params', error: result);
|
||||
return (null, false);
|
||||
}
|
||||
return (result.value.proofOfWork as Response_ProofOfWork, false);
|
||||
|
|
@ -758,6 +760,85 @@ class ApiService {
|
|||
return sendRequestSync(req, contactId: userId.toInt());
|
||||
}
|
||||
|
||||
Future<Result> registerPasswordLessRecovery(
|
||||
List<int> encryptedServerKey,
|
||||
List<int>? pinUnlockToken,
|
||||
) async {
|
||||
final req = createClientToServerFromApplicationData(
|
||||
ApplicationData(
|
||||
registerPasswordlessRecovery:
|
||||
ApplicationData_RegisterPasswordLessRecovery(
|
||||
encryptedServerKey: encryptedServerKey,
|
||||
pinUnlockToken: pinUnlockToken,
|
||||
),
|
||||
),
|
||||
);
|
||||
return sendRequestSync(req);
|
||||
}
|
||||
|
||||
Future<Result> getServerKeyForPasswordlessRecovery({
|
||||
required int userId,
|
||||
List<int>? encryptedServerKeyNone,
|
||||
List<int>? pinUnlockToken,
|
||||
List<int>? pinProtectionKey,
|
||||
String? email,
|
||||
}) async {
|
||||
final get = Handshake_GetServerKeyForPasswordLessRecovery()
|
||||
..userId = Int64(userId);
|
||||
if (encryptedServerKeyNone != null) {
|
||||
get.encryptedServerKeyNone = encryptedServerKeyNone;
|
||||
}
|
||||
if (pinUnlockToken != null) {
|
||||
get.pinUnlockToken = pinUnlockToken;
|
||||
}
|
||||
if (pinProtectionKey != null) {
|
||||
get.pinProtectionKey = pinProtectionKey;
|
||||
}
|
||||
if (email != null) {
|
||||
get.email = email;
|
||||
}
|
||||
|
||||
final handshake = Handshake()..getServerKeyForPasswordlessRecovery = get;
|
||||
final req = createClientToServerFromHandshake(handshake);
|
||||
return sendRequestSync(req, authenticated: false);
|
||||
}
|
||||
|
||||
|
||||
Future<Result> submitRecoveryShare({
|
||||
required String notificationId,
|
||||
required List<int> encryptedMessage,
|
||||
}) async {
|
||||
final req = createClientToServerFromApplicationData(
|
||||
ApplicationData(
|
||||
passwordlessNotification: ApplicationData_PasswordlessNotification(
|
||||
notificationId: notificationId,
|
||||
encryptedMessage: encryptedMessage,
|
||||
),
|
||||
),
|
||||
);
|
||||
return sendRequestSync(req);
|
||||
}
|
||||
|
||||
Future<Result> registerPasswordlessNotification({
|
||||
required String notificationId,
|
||||
required List<int> downloadAuthToken,
|
||||
required String langCode,
|
||||
required String? googleFcm,
|
||||
}) async {
|
||||
final registerNotif = Handshake_RegisterPasswordlessNotification()
|
||||
..notificationId = notificationId
|
||||
..downloadAuthToken = downloadAuthToken
|
||||
..langCode = langCode;
|
||||
if (googleFcm != null) {
|
||||
registerNotif.googleFcm = googleFcm;
|
||||
}
|
||||
|
||||
final handshake = Handshake()
|
||||
..registerPasswordlessNotification = registerNotif;
|
||||
final req = createClientToServerFromHandshake(handshake);
|
||||
return sendRequestSync(req, authenticated: false);
|
||||
}
|
||||
|
||||
Future<Result> addAdditionalUser(Int64 userId) async {
|
||||
final get = ApplicationData_AddAdditionalUser()..userId = userId;
|
||||
final appData = ApplicationData()..addAdditionalUser = get;
|
||||
|
|
@ -870,4 +951,30 @@ class ApiService {
|
|||
final req = createClientToServerFromApplicationData(appData);
|
||||
return sendRequestSync(req, contactId: target);
|
||||
}
|
||||
|
||||
/// Polls the server for new passwordless recovery notification messages.
|
||||
/// [alreadyReceivedIds] prevents the server from sending duplicates.
|
||||
Future<server.Response_PasswordlessNotificationMessages?> checkForPasswordlessNotification({
|
||||
required String notificationId,
|
||||
required List<int> downloadAuthToken,
|
||||
List<Int64>? alreadyReceivedIds,
|
||||
}) async {
|
||||
final check = Handshake_CheckForPasswordlessNotification()
|
||||
..notificationId = notificationId
|
||||
..downloadAuthToken = downloadAuthToken;
|
||||
if (alreadyReceivedIds != null && alreadyReceivedIds.isNotEmpty) {
|
||||
check.alreadyReceivedMessageIds.addAll(alreadyReceivedIds);
|
||||
}
|
||||
|
||||
final handshake = Handshake()..checkForPasswordlessNotification = check;
|
||||
final req = createClientToServerFromHandshake(handshake);
|
||||
final res = await sendRequestSync(req, authenticated: false);
|
||||
if (res.isSuccess) {
|
||||
final ok = res.value as server.Response_Ok;
|
||||
if (ok.hasPasswordlessNotificationMessages()) {
|
||||
return ok.passwordlessNotificationMessages;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ Future<void> handleErrorMessage(
|
|||
String receiptId, {
|
||||
String? groupId,
|
||||
}) async {
|
||||
Log.error('[$receiptId] Got error from $fromUserId: $error');
|
||||
Log.warn('[$receiptId] Got error from $fromUserId: $error');
|
||||
|
||||
switch (error.type) {
|
||||
case EncryptedContent_ErrorMessages_Type
|
||||
|
|
@ -38,14 +38,14 @@ Future<void> handleErrorMessage(
|
|||
break; // The other user initiated a new signal session, so ignore the error in this case, as the new session works...
|
||||
case EncryptedContent_ErrorMessages_Type.GROUP_NOT_FOUND_OR_NOT_A_MEMBER:
|
||||
if (groupId == null) {
|
||||
Log.error(
|
||||
Log.warn(
|
||||
'[$receiptId] GROUP_NOT_FOUND_OR_NOT_A_MEMBER error received, but groupId is null.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
final group = await twonlyDB.groupsDao.getGroup(groupId);
|
||||
if (group == null) {
|
||||
Log.error(
|
||||
Log.warn(
|
||||
'[$receiptId] GROUP_NOT_FOUND_OR_NOT_A_MEMBER error received, but group $groupId is not found in database.',
|
||||
);
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import 'package:twonly/src/services/flame.service.dart';
|
|||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
Future<void> handleMedia(
|
||||
Future<bool> handleMedia(
|
||||
int fromUserId,
|
||||
String groupId,
|
||||
EncryptedContent_Media media,
|
||||
|
|
@ -36,7 +36,7 @@ Future<void> handleMedia(
|
|||
Log.warn(
|
||||
'[$receiptId] Got reupload for a message that either does not exists (${message == null}) or senderId = ${message?.senderId}',
|
||||
);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// in case there was already a downloaded file delete it...
|
||||
|
|
@ -64,7 +64,7 @@ Future<void> handleMedia(
|
|||
unawaited(startDownloadMedia(mediaFile, false));
|
||||
}
|
||||
|
||||
return;
|
||||
return true;
|
||||
case EncryptedContent_Media_Type.IMAGE:
|
||||
mediaType = MediaType.image;
|
||||
case EncryptedContent_Media_Type.VIDEO:
|
||||
|
|
@ -85,13 +85,13 @@ Future<void> handleMedia(
|
|||
Log.warn(
|
||||
'[$receiptId] $fromUserId tried to modify the message from ${messageTmp.senderId}.',
|
||||
);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
if (messageTmp.mediaId == null) {
|
||||
Log.warn(
|
||||
'[$receiptId] This message already exit without a mediaId. Message is dropped.',
|
||||
);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(
|
||||
messageTmp.mediaId!,
|
||||
|
|
@ -100,7 +100,7 @@ Future<void> handleMedia(
|
|||
Log.warn(
|
||||
'[$receiptId] This message and media file already exit and was not requested again. Dropping it.',
|
||||
);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mediaFile != null) {
|
||||
|
|
@ -145,9 +145,13 @@ Future<void> handleMedia(
|
|||
|
||||
if (mediaFile == null) {
|
||||
Log.error('[$receiptId] Could not insert media file into database');
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
Log.info(
|
||||
'[$receiptId] Inserting media message: messageId=${media.senderMessageId}, mediaId=${mediaFile!.mediaId}',
|
||||
);
|
||||
|
||||
message = await twonlyDB.messagesDao.insertMessage(
|
||||
MessagesCompanion(
|
||||
messageId: Value(media.senderMessageId),
|
||||
|
|
@ -186,6 +190,7 @@ Future<void> handleMedia(
|
|||
);
|
||||
|
||||
unawaited(startDownloadMedia(mediaFile!, false));
|
||||
return true;
|
||||
} else {
|
||||
if (mediaFile == null && message == null) {
|
||||
Log.error(
|
||||
|
|
@ -200,6 +205,7 @@ Future<void> handleMedia(
|
|||
'[$receiptId] Could not insert new message as the message is empty.',
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
|
|||
import 'package:twonly/src/services/api/utils.api.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
Future<void> handleTextMessage(
|
||||
Future<bool> handleTextMessage(
|
||||
int fromUserId,
|
||||
String groupId,
|
||||
EncryptedContent_TextMessage textMessage,
|
||||
|
|
@ -26,7 +26,7 @@ Future<void> handleTextMessage(
|
|||
Log.warn(
|
||||
'[$receiptId] $fromUserId tried to overwrite message from ${existing.senderId}. Dropping.',
|
||||
);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
final message = await twonlyDB.messagesDao.insertMessage(
|
||||
|
|
@ -50,4 +50,5 @@ Future<void> handleTextMessage(
|
|||
if (message != null) {
|
||||
Log.info('[$receiptId] Inserted a new text message with ID: ${message.messageId}');
|
||||
}
|
||||
return message != null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -143,8 +143,12 @@ Future<void> handleDownloadStatusUpdate(TaskStatusUpdate update) async {
|
|||
failed = false;
|
||||
} else {
|
||||
failed = true;
|
||||
Log.warn(
|
||||
'[$mediaId] Got invalid response status code: ${update.responseStatusCode}',
|
||||
);
|
||||
Log.error(
|
||||
'Got invalid response status code: ${update.responseStatusCode}',
|
||||
onlyIfSentryEnabled: true,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
|
@ -227,7 +231,7 @@ Future<void> startDownloadMedia(MediaFile media, bool force) async {
|
|||
try {
|
||||
await downloadFileFast(media, apiUrl, mediaService.encryptedPath);
|
||||
} catch (e) {
|
||||
Log.error('Fast download failed: $e');
|
||||
Log.warn('Fast download failed: $e');
|
||||
await FileDownloader().enqueue(task);
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
@ -251,9 +255,13 @@ Future<void> downloadFileFast(
|
|||
return;
|
||||
} else {
|
||||
if (response.statusCode == 404 || response.statusCode == 403) {
|
||||
Log.error(
|
||||
Log.warn(
|
||||
'Got ${response.statusCode} from server for media ID ${media.mediaId}. Requesting upload again',
|
||||
);
|
||||
Log.error(
|
||||
'Got ${response.statusCode} from server for media ID.',
|
||||
onlyIfSentryEnabled: true,
|
||||
);
|
||||
// Message was deleted from the server. Requesting it again from the sender to upload it again...
|
||||
await requestMediaReupload(media.mediaId);
|
||||
return;
|
||||
|
|
@ -264,6 +272,13 @@ Future<void> downloadFileFast(
|
|||
}
|
||||
|
||||
Future<void> requestMediaReupload(String mediaId) async {
|
||||
await twonlyDB.mediaFilesDao.updateMedia(
|
||||
mediaId,
|
||||
const MediaFilesCompanion(
|
||||
downloadState: Value(DownloadState.reuploadRequested),
|
||||
),
|
||||
);
|
||||
|
||||
final messages = await twonlyDB.messagesDao.getMessagesByMediaId(mediaId);
|
||||
|
||||
for (final message in messages) {
|
||||
|
|
@ -277,12 +292,6 @@ Future<void> requestMediaReupload(String mediaId) async {
|
|||
),
|
||||
),
|
||||
);
|
||||
await twonlyDB.mediaFilesDao.updateMedia(
|
||||
mediaId,
|
||||
const MediaFilesCompanion(
|
||||
downloadState: Value(DownloadState.reuploadRequested),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -293,7 +302,7 @@ Future<void> handleEncryptedFile(String mediaId) async {
|
|||
action: () async {
|
||||
final mediaService = await MediaFileService.fromMediaId(mediaId);
|
||||
if (mediaService == null) {
|
||||
Log.error('Media file not found in database.');
|
||||
Log.warn('[$mediaId] Media file not found in database.');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -80,9 +80,13 @@ Future<void> handleUploadStatusUpdate(TaskStatusUpdate update) async {
|
|||
await markUploadAsSuccessful(media);
|
||||
return;
|
||||
}
|
||||
Log.error(
|
||||
Log.warn(
|
||||
'Got HTTP error ${update.responseStatusCode} for $mediaId',
|
||||
);
|
||||
Log.error(
|
||||
'Got HTTP error ${update.responseStatusCode} for media.',
|
||||
onlyIfSentryEnabled: true,
|
||||
);
|
||||
}
|
||||
|
||||
if (update.status == TaskStatus.notFound) {
|
||||
|
|
@ -118,9 +122,13 @@ Future<void> handleUploadStatusUpdate(TaskStatusUpdate update) async {
|
|||
|
||||
if (update.status == TaskStatus.failed ||
|
||||
update.status == TaskStatus.canceled) {
|
||||
Log.error(
|
||||
Log.warn(
|
||||
'Background upload failed for $mediaId with status ${update.status} and ${update.responseStatusCode}. ',
|
||||
);
|
||||
Log.error(
|
||||
'Background upload failed with status ${update.status} and ${update.responseStatusCode}.',
|
||||
onlyIfSentryEnabled: true,
|
||||
);
|
||||
final mediaService = MediaFileService(media);
|
||||
|
||||
// in case the media file is already uploaded to not reqtry
|
||||
|
|
|
|||
|
|
@ -439,6 +439,10 @@ Future<void> _startBackgroundMediaUploadInternal(
|
|||
// Refresh the media file state inside the mutex
|
||||
await mediaService.updateFromDB();
|
||||
|
||||
if (mediaService.mediaFile.uploadState == UploadState.uploading) {
|
||||
await _checkAndRecoverMissingUploadRequest(mediaService);
|
||||
}
|
||||
|
||||
if (mediaService.mediaFile.uploadState == UploadState.initialized ||
|
||||
mediaService.mediaFile.uploadState == UploadState.preprocessing) {
|
||||
Log.info(
|
||||
|
|
@ -672,7 +676,31 @@ Future<void> _createUploadRequest(MediaFileService media) async {
|
|||
await media.uploadRequestPath.writeAsBytes(uploadRequestBytes);
|
||||
}
|
||||
|
||||
Future<bool> _checkAndRecoverMissingUploadRequest(
|
||||
MediaFileService media, {
|
||||
bool triggerBackgroundUpload = false,
|
||||
}) async {
|
||||
if (!media.uploadRequestPath.existsSync()) {
|
||||
Log.warn(
|
||||
'UploadRequestPath for media ${media.mediaFile.mediaId} does not exist. Reverting to preprocessing.',
|
||||
);
|
||||
await media.setUploadState(UploadState.preprocessing);
|
||||
if (triggerBackgroundUpload) {
|
||||
unawaited(startBackgroundMediaUpload(media));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<void> _uploadUploadRequest(MediaFileService media) async {
|
||||
if (await _checkAndRecoverMissingUploadRequest(
|
||||
media,
|
||||
triggerBackgroundUpload: true,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final currentMedia = await twonlyDB.mediaFilesDao.getMediaFileById(
|
||||
media.mediaFile.mediaId,
|
||||
);
|
||||
|
|
@ -722,6 +750,13 @@ Future<void> uploadFileFastOrEnqueue(
|
|||
UploadTask task,
|
||||
MediaFileService media,
|
||||
) async {
|
||||
if (await _checkAndRecoverMissingUploadRequest(
|
||||
media,
|
||||
triggerBackgroundUpload: true,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final requestMultipart = http.MultipartRequest(
|
||||
'POST',
|
||||
Uri.parse(task.url),
|
||||
|
|
|
|||
|
|
@ -101,14 +101,18 @@ Future<(Uint8List, Uint8List?)?> _tryToSendCompleteMessageInternal({
|
|||
if (receiptId == null && receipt == null) return null;
|
||||
|
||||
try {
|
||||
if (receipt == null) {
|
||||
// ignore: parameter_assignments
|
||||
receipt = await twonlyDB.receiptsDao.getReceiptById(receiptId!);
|
||||
if (receipt == null) {
|
||||
Log.warn('[$receiptId] Receipt not found.');
|
||||
return null;
|
||||
}
|
||||
final targetReceiptId = receipt?.receiptId ?? receiptId!;
|
||||
final loadedReceipt = await twonlyDB.receiptsDao.getReceiptById(
|
||||
targetReceiptId,
|
||||
);
|
||||
if (loadedReceipt == null) {
|
||||
Log.info(
|
||||
'[$targetReceiptId] Receipt not found (might have been processed or deleted).',
|
||||
);
|
||||
return null;
|
||||
}
|
||||
// ignore: parameter_assignments
|
||||
receipt = loadedReceipt;
|
||||
|
||||
if (receipt.retryCount >= 2) {
|
||||
// After two retries, change the receiptId. This addresses a bug where the receiver received the message and marked it as received,
|
||||
|
|
@ -138,7 +142,7 @@ Future<(Uint8List, Uint8List?)?> _tryToSendCompleteMessageInternal({
|
|||
if (!onlyReturnEncryptedData &&
|
||||
receipt.ackByServerAt != null &&
|
||||
receipt.markForRetry == null) {
|
||||
Log.error('Message already uploaded and mark for retry is not set!');
|
||||
Log.info('Message already uploaded and mark for retry is not set.');
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ 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/notifications/fcm.notifications.dart';
|
||||
import 'package:twonly/src/services/passwordless_recovery.service.dart';
|
||||
import 'package:twonly/src/services/signal/encryption.signal.dart';
|
||||
import 'package:twonly/src/services/signal/session.signal.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
|
@ -55,9 +56,13 @@ Future<void> handleServerMessage(server.ServerToClient msg) async {
|
|||
Log.info(
|
||||
'Got ${msg.v0.newMessages.newMessages.length} messages from the server.',
|
||||
);
|
||||
final brokenSessionsInCurrentBatch = <int>{};
|
||||
for (final newMessage in msg.v0.newMessages.newMessages) {
|
||||
try {
|
||||
await handleClient2ClientMessage(newMessage);
|
||||
await handleClient2ClientMessage(
|
||||
newMessage,
|
||||
brokenSessionsInCurrentBatch: brokenSessionsInCurrentBatch,
|
||||
);
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
}
|
||||
|
|
@ -82,7 +87,10 @@ DateTime lastPushKeyRequest = clock.now().subtract(const Duration(hours: 1));
|
|||
|
||||
final Map<String, Mutex> _messageLocks = {};
|
||||
|
||||
Future<void> handleClient2ClientMessage(NewMessage newMessage) async {
|
||||
Future<void> handleClient2ClientMessage(
|
||||
NewMessage newMessage, {
|
||||
Set<int>? brokenSessionsInCurrentBatch,
|
||||
}) async {
|
||||
final body = Uint8List.fromList(newMessage.body);
|
||||
final message = Message.fromBuffer(body);
|
||||
final receiptId = message.receiptId;
|
||||
|
|
@ -96,7 +104,11 @@ Future<void> handleClient2ClientMessage(NewMessage newMessage) async {
|
|||
}
|
||||
await mutex.protect(() async {
|
||||
try {
|
||||
await _handleClient2ClientMessage(newMessage, message);
|
||||
await _handleClient2ClientMessage(
|
||||
newMessage,
|
||||
message,
|
||||
brokenSessionsInCurrentBatch: brokenSessionsInCurrentBatch,
|
||||
);
|
||||
} finally {
|
||||
_messageLocks.remove(receiptId);
|
||||
}
|
||||
|
|
@ -105,11 +117,27 @@ Future<void> handleClient2ClientMessage(NewMessage newMessage) async {
|
|||
|
||||
Future<void> _handleClient2ClientMessage(
|
||||
NewMessage newMessage,
|
||||
Message message,
|
||||
) async {
|
||||
Message message, {
|
||||
Set<int>? brokenSessionsInCurrentBatch,
|
||||
}) async {
|
||||
final fromUserId = newMessage.fromUserId.toInt();
|
||||
final receiptId = message.receiptId;
|
||||
|
||||
if (brokenSessionsInCurrentBatch?.contains(fromUserId) == true) {
|
||||
// This happens when a session goes out of sync (e.g. wrong message order).
|
||||
// We skip the remaining messages in the batch because each failed decryption
|
||||
// attempt is extremely slow (~1.2s) and would otherwise freeze the app.
|
||||
// By returning early, we skip gotReceipt() and error responses.
|
||||
// The server still deletes the batch since we ACK the entire batch later.
|
||||
// The sender keeps the message unacknowledged. Once they process our SESSION_OUT_OF_SYNC
|
||||
// error and establish a new session, their retry logic will automatically re-encrypt
|
||||
// and re-send these messages with the new keys.
|
||||
Log.info(
|
||||
'Skipping message from $fromUserId - session known broken in this batch',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (await twonlyDB.receiptsDao.isDuplicated(receiptId)) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -199,6 +227,7 @@ Future<void> _handleClient2ClientMessage(
|
|||
encryptedContentRaw,
|
||||
message.type,
|
||||
receiptId,
|
||||
brokenSessionsInCurrentBatch: brokenSessionsInCurrentBatch,
|
||||
);
|
||||
if (plainTextContent != null) {
|
||||
response = Message(
|
||||
|
|
@ -252,7 +281,11 @@ Future<void> _handleClient2ClientMessage(
|
|||
await twonlyDB.receiptsDao.gotReceipt(receiptId);
|
||||
Log.info('[$receiptId] Finished processing');
|
||||
} catch (e) {
|
||||
Log.error('[$receiptId] Error marking message as received: $e');
|
||||
Log.warn('[$receiptId] Error marking message as received: $e');
|
||||
Log.error(
|
||||
'Error marking message as received: $e',
|
||||
onlyIfSentryEnabled: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -260,13 +293,15 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessageRaw(
|
|||
int fromUserId,
|
||||
Uint8List encryptedContentRaw,
|
||||
Message_Type messageType,
|
||||
String receiptId,
|
||||
) async {
|
||||
String receiptId, {
|
||||
Set<int>? brokenSessionsInCurrentBatch,
|
||||
}) async {
|
||||
Log.info('[$receiptId] calling signalDecryptMessage');
|
||||
var (encryptedContent, decryptionErrorType) = await signalDecryptMessage(
|
||||
fromUserId,
|
||||
encryptedContentRaw,
|
||||
messageType.value,
|
||||
brokenSessionsInCurrentBatch: brokenSessionsInCurrentBatch,
|
||||
);
|
||||
|
||||
if (encryptedContent == null) {
|
||||
|
|
@ -283,7 +318,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessageRaw(
|
|||
|
||||
Log.info('[$receiptId] Calling handleEncryptedMessage');
|
||||
|
||||
final (a, b) = await handleEncryptedMessage(
|
||||
final result = await handleEncryptedMessage(
|
||||
fromUserId,
|
||||
encryptedContent,
|
||||
messageType,
|
||||
|
|
@ -292,9 +327,9 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessageRaw(
|
|||
|
||||
Log.info('[$receiptId] Finished handleEncryptedMessage');
|
||||
|
||||
if (a == null && b == null) {
|
||||
if (result.responseCipherText == null && result.responsePlaintext == null) {
|
||||
unawaited(FcmNotificationService.updateLastServerMessageTimestamp());
|
||||
if (Platform.isAndroid) {
|
||||
if (Platform.isAndroid && result.showPushNotification) {
|
||||
// Message was handled without any error. Show push notification to the user for Android.
|
||||
await showPushNotificationFromServerMessages(
|
||||
fromUserId,
|
||||
|
|
@ -303,10 +338,10 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessageRaw(
|
|||
}
|
||||
}
|
||||
|
||||
return (a, b);
|
||||
return (result.responseCipherText, result.responsePlaintext);
|
||||
}
|
||||
|
||||
Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
||||
Future<DecryptedMessageResult> handleEncryptedMessage(
|
||||
int fromUserId,
|
||||
EncryptedContent content,
|
||||
Message_Type messageType,
|
||||
|
|
@ -347,13 +382,12 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
|||
content.contactRequest,
|
||||
receiptId,
|
||||
)) {
|
||||
return (
|
||||
null,
|
||||
PlaintextContent()
|
||||
return DecryptedMessageResult(
|
||||
responsePlaintext: PlaintextContent()
|
||||
..retryControlError = PlaintextContent_RetryErrorMessage(),
|
||||
);
|
||||
}
|
||||
return (null, null);
|
||||
return const DecryptedMessageResult();
|
||||
}
|
||||
|
||||
if (content.hasErrorMessages()) {
|
||||
|
|
@ -363,7 +397,25 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
|||
receiptId,
|
||||
groupId: content.hasGroupId() ? content.groupId : null,
|
||||
);
|
||||
return (null, null);
|
||||
return const DecryptedMessageResult(showPushNotification: false);
|
||||
}
|
||||
|
||||
if (content.hasPasswordlessRecovery()) {
|
||||
await PasswordlessRecoveryService.handlePasswordlessRecovery(
|
||||
fromUserId,
|
||||
content.passwordlessRecovery,
|
||||
receiptId,
|
||||
);
|
||||
return const DecryptedMessageResult();
|
||||
}
|
||||
|
||||
if (content.hasPasswordlessRecoveryHeartbeat()) {
|
||||
await PasswordlessRecoveryService.handlePasswordlessRecoveryHeartbeat(
|
||||
fromUserId,
|
||||
content.passwordlessRecoveryHeartbeat,
|
||||
receiptId,
|
||||
);
|
||||
return const DecryptedMessageResult();
|
||||
}
|
||||
|
||||
if (content.hasContactUpdate()) {
|
||||
|
|
@ -373,7 +425,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
|||
senderProfileCounter,
|
||||
receiptId,
|
||||
);
|
||||
return (null, null);
|
||||
return const DecryptedMessageResult(showPushNotification: false);
|
||||
}
|
||||
|
||||
if (content.hasUserDiscoveryRequest()) {
|
||||
|
|
@ -382,7 +434,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
|||
content.userDiscoveryRequest,
|
||||
receiptId,
|
||||
);
|
||||
return (null, null);
|
||||
return const DecryptedMessageResult(showPushNotification: false);
|
||||
}
|
||||
|
||||
if (content.hasUserDiscoveryUpdate()) {
|
||||
|
|
@ -391,12 +443,12 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
|||
content.userDiscoveryUpdate,
|
||||
receiptId,
|
||||
);
|
||||
return (null, null);
|
||||
return const DecryptedMessageResult();
|
||||
}
|
||||
|
||||
if (content.hasPushKeys()) {
|
||||
await handlePushKey(fromUserId, content.pushKeys, receiptId);
|
||||
return (null, null);
|
||||
return const DecryptedMessageResult();
|
||||
}
|
||||
|
||||
if (content.hasMessageUpdate()) {
|
||||
|
|
@ -405,7 +457,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
|||
content.messageUpdate,
|
||||
receiptId,
|
||||
);
|
||||
return (null, null);
|
||||
return const DecryptedMessageResult();
|
||||
}
|
||||
|
||||
if (content.hasKeyVerificationProof()) {
|
||||
|
|
@ -413,7 +465,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
|||
fromUserId,
|
||||
content.keyVerificationProof.calculatedMac,
|
||||
);
|
||||
return (null, null);
|
||||
return const DecryptedMessageResult();
|
||||
}
|
||||
|
||||
if (content.hasMediaUpdate()) {
|
||||
|
|
@ -422,12 +474,19 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
|||
content.mediaUpdate,
|
||||
receiptId,
|
||||
);
|
||||
return (null, null);
|
||||
return const DecryptedMessageResult();
|
||||
}
|
||||
|
||||
if (!content.hasGroupId()) {
|
||||
Log.error('[$receiptId] Messages should have a groupId $fromUserId.');
|
||||
return (null, null);
|
||||
final type = _getEncryptedContentType(content);
|
||||
Log.warn(
|
||||
'[$receiptId] Messages should have a groupId $fromUserId. Type: $type',
|
||||
);
|
||||
Log.error(
|
||||
'Messages should have a groupId. Type: $type',
|
||||
onlyIfSentryEnabled: true,
|
||||
);
|
||||
return const DecryptedMessageResult();
|
||||
}
|
||||
|
||||
if (content.hasGroupCreate()) {
|
||||
|
|
@ -437,7 +496,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
|||
content.groupCreate,
|
||||
receiptId,
|
||||
);
|
||||
return (null, null);
|
||||
return const DecryptedMessageResult();
|
||||
}
|
||||
|
||||
/// Verify that the user is (still) in that group...
|
||||
|
|
@ -453,18 +512,17 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
|||
);
|
||||
if (contact == null || !contact.accepted || contact.deletedByUser) {
|
||||
await handleNewContactRequest(fromUserId);
|
||||
Log.error(
|
||||
Log.warn(
|
||||
'[$receiptId] User tries to send message to direct chat while the user does not exist!',
|
||||
);
|
||||
return (
|
||||
EncryptedContent(
|
||||
return DecryptedMessageResult(
|
||||
responseCipherText: EncryptedContent(
|
||||
errorMessages: EncryptedContent_ErrorMessages(
|
||||
type: EncryptedContent_ErrorMessages_Type
|
||||
.ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD,
|
||||
relatedReceiptId: receiptId,
|
||||
),
|
||||
),
|
||||
null,
|
||||
);
|
||||
}
|
||||
Log.info(
|
||||
|
|
@ -478,22 +536,21 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
|||
);
|
||||
} else {
|
||||
if (content.hasGroupJoin()) {
|
||||
Log.error(
|
||||
Log.warn(
|
||||
'[$receiptId] Got group join message, but group does not exist yet, retry later. As probably the GroupCreate was not yet received.',
|
||||
);
|
||||
// In case the group join was received before the GroupCreate the sender should send it later again.
|
||||
return (
|
||||
null,
|
||||
PlaintextContent()
|
||||
return DecryptedMessageResult(
|
||||
responsePlaintext: PlaintextContent()
|
||||
..retryControlError = PlaintextContent_RetryErrorMessage(),
|
||||
);
|
||||
}
|
||||
|
||||
Log.error(
|
||||
Log.warn(
|
||||
'[$receiptId] User $fromUserId tried to access group ${content.groupId}. Sending GROUP_NOT_FOUND_OR_NOT_A_MEMBER error.',
|
||||
);
|
||||
return (
|
||||
EncryptedContent(
|
||||
return DecryptedMessageResult(
|
||||
responseCipherText: EncryptedContent(
|
||||
groupId: content.groupId,
|
||||
errorMessages: EncryptedContent_ErrorMessages(
|
||||
type: EncryptedContent_ErrorMessages_Type
|
||||
|
|
@ -501,14 +558,13 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
|||
relatedReceiptId: receiptId,
|
||||
),
|
||||
),
|
||||
null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (content.hasFlameSync()) {
|
||||
await handleFlameSync(content.groupId, content.flameSync, receiptId);
|
||||
return (null, null);
|
||||
return const DecryptedMessageResult();
|
||||
}
|
||||
|
||||
if (content.hasGroupUpdate()) {
|
||||
|
|
@ -518,7 +574,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
|||
content.groupUpdate,
|
||||
receiptId,
|
||||
);
|
||||
return (null, null);
|
||||
return const DecryptedMessageResult();
|
||||
}
|
||||
|
||||
if (content.hasGroupJoin()) {
|
||||
|
|
@ -528,13 +584,12 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
|||
content.groupJoin,
|
||||
receiptId,
|
||||
)) {
|
||||
return (
|
||||
null,
|
||||
PlaintextContent()
|
||||
return DecryptedMessageResult(
|
||||
responsePlaintext: PlaintextContent()
|
||||
..retryControlError = PlaintextContent_RetryErrorMessage(),
|
||||
);
|
||||
}
|
||||
return (null, null);
|
||||
return const DecryptedMessageResult();
|
||||
}
|
||||
|
||||
if (content.hasResendGroupPublicKey()) {
|
||||
|
|
@ -544,7 +599,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
|||
content.groupJoin,
|
||||
receiptId,
|
||||
);
|
||||
return (null, null);
|
||||
return const DecryptedMessageResult();
|
||||
}
|
||||
|
||||
if (content.hasAdditionalDataMessage()) {
|
||||
|
|
@ -554,17 +609,17 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
|||
content.additionalDataMessage,
|
||||
receiptId,
|
||||
);
|
||||
return (null, null);
|
||||
return const DecryptedMessageResult();
|
||||
}
|
||||
|
||||
if (content.hasTextMessage()) {
|
||||
await handleTextMessage(
|
||||
final isNewText = await handleTextMessage(
|
||||
fromUserId,
|
||||
content.groupId,
|
||||
content.textMessage,
|
||||
receiptId,
|
||||
);
|
||||
return (null, null);
|
||||
return DecryptedMessageResult(showPushNotification: isNewText);
|
||||
}
|
||||
|
||||
if (content.hasReaction()) {
|
||||
|
|
@ -574,17 +629,17 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
|||
content.reaction,
|
||||
receiptId,
|
||||
);
|
||||
return (null, null);
|
||||
return const DecryptedMessageResult();
|
||||
}
|
||||
|
||||
if (content.hasMedia()) {
|
||||
await handleMedia(
|
||||
final isNewMedia = await handleMedia(
|
||||
fromUserId,
|
||||
content.groupId,
|
||||
content.media,
|
||||
receiptId,
|
||||
);
|
||||
return (null, null);
|
||||
return DecryptedMessageResult(showPushNotification: isNewMedia);
|
||||
}
|
||||
|
||||
if (content.hasTypingIndicator()) {
|
||||
|
|
@ -596,5 +651,39 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
|||
);
|
||||
}
|
||||
|
||||
return (null, null);
|
||||
return const DecryptedMessageResult();
|
||||
}
|
||||
|
||||
String _getEncryptedContentType(EncryptedContent content) {
|
||||
if (content.hasMessageUpdate()) return 'messageUpdate';
|
||||
if (content.hasMedia()) return 'media';
|
||||
if (content.hasMediaUpdate()) return 'mediaUpdate';
|
||||
if (content.hasContactUpdate()) return 'contactUpdate';
|
||||
if (content.hasContactRequest()) return 'contactRequest';
|
||||
if (content.hasFlameSync()) return 'flameSync';
|
||||
if (content.hasPushKeys()) return 'pushKeys';
|
||||
if (content.hasReaction()) return 'reaction';
|
||||
if (content.hasTextMessage()) return 'textMessage';
|
||||
if (content.hasGroupCreate()) return 'groupCreate';
|
||||
if (content.hasGroupJoin()) return 'groupJoin';
|
||||
if (content.hasGroupUpdate()) return 'groupUpdate';
|
||||
if (content.hasResendGroupPublicKey()) return 'resendGroupPublicKey';
|
||||
if (content.hasErrorMessages()) return 'errorMessages';
|
||||
if (content.hasAdditionalDataMessage()) return 'additionalDataMessage';
|
||||
if (content.hasTypingIndicator()) return 'typingIndicator';
|
||||
if (content.hasUserDiscoveryRequest()) return 'userDiscoveryRequest';
|
||||
if (content.hasUserDiscoveryUpdate()) return 'userDiscoveryUpdate';
|
||||
if (content.hasKeyVerificationProof()) return 'keyVerificationProof';
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
class DecryptedMessageResult {
|
||||
const DecryptedMessageResult({
|
||||
this.responseCipherText,
|
||||
this.responsePlaintext,
|
||||
this.showPushNotification = true,
|
||||
});
|
||||
final EncryptedContent? responseCipherText;
|
||||
final PlaintextContent? responsePlaintext;
|
||||
final bool showPushNotification;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -105,8 +105,9 @@ class BackupService {
|
|||
))) {
|
||||
final backupId = await RustBackupIdentity.getBackupId();
|
||||
if (backupId == null) {
|
||||
Log.error('No backup password was set by the user.');
|
||||
Log.warn('No backup password was set by the user.');
|
||||
backup.identityState = LastBackupUploadState.failed;
|
||||
await UserService.update((u) => u.isBackupEnabled = false);
|
||||
} else {
|
||||
Log.info('Performing a identity backup.');
|
||||
final encryptedBackup =
|
||||
|
|
@ -162,7 +163,7 @@ class BackupService {
|
|||
(backupDownloadToken, backupArchive) =
|
||||
await RustBackupArchive.createBackupArchive();
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
Log.warn('Creating archive backup failed: $e');
|
||||
return;
|
||||
}
|
||||
Log.info(
|
||||
|
|
@ -324,6 +325,26 @@ class BackupService {
|
|||
return _nextBackupStage();
|
||||
}
|
||||
|
||||
static Future<RecoveryError?> startPasswordlessBackupRecovery(
|
||||
int userId,
|
||||
String username,
|
||||
Uint8List keyManagerBytes,
|
||||
) async {
|
||||
final state = BackupRecovery(
|
||||
username: username,
|
||||
password: '',
|
||||
userId: userId,
|
||||
)..state = BackupRecoveryState.archiveBackupStarted;
|
||||
|
||||
await deleteLocalUserData();
|
||||
|
||||
// Import KeyManager keys into secure storage & in-memory key manager
|
||||
await RustKeyManager.importSerialized(serializedBytes: keyManagerBytes);
|
||||
|
||||
await KeyValueStore.put(KeyValueKeys.backupRecoveryState, state.toJson());
|
||||
return _nextBackupStage();
|
||||
}
|
||||
|
||||
static Future<(Uint8List?, RecoveryError?)> _downloadBackup(
|
||||
String backupServerUrl,
|
||||
) async {
|
||||
|
|
@ -337,7 +358,7 @@ class BackupService {
|
|||
},
|
||||
);
|
||||
} catch (e) {
|
||||
Log.error('Error fetching backup: $e');
|
||||
Log.warn('Error fetching backup: $e');
|
||||
return (null, RecoveryError.noInternet);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_sharing_intent/flutter_sharing_intent.dart';
|
||||
import 'package:flutter_sharing_intent/model/sharing_file.dart';
|
||||
|
|
@ -13,6 +13,8 @@ import 'package:twonly/src/constants/routes.keys.dart';
|
|||
import 'package:twonly/src/database/tables/contacts.table.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
|
||||
import 'package:twonly/src/services/passwordless_recovery.service.dart'
|
||||
show PasswordlessRecoveryService;
|
||||
import 'package:twonly/src/services/signal/session.signal.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
|
@ -30,6 +32,12 @@ Future<bool> handleIntentUrl(BuildContext context, Uri uri) async {
|
|||
// Check if this is the QR code link which was
|
||||
// therefore scanned with the system camera
|
||||
|
||||
if (kDebugMode &&
|
||||
uri.toString().startsWith(PasswordlessRecoveryService.linkPrefix)) {
|
||||
await PasswordlessRecoveryService.handleRecoveryLink(uri.toString());
|
||||
return true;
|
||||
}
|
||||
|
||||
if (uri.toString().startsWith(QrCodeUtils.linkPrefix)) {
|
||||
final result = await QrCodeUtils.handleQrCodeLink(uri.toString());
|
||||
|
||||
|
|
|
|||
|
|
@ -33,17 +33,18 @@ Future<void> compressImage(
|
|||
|
||||
Log.info('Compressed images size in bytes: ${compressedBytes.length}');
|
||||
|
||||
if (compressedBytes.length >= 1 * 1000 * 1000) {
|
||||
if (compressedBytes.length >= 2 * 1000 * 1000) {
|
||||
// if the media file is over 1MB compress it with 60%
|
||||
final tmpCompressedBytes = await FlutterImageCompress.compressWithFile(
|
||||
sourceFile.path,
|
||||
format: CompressFormat.webp,
|
||||
quality: 60,
|
||||
);
|
||||
if (tmpCompressedBytes != null) {
|
||||
if (tmpCompressedBytes == null) {
|
||||
Log.error(
|
||||
'Could not compress media file with 60%: $sourceFile. Sending original 90% compressed file.',
|
||||
);
|
||||
} else {
|
||||
compressedBytes = tmpCompressedBytes;
|
||||
}
|
||||
}
|
||||
|
|
@ -106,11 +107,11 @@ Future<void> compressAndOverlayVideo(MediaFileService media) async {
|
|||
},
|
||||
);
|
||||
} catch (e) {
|
||||
Log.error('during video compression: $e');
|
||||
Log.warn('during video compression: $e');
|
||||
}
|
||||
|
||||
if (compressedPath == null) {
|
||||
Log.error('Could not compress video using original video.');
|
||||
Log.warn('Could not compress video using original video.');
|
||||
// as a fall back use the non compressed version
|
||||
media.ffmpegOutputPath.copySync(media.tempPath.path);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -252,7 +252,7 @@ class MediaFileService {
|
|||
|
||||
Future<void> compressMedia() async {
|
||||
if (!originalPath.existsSync()) {
|
||||
Log.error('Could not compress as original media does not exists.');
|
||||
Log.warn('Could not compress as original media does not exists.');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -296,7 +296,8 @@ class MediaFileService {
|
|||
(thumbnailPath.existsSync() && thumbnailPath.lengthSync() > 0) ||
|
||||
mediaFile.type == MediaType.audio ||
|
||||
((mediaFile.type == MediaType.image || mediaFile.type == MediaType.gif) &&
|
||||
storedPath.existsSync() && storedPath.lengthSync() > 0);
|
||||
storedPath.existsSync() &&
|
||||
storedPath.lengthSync() > 0);
|
||||
|
||||
Future<void> storeMediaFile() async {
|
||||
Log.info('Storing media file ${mediaFile.mediaId}');
|
||||
|
|
@ -327,8 +328,8 @@ class MediaFileService {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
Log.error(
|
||||
'Could not store image neither as ${tempPath.path} does not exists.',
|
||||
Log.warn(
|
||||
'Could not store image locally as ${tempPath.path} does not exist.',
|
||||
);
|
||||
}
|
||||
unawaited(createThumbnail());
|
||||
|
|
@ -469,7 +470,7 @@ class MediaFileService {
|
|||
format: CompressFormat.webp,
|
||||
quality: 90,
|
||||
);
|
||||
|
||||
|
||||
if (webpBytes.isNotEmpty) {
|
||||
await storedPath.writeAsBytes(webpBytes);
|
||||
} else {
|
||||
|
|
@ -503,7 +504,7 @@ class MediaFileService {
|
|||
);
|
||||
await updateFromDB();
|
||||
} catch (e) {
|
||||
Log.error(
|
||||
Log.warn(
|
||||
'Error auto-cropping transparent borders for mediaId ${mediaFile.mediaId}: $e',
|
||||
);
|
||||
await twonlyDB.mediaFilesDao.updateMedia(
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ Future<bool> createThumbnailsForImage(
|
|||
);
|
||||
return true;
|
||||
} else {
|
||||
Log.error('Compressed image thumbnail is empty or missing.');
|
||||
Log.warn('Compressed image thumbnail is empty or missing.');
|
||||
try {
|
||||
if (destinationFile.existsSync()) {
|
||||
destinationFile.deleteSync();
|
||||
|
|
|
|||
|
|
@ -1,115 +1,640 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert' show base64Encode, utf8;
|
||||
import 'dart:typed_data';
|
||||
import 'dart:convert' show base64Url, utf8;
|
||||
|
||||
import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'
|
||||
show FlutterChacha20;
|
||||
import 'package:cryptography_plus/cryptography_plus.dart' show SecretKey;
|
||||
import 'package:hashlib/hashlib.dart' show Scrypt;
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:crypto/crypto.dart' hide Hmac;
|
||||
import 'package:cryptography_plus/cryptography_plus.dart'
|
||||
show Hmac, Mac, SecretBox, SecretKey, Xchacha20;
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:twonly/core/bridge/wrapper.dart';
|
||||
import 'package:twonly/core/bridge/wrapper/key_manager.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/constants/keyvalue.keys.dart';
|
||||
import 'package:twonly/src/constants/routes.keys.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart'
|
||||
show getContactDisplayName;
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/json/onboarding_state.model.dart';
|
||||
import 'package:twonly/src/model/json/userdata.model.dart'
|
||||
show PasswordLessRecovery;
|
||||
import 'package:twonly/src/model/protobuf/client/generated/passwordless_recovery/types.pb.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
|
||||
as pb;
|
||||
import 'package:twonly/src/model/protobuf/client/generated/passwordless_recovery.pb.dart';
|
||||
import 'package:twonly/src/providers/routing.provider.dart';
|
||||
import 'package:twonly/src/services/api/messages.api.dart';
|
||||
import 'package:twonly/src/services/user.service.dart';
|
||||
import 'package:twonly/src/utils/avatars.dart' show getAvatarSvg;
|
||||
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/visual/views/settings/backup/passwordless_recovery/help_a_friend.passwordless_recovery.view.dart';
|
||||
|
||||
enum SecondFactorType { none, pin, email }
|
||||
enum SecondFactorType { email, pin, none }
|
||||
|
||||
class PasswordlessRecoveryService {
|
||||
static final StreamController<String> onEmailTokenReceived =
|
||||
StreamController<String>.broadcast();
|
||||
|
||||
static String linkPrefix = 'https://me.twonly.eu/r/#';
|
||||
|
||||
static final Set<String> _handledNotificationIds = {};
|
||||
|
||||
static Future<void> handleRecoveryLink(String link) async {
|
||||
final hashIndex = link.indexOf('#');
|
||||
if (hashIndex == -1) return;
|
||||
|
||||
final fragment = link.substring(hashIndex + 1);
|
||||
final parts = fragment.split('/');
|
||||
if (parts.length < 2) {
|
||||
if (fragment.isNotEmpty) {
|
||||
onEmailTokenReceived.add(fragment);
|
||||
final context = rootNavigatorKey.currentContext;
|
||||
if (context != null && context.mounted) {
|
||||
unawaited(context.push(Routes.recoverPasswordless, extra: fragment));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final notificationId = parts[0];
|
||||
final base64Key = parts[1];
|
||||
|
||||
if (_handledNotificationIds.contains(notificationId)) {
|
||||
Log.info(
|
||||
'Notification ID $notificationId was already handled, skipping.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
_handledNotificationIds.add(notificationId);
|
||||
|
||||
final encryptionKey = base64Url.decode(base64Url.normalize(base64Key));
|
||||
|
||||
final context = rootNavigatorKey.currentContext;
|
||||
if (context != null && context.mounted) {
|
||||
unawaited(
|
||||
context.navPush(
|
||||
HelpAFriendPasswordlessRecoveryView(
|
||||
notificationId: notificationId,
|
||||
encryptionKey: encryptionKey,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<bool> submitRecoveryShare(
|
||||
String notificationId,
|
||||
List<int> encryptionKey,
|
||||
Contact contact,
|
||||
) async {
|
||||
try {
|
||||
final share = contact.recoveryContactsSecretShare;
|
||||
if (share == null) {
|
||||
Log.warn('Contact does not have a recovery share stored.');
|
||||
return false;
|
||||
}
|
||||
|
||||
final trustedFriend = TrustedFriendShare_User(
|
||||
userId: Int64(userService.currentUser.userId),
|
||||
displayName: userService.currentUser.displayName,
|
||||
avatar: userService.currentUser.avatarSvg != null
|
||||
? utf8.encode(userService.currentUser.avatarSvg!)
|
||||
: null,
|
||||
);
|
||||
|
||||
final shareUser = TrustedFriendShare_User(
|
||||
userId: Int64(contact.userId),
|
||||
displayName: getContactDisplayName(contact),
|
||||
avatar: contact.avatarSvgCompressed != null
|
||||
? utf8.encode(getAvatarSvg(contact.avatarSvgCompressed!))
|
||||
: null,
|
||||
);
|
||||
|
||||
final trustedFriendShare = TrustedFriendShare(
|
||||
trustedFriend: trustedFriend,
|
||||
shareUser: shareUser,
|
||||
threshold: contact.recoveryContactsThreshold,
|
||||
sharedSecretData: share,
|
||||
);
|
||||
|
||||
final xchacha20 = Xchacha20.poly1305Aead();
|
||||
final secretBox = await xchacha20.encrypt(
|
||||
trustedFriendShare.writeToBuffer(),
|
||||
secretKey: SecretKey(encryptionKey),
|
||||
nonce: xchacha20.newNonce(),
|
||||
);
|
||||
|
||||
final envelope = EncryptedEnvelope(
|
||||
encryptedData: secretBox.cipherText,
|
||||
iv: secretBox.nonce,
|
||||
mac: secretBox.mac.bytes,
|
||||
);
|
||||
|
||||
final res = await apiService.submitRecoveryShare(
|
||||
notificationId: notificationId,
|
||||
encryptedMessage: envelope.writeToBuffer(),
|
||||
);
|
||||
return res.isSuccess;
|
||||
} catch (e) {
|
||||
Log.error('Failed to submit recovery share', error: e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<bool> enablePasswordlessRecovery({
|
||||
required List<int> trustedFriendIds,
|
||||
required SecondFactorType secondFactorType,
|
||||
required String secondFactorValue,
|
||||
required int threshold,
|
||||
}) async {
|
||||
// 1. Get all currently trusted friends contacts and send them delete messages
|
||||
final oldTrustedFriends = await (twonlyDB.select(
|
||||
twonlyDB.contacts,
|
||||
)..where((t) => t.recoveryIsTrustedFriend.equals(true))).get();
|
||||
|
||||
for (final contact in oldTrustedFriends) {
|
||||
try {
|
||||
await sendCipherText(
|
||||
contact.userId,
|
||||
pb.EncryptedContent(
|
||||
passwordlessRecovery: pb.EncryptedContent_PasswordLessRecovery(
|
||||
delete: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
Log.error(
|
||||
'Failed to send delete PasswordLessRecovery message to contact ${contact.userId}: $e',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Reset current recovery data to ensure a clean state
|
||||
|
||||
await twonlyDB.contactsDao.resetRecoveryDataForAllContacts();
|
||||
await UserService.update((u) => u.passwordLessRecovery = null);
|
||||
|
||||
// 2. Update the user model configuration
|
||||
final config = PasswordLessRecovery(threshold);
|
||||
final xchacha20 = Xchacha20.poly1305Aead();
|
||||
|
||||
final config = PasswordLessRecovery();
|
||||
// 2. If enabled, handle the second factor and create serverKey
|
||||
|
||||
final serverKey = getRandomUint8List(32);
|
||||
Uint8List? protectedEmailServerKey;
|
||||
Uint8List? pinUnlockToken;
|
||||
Uint8List? protectedPin;
|
||||
Uint8List? serverKey;
|
||||
Uint8List? encryptedServerKey;
|
||||
|
||||
SecretKey? secondFactorEncryptedServerKeyKey;
|
||||
String? emailHint;
|
||||
|
||||
switch (secondFactorType) {
|
||||
case SecondFactorType.email:
|
||||
final emailSeed = getRandomUint8List(32);
|
||||
|
||||
// Store it, so the user can see it again.
|
||||
config.email = secondFactorValue;
|
||||
|
||||
final chacha20 = FlutterChacha20.poly1305Aead();
|
||||
|
||||
final scrypt = Scrypt(
|
||||
cost: 65536,
|
||||
salt: emailSeed,
|
||||
);
|
||||
|
||||
final protectedEmailKey = scrypt
|
||||
.convert(utf8.encode(secondFactorValue))
|
||||
.bytes;
|
||||
|
||||
// there must be a emailSeed like for the pin...
|
||||
|
||||
// The serverKey is encrypted with the email; this protects the server from seeing the user's email address, while
|
||||
// also ensuring that the server can only send the real secret to the user's configured email, as a different
|
||||
// email will result in a different secret key.
|
||||
final secretBox = await chacha20.encrypt(
|
||||
serverKey,
|
||||
secretKey: SecretKey(
|
||||
Uint8List.fromList(protectedEmailKey),
|
||||
),
|
||||
nonce: chacha20.newNonce(),
|
||||
);
|
||||
// This is only stored so the user can see there email, and verify that he has set the valid mail...
|
||||
config.email = secondFactorValue;
|
||||
emailHint = createEmailHint(secondFactorValue);
|
||||
|
||||
protectedEmailServerKey = EncryptedEnvelope(
|
||||
encryptedData: secretBox.cipherText,
|
||||
// iv: secretBox.nonce,
|
||||
mac: secretBox.mac.bytes,
|
||||
).writeToBuffer();
|
||||
// E-Mail Protection:
|
||||
// - Server can only learn the email during recovery. Ensured as the server gets the NONCE and MAC to decrpyt during recovery.
|
||||
// - Trusted-friends: Server key is send to the mail, they whould need access to the user's mail account.
|
||||
secondFactorEncryptedServerKeyKey = SecretKey(
|
||||
Uint8List.fromList(sha256.convert(utf8.encode(config.email!)).bytes),
|
||||
);
|
||||
|
||||
case SecondFactorType.pin:
|
||||
final pinSeed = getRandomUint8List(32);
|
||||
pinUnlockToken = getRandomUint8List(32);
|
||||
|
||||
// The pin seed is to ensure that the server does never learns the real 4-digit pin. The seed is never send to
|
||||
// the server. As the pin are heavily protected against brute-forcing and will be discared by the server after X
|
||||
// tries, this prevents that a malicous user trigger this deletion...
|
||||
config.pinSeed = base64Encode(pinSeed);
|
||||
config.pinUnlockToken = base64Encode(pinUnlockToken);
|
||||
// The pin seed - never shared with the server - ensures that the server is unable to brute-force real user's pin.
|
||||
config.pinSeed = getRandomUint8List(32);
|
||||
|
||||
final scrypt = Scrypt(
|
||||
cost: 65536,
|
||||
salt: pinSeed,
|
||||
// As the pin is heavily protected against brute-forcing e.g. will be deleted by the server after X tries, the
|
||||
// unlock token is required to prevent a malicous user (except the trusted friends) to triger this deletion.
|
||||
config.pinUnlockToken = getRandomUint8List(32);
|
||||
|
||||
// Brute-force protection for the user's pin:
|
||||
// - Server: Does not know the seed.
|
||||
// - Trusted friends:
|
||||
// Can only check the result X times before the server deletes the key. As they do not have
|
||||
// the mac and the cypher text they are unable to brute-force the pin localy. And the server only allows 10
|
||||
// tries.
|
||||
final pinProtectionKey = await Hmac.sha256().calculateMac(
|
||||
Uint8List.fromList(utf8.encode(secondFactorValue)),
|
||||
secretKey: SecretKey(config.pinSeed!),
|
||||
);
|
||||
|
||||
final key = scrypt.convert(utf8.encode(secondFactorValue)).bytes;
|
||||
protectedPin = key.sublist(0, 32);
|
||||
// To restore the user has to provide the server with this encryption key. The server then can verify the
|
||||
// correct pin was entered, when decrypting the server key as he also receives the mac and nonce from the user
|
||||
// during recovery. Only when the mac is correct the server provides the user with the serverKey.
|
||||
secondFactorEncryptedServerKeyKey = SecretKey(pinProtectionKey.bytes);
|
||||
|
||||
case SecondFactorType.none:
|
||||
}
|
||||
|
||||
// 3. Use the second factor and generate the shares...
|
||||
if (secondFactorEncryptedServerKeyKey != null) {
|
||||
// The server key is used to encrypt the RecoveryData of the users. This ensures that when the trusted friends
|
||||
// colaberate, they additional need the serverKey to decrypt the user's key.
|
||||
serverKey = getRandomUint8List(32);
|
||||
|
||||
// Generate the shares, and store them in the contacrs.table.
|
||||
final secretBox = await xchacha20.encrypt(
|
||||
serverKey,
|
||||
secretKey: secondFactorEncryptedServerKeyKey,
|
||||
nonce: xchacha20.newNonce(),
|
||||
);
|
||||
|
||||
Log.info('Enabling passwordless recovery with:');
|
||||
Log.info(' - Trusted Friends: $trustedFriendIds');
|
||||
Log.info(' - Second Factor Type: $secondFactorType');
|
||||
Log.info(' - Second Factor Value: $secondFactorValue');
|
||||
Log.info(' - Threshold: $threshold');
|
||||
// The server only gets the encrypted server key and the mac. Because the server does not know the nonce (192-bit
|
||||
// because of XChaCha), he is unable to decrypt the server key without the help of the trusted friends. This
|
||||
// ensures that the server never learns the users orginal pin, as he is missing the pin_seed and also unable to
|
||||
// brute-force the email of the user as he does not have the nonce.
|
||||
encryptedServerKey = Uint8List.fromList([
|
||||
...secretBox.cipherText,
|
||||
...secretBox.mac.bytes,
|
||||
]);
|
||||
|
||||
// Use the serverKey to protect the recovery_data_encrypted
|
||||
config
|
||||
..encryptedServerKeyNonce = secretBox.nonce
|
||||
..encryptedServerKey = encryptedServerKey;
|
||||
}
|
||||
|
||||
// 4. Send the data to the server:
|
||||
// 3. Using shamir's secret to generate the shares for the users.
|
||||
|
||||
// to th server
|
||||
// protectedPin, pinUnlockToken, protectedEmailServerKey;
|
||||
// 3.1. Create the SharedSecretData
|
||||
|
||||
// 5. send to the contacts / implement the heartbeat check, and notify the user in case the hearbeath shows that to few users are active...
|
||||
var recoveryData = RecoveryData(
|
||||
userId: Int64(userService.currentUser.userId),
|
||||
keyManager: await RustKeyManager.serialize(),
|
||||
).writeToBuffer();
|
||||
|
||||
if (serverKey != null) {
|
||||
// Second factor was enabled, so encrypt the recoveryData using the serverKey.
|
||||
|
||||
final secretBox = await xchacha20.encrypt(
|
||||
recoveryData,
|
||||
secretKey: SecretKey(serverKey),
|
||||
nonce: xchacha20.newNonce(),
|
||||
);
|
||||
|
||||
recoveryData = EncryptedEnvelope(
|
||||
encryptedData: secretBox.cipherText,
|
||||
iv: secretBox.nonce,
|
||||
mac: secretBox.mac.bytes,
|
||||
).writeToBuffer();
|
||||
}
|
||||
|
||||
final sharedSecretData = SharedSecretData(
|
||||
recoveryData: recoveryData,
|
||||
pinSeed: config.pinSeed,
|
||||
pinUnlockToken: config.pinUnlockToken,
|
||||
emailHint: emailHint,
|
||||
encryptedServerKeyNonce: config.encryptedServerKeyNonce,
|
||||
).writeToBuffer();
|
||||
|
||||
// 3.2. Use the amount of trusted friends to generate the shares
|
||||
|
||||
final List<Uint8List> shares;
|
||||
try {
|
||||
shares = await RustUtils.generateShares(
|
||||
secret: sharedSecretData,
|
||||
total: trustedFriendIds.length,
|
||||
threshold: threshold,
|
||||
);
|
||||
|
||||
if (shares.length != trustedFriendIds.length) {
|
||||
Log.error('shares.length != trustedFriendIds.length');
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
Log.error('Failed to generate secret shares: $e');
|
||||
return false;
|
||||
}
|
||||
|
||||
await UserService.update((u) => u.passwordLessRecovery = config);
|
||||
|
||||
// 3.4. Store the shares in the contact's rows
|
||||
for (final contactId in trustedFriendIds) {
|
||||
await twonlyDB.contactsDao.updateContact(
|
||||
contactId,
|
||||
ContactsCompanion(
|
||||
recoveryIsTrustedFriend: const Value(true),
|
||||
recoveryLastHeartbeat: const Value(null),
|
||||
recoverySecretShare: Value(shares.removeLast()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
unawaited(performHeartbeat());
|
||||
|
||||
// The passwordless is configured sucessfully.
|
||||
return true;
|
||||
}
|
||||
|
||||
static Future<bool> testPin(String pin) async {
|
||||
final config = userService.currentUser.passwordLessRecovery;
|
||||
if (config?.pinSeed == null || config?.encryptedServerKey == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
final pinProtectionKey = await Hmac.sha256().calculateMac(
|
||||
Uint8List.fromList(utf8.encode(pin)),
|
||||
secretKey: SecretKey(config!.pinSeed!),
|
||||
);
|
||||
|
||||
final secondFactorEncryptedServerKeyKey = SecretKey(
|
||||
pinProtectionKey.bytes,
|
||||
);
|
||||
|
||||
final xchacha20 = Xchacha20.poly1305Aead();
|
||||
|
||||
final combined = config.encryptedServerKey!;
|
||||
final cipherText = combined.sublist(0, combined.length - 16);
|
||||
final macBytes = combined.sublist(combined.length - 16);
|
||||
|
||||
final secretBox = SecretBox(
|
||||
cipherText,
|
||||
nonce: config.encryptedServerKeyNonce!,
|
||||
mac: Mac(macBytes),
|
||||
);
|
||||
|
||||
await xchacha20.decrypt(
|
||||
secretBox,
|
||||
secretKey: secondFactorEncryptedServerKeyKey,
|
||||
);
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
Log.error('Failed to test pin: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> performHeartbeat() async {
|
||||
final config = userService.currentUser.passwordLessRecovery;
|
||||
|
||||
if (config != null) {
|
||||
final lastHeartbeat = config.lastServerHeartbeat;
|
||||
final isOlderThanAMonth =
|
||||
lastHeartbeat != null &&
|
||||
clock.now().difference(lastHeartbeat).inDays > 20;
|
||||
|
||||
if ((lastHeartbeat == null || isOlderThanAMonth) &&
|
||||
config.encryptedServerKey != null) {
|
||||
final res = await apiService.registerPasswordLessRecovery(
|
||||
config.encryptedServerKey!,
|
||||
config.pinUnlockToken,
|
||||
);
|
||||
|
||||
if (res.isSuccess) {
|
||||
await UserService.update((u) {
|
||||
u.passwordLessRecovery?.lastServerHeartbeat = clock.now();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
final lastContactHeartbeat = config.lastContactHeartbeat;
|
||||
final isContactHeartbeatOlderThan24h =
|
||||
lastContactHeartbeat == null ||
|
||||
clock.now().difference(lastContactHeartbeat).inHours >= 24;
|
||||
|
||||
if (isContactHeartbeatOlderThan24h) {
|
||||
// Get all contacts where recoveryLastHeartbeat is NULL. Then for each contacts send.
|
||||
// recoveryLastHeartbeat is ONLY updated in case the contact has responded.
|
||||
final pendingShares =
|
||||
await (twonlyDB.select(twonlyDB.contacts)..where(
|
||||
(t) =>
|
||||
t.recoveryIsTrustedFriend.equals(true) &
|
||||
t.recoveryLastHeartbeat.isNull() &
|
||||
t.recoverySecretShare.isNotNull(),
|
||||
))
|
||||
.get();
|
||||
|
||||
for (final contact in pendingShares) {
|
||||
try {
|
||||
await sendCipherText(
|
||||
contact.userId,
|
||||
pb.EncryptedContent(
|
||||
passwordlessRecovery: pb.EncryptedContent_PasswordLessRecovery(
|
||||
recoverySecretShare: contact.recoverySecretShare,
|
||||
delete: false,
|
||||
threshold: Int64(config.threshold),
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
Log.error(
|
||||
'Failed to send PasswordLessRecovery share to contact ${contact.userId}: $e',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await UserService.update((u) {
|
||||
u.passwordLessRecovery?.lastContactHeartbeat = clock.now();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Send heartbeat to the friends I am a trusted friend.
|
||||
final oneWeekAgo = clock.now().subtract(const Duration(days: 7));
|
||||
final trustedFriendsToNotify =
|
||||
await (twonlyDB.select(twonlyDB.contacts)..where(
|
||||
(t) =>
|
||||
t.recoveryContactsSecretShare.isNotNull() &
|
||||
(t.recoveryContactsLastHeartbeat.isNull() |
|
||||
t.recoveryContactsLastHeartbeat.isSmallerThanValue(
|
||||
oneWeekAgo,
|
||||
)),
|
||||
))
|
||||
.get();
|
||||
|
||||
for (final contact in trustedFriendsToNotify) {
|
||||
try {
|
||||
final share = contact.recoveryContactsSecretShare!;
|
||||
final hash = sha256.convert(share).bytes;
|
||||
|
||||
await sendCipherText(
|
||||
contact.userId,
|
||||
pb.EncryptedContent(
|
||||
passwordlessRecoveryHeartbeat:
|
||||
pb.EncryptedContent_PasswordLessRecoveryHeartbeat(
|
||||
hash: hash,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await twonlyDB.contactsDao.updateContact(
|
||||
contact.userId,
|
||||
ContactsCompanion(
|
||||
recoveryContactsLastHeartbeat: Value(clock.now()),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
Log.error(
|
||||
'Failed to send PasswordLessRecoveryHeartbeat to contact ${contact.userId}: $e',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> handlePasswordlessRecovery(
|
||||
int fromUserId,
|
||||
pb.EncryptedContent_PasswordLessRecovery msg,
|
||||
String receiptId,
|
||||
) async {
|
||||
if (msg.delete) {
|
||||
Log.info(
|
||||
'[$receiptId] Received request to delete passwordless recovery share from contact $fromUserId',
|
||||
);
|
||||
await twonlyDB.contactsDao.updateContact(
|
||||
fromUserId,
|
||||
const ContactsCompanion(
|
||||
recoveryContactsSecretShare: Value(null),
|
||||
recoveryContactsLastHeartbeat: Value(null),
|
||||
),
|
||||
);
|
||||
} else if (msg.hasRecoverySecretShare() && msg.hasThreshold()) {
|
||||
Log.info(
|
||||
'[$receiptId] Received new passwordless recovery share from contact $fromUserId',
|
||||
);
|
||||
await twonlyDB.contactsDao.updateContact(
|
||||
fromUserId,
|
||||
ContactsCompanion(
|
||||
recoveryContactsSecretShare: Value(
|
||||
Uint8List.fromList(msg.recoverySecretShare),
|
||||
),
|
||||
recoveryContactsThreshold: Value(msg.threshold.toInt()),
|
||||
recoveryContactsLastHeartbeat: const Value(
|
||||
null, // this will trigger that a heartbeat will be send...
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
unawaited(performHeartbeat());
|
||||
}
|
||||
|
||||
static Future<void> handlePasswordlessRecoveryHeartbeat(
|
||||
int fromUserId,
|
||||
pb.EncryptedContent_PasswordLessRecoveryHeartbeat msg,
|
||||
String receiptId,
|
||||
) async {
|
||||
Log.info(
|
||||
'[$receiptId] Received passwordless recovery heartbeat from contact $fromUserId',
|
||||
);
|
||||
final contact = await twonlyDB.contactsDao.getContactById(fromUserId);
|
||||
final storedShare = contact?.recoverySecretShare;
|
||||
|
||||
if (storedShare == null) {
|
||||
unawaited(
|
||||
sendCipherText(
|
||||
fromUserId,
|
||||
pb.EncryptedContent(
|
||||
passwordlessRecovery: pb.EncryptedContent_PasswordLessRecovery(
|
||||
delete: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
Log.warn(
|
||||
'[$receiptId] Received passwordless recovery heartbeat from $fromUserId but we did not send him a secret share.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final computedHash = sha256.convert(storedShare).bytes;
|
||||
final recoveryLastHeartbeat =
|
||||
const ListEquality().equals(computedHash, msg.hash)
|
||||
? clock.now()
|
||||
: null; // The stored share not valid (maybe a old backup was restored). This will cause the performHeartbeat to resend him his share
|
||||
Log.info(
|
||||
'[$receiptId] Got heartbeat: ($recoveryLastHeartbeat)',
|
||||
);
|
||||
await twonlyDB.contactsDao.updateContact(
|
||||
fromUserId,
|
||||
ContactsCompanion(
|
||||
recoveryLastHeartbeat: Value(recoveryLastHeartbeat),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<bool> checkAndStorePasswordlessMessages(
|
||||
OnboardingState state,
|
||||
) async {
|
||||
if (!state.serverRegistered ||
|
||||
state.notificationId == null ||
|
||||
state.downloadAuthToken == null ||
|
||||
state.encryptionKey == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final alreadyReceivedIds = state.receivedShares
|
||||
.map((s) => Int64(s.messageId))
|
||||
.toList();
|
||||
|
||||
final response = await apiService.checkForPasswordlessNotification(
|
||||
notificationId: state.notificationId!,
|
||||
downloadAuthToken: state.downloadAuthToken!,
|
||||
alreadyReceivedIds: alreadyReceivedIds,
|
||||
);
|
||||
|
||||
if (response == null || response.messages.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final xchacha20 = Xchacha20.poly1305Aead();
|
||||
final secretKey = SecretKey(state.encryptionKey!);
|
||||
var didUpdate = false;
|
||||
|
||||
for (final msg in response.messages) {
|
||||
final msgId = msg.id.toInt();
|
||||
|
||||
try {
|
||||
final envelope = EncryptedEnvelope.fromBuffer(msg.encryptedMessage);
|
||||
final secretBox = SecretBox(
|
||||
envelope.encryptedData,
|
||||
nonce: envelope.iv,
|
||||
mac: Mac(envelope.mac),
|
||||
);
|
||||
final plaintext = await xchacha20.decrypt(
|
||||
secretBox,
|
||||
secretKey: secretKey,
|
||||
);
|
||||
|
||||
final share = TrustedFriendShare.fromBuffer(plaintext);
|
||||
|
||||
final receivedShare = ReceivedRecoveryShare(
|
||||
messageId: msgId,
|
||||
trustedFriendDisplayName: share.trustedFriend.displayName,
|
||||
myDisplayName: share.shareUser.displayName,
|
||||
myUserId: share.shareUser.userId.toInt(),
|
||||
myAvatarSvg: share.shareUser.hasAvatar()
|
||||
? share.shareUser.avatar
|
||||
: null,
|
||||
threshold: share.threshold,
|
||||
sharedSecretDataBytes: share.sharedSecretData,
|
||||
);
|
||||
|
||||
state.receivedShares.add(receivedShare);
|
||||
didUpdate = true;
|
||||
|
||||
Log.info(
|
||||
'Received recovery share from ${share.trustedFriend.displayName} '
|
||||
'for user ${share.shareUser.displayName}',
|
||||
);
|
||||
} catch (e) {
|
||||
Log.error(
|
||||
'Failed to decrypt/parse passwordless notification message $msgId: $e',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (didUpdate) {
|
||||
await KeyValueStore.update<OnboardingState>(
|
||||
key: KeyValueKeys.onboardingState,
|
||||
update: (s) => s.receivedShares = state.receivedShares,
|
||||
);
|
||||
}
|
||||
|
||||
return didUpdate;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1 @@
|
|||
enum SetupProfile { standard, customized, maximum }
|
||||
|
||||
enum SecurityProfile { normal, strict }
|
||||
|
||||
extension SecurityProfileExtension on SecurityProfile {
|
||||
bool get showWarningForNonVerifiedContacts => this == SecurityProfile.strict;
|
||||
bool get showOnlyVerifiedInChatViewList => this == SecurityProfile.normal;
|
||||
}
|
||||
enum SetupProfile { standard, customized }
|
||||
|
|
|
|||
|
|
@ -38,8 +38,9 @@ Future<(EncryptedContent?, PlaintextContent_DecryptionErrorMessage_Type?)>
|
|||
signalDecryptMessage(
|
||||
int fromUserId,
|
||||
Uint8List encryptedContentRaw,
|
||||
int type,
|
||||
) async {
|
||||
int type, {
|
||||
Set<int>? brokenSessionsInCurrentBatch,
|
||||
}) async {
|
||||
// Hold the lock only for the cryptographic operation, not for network I/O
|
||||
Log.info('Acquiring lockingSignalProtocol for $fromUserId');
|
||||
final (
|
||||
|
|
@ -74,6 +75,7 @@ signalDecryptMessage(
|
|||
);
|
||||
}
|
||||
|
||||
recordResyncAttempt(fromUserId, success: true);
|
||||
return (EncryptedContent.fromBuffer(plaintext), null, false);
|
||||
} on InvalidKeyIdException catch (e) {
|
||||
Log.warn(e);
|
||||
|
|
@ -113,22 +115,25 @@ signalDecryptMessage(
|
|||
|
||||
// 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);
|
||||
if (needsResync) {
|
||||
brokenSessionsInCurrentBatch?.add(fromUserId);
|
||||
if (shouldAttemptResync(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
|
||||
recordResyncAttempt(fromUserId, success: false);
|
||||
|
||||
// This message contains a new PreKeyBundle establishing a new signal
|
||||
// session
|
||||
await sendCipherText(
|
||||
fromUserId,
|
||||
EncryptedContent(
|
||||
errorMessages: EncryptedContent_ErrorMessages(
|
||||
type: EncryptedContent_ErrorMessages_Type.SESSION_OUT_OF_SYNC,
|
||||
// This message contains a new PreKeyBundle establishing a new signal
|
||||
// session
|
||||
await sendCipherText(
|
||||
fromUserId,
|
||||
EncryptedContent(
|
||||
errorMessages: EncryptedContent_ErrorMessages(
|
||||
type: EncryptedContent_ErrorMessages_Type.SESSION_OUT_OF_SYNC,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,36 @@
|
|||
import 'dart:math';
|
||||
import 'package:mutex/mutex.dart';
|
||||
|
||||
/// Unified lock for all Signal protocol operations (encryption, decryption, session management).
|
||||
final lockingSignalProtocol = Mutex();
|
||||
|
||||
/// Tracking users who have already been resynced in the current session.
|
||||
final resyncedUsers = <int>{};
|
||||
final Map<int, ({int failureCount, DateTime lastAttempt})> _resyncAttempts = {};
|
||||
|
||||
/// Reset the resync tracking set.
|
||||
void resetResyncedUsers() {
|
||||
resyncedUsers.clear();
|
||||
const int maxResyncAttempts = 3;
|
||||
|
||||
bool shouldAttemptResync(int userId) {
|
||||
final attempt = _resyncAttempts[userId];
|
||||
if (attempt == null) return true;
|
||||
if (attempt.failureCount >= maxResyncAttempts) return false;
|
||||
|
||||
final cooldown = Duration(minutes: 5 * pow(5, attempt.failureCount - 1).toInt());
|
||||
return DateTime.now().difference(attempt.lastAttempt) > cooldown;
|
||||
}
|
||||
|
||||
void recordResyncAttempt(int userId, {required bool success}) {
|
||||
if (success) {
|
||||
_resyncAttempts.remove(userId);
|
||||
} else {
|
||||
final current = _resyncAttempts[userId];
|
||||
_resyncAttempts[userId] = (
|
||||
failureCount: (current?.failureCount ?? 0) + 1,
|
||||
lastAttempt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset the resync tracking set (currently unused, backoff handles expiry naturally).
|
||||
void resetResyncedUsers() {
|
||||
// No-op. We want the backoff state to persist across reconnects.
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:twonly/locator.dart';
|
||||
|
|
@ -128,6 +130,12 @@ Future<void> handleUserStudyUpload() async {
|
|||
});
|
||||
}
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
if (e is http.ClientException ||
|
||||
e is SocketException ||
|
||||
e is TimeoutException) {
|
||||
Log.warn('Error uploading user study data: $e');
|
||||
} else {
|
||||
Log.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,39 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:mutex/mutex.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/model/json/backup.model.dart';
|
||||
import 'package:twonly/src/model/json/onboarding_state.model.dart';
|
||||
import 'package:twonly/src/utils/exclusive_access.utils.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
typedef ModelFactory = ({
|
||||
Object Function(Map<String, dynamic> json) fromJson,
|
||||
Map<String, dynamic> Function(Object value) toJson,
|
||||
Object Function() defaultValue,
|
||||
});
|
||||
|
||||
class KeyValueStore {
|
||||
static final Map<Type, ModelFactory> _registry = {
|
||||
OnboardingState: (
|
||||
fromJson: OnboardingState.fromJson,
|
||||
toJson: (val) => (val as OnboardingState).toJson(),
|
||||
defaultValue: OnboardingState.new,
|
||||
),
|
||||
CurrentBackupStatus: (
|
||||
fromJson: CurrentBackupStatus.fromJson,
|
||||
toJson: (val) => (val as CurrentBackupStatus).toJson(),
|
||||
defaultValue: CurrentBackupStatus.new,
|
||||
),
|
||||
BackupRecovery: (
|
||||
fromJson: BackupRecovery.fromJson,
|
||||
toJson: (val) => (val as BackupRecovery).toJson(),
|
||||
defaultValue: () => BackupRecovery(username: '', password: '', userId: 0),
|
||||
),
|
||||
};
|
||||
|
||||
static final Map<String, Mutex> _mutexes = {};
|
||||
|
||||
static Mutex _getMutex(String key) {
|
||||
|
|
@ -65,4 +92,77 @@ class KeyValueStore {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
static Future<T> update<T>({
|
||||
required String key,
|
||||
required FutureOr<void> Function(T value) update,
|
||||
}) async {
|
||||
final factory = _registry[T];
|
||||
if (factory == null) {
|
||||
throw ArgumentError('Type $T is not registered in KeyValueStore.');
|
||||
}
|
||||
return _exclusive(key, () async {
|
||||
T val;
|
||||
final file = await _getFilePath(key);
|
||||
try {
|
||||
if (file.existsSync()) {
|
||||
final contents = await file.readAsString();
|
||||
val = factory.fromJson(jsonDecode(contents) as Map<String, dynamic>) as T;
|
||||
} else {
|
||||
val = factory.defaultValue() as T;
|
||||
}
|
||||
} catch (e) {
|
||||
Log.warn('Error reading file. Resetting to default.: $e');
|
||||
val = factory.defaultValue() as T;
|
||||
}
|
||||
|
||||
await update(val);
|
||||
|
||||
try {
|
||||
await file.parent.create(recursive: true);
|
||||
await file.writeAsString(jsonEncode(factory.toJson(val as Object)));
|
||||
} catch (e) {
|
||||
Log.error('Error writing file: $e');
|
||||
}
|
||||
return val;
|
||||
});
|
||||
}
|
||||
|
||||
static Future<T> getModel<T>(String key) async {
|
||||
final factory = _registry[T];
|
||||
if (factory == null) {
|
||||
throw ArgumentError('Type $T is not registered in KeyValueStore.');
|
||||
}
|
||||
return _exclusive(key, () async {
|
||||
final file = await _getFilePath(key);
|
||||
try {
|
||||
if (file.existsSync()) {
|
||||
final contents = await file.readAsString();
|
||||
return factory.fromJson(jsonDecode(contents) as Map<String, dynamic>) as T;
|
||||
}
|
||||
} catch (e) {
|
||||
Log.warn('Error reading file. Returning default.: $e');
|
||||
}
|
||||
return factory.defaultValue() as T;
|
||||
});
|
||||
}
|
||||
|
||||
static Future<T?> getModelOrNull<T>(String key) async {
|
||||
final factory = _registry[T];
|
||||
if (factory == null) {
|
||||
throw ArgumentError('Type $T is not registered in KeyValueStore.');
|
||||
}
|
||||
return _exclusive(key, () async {
|
||||
final file = await _getFilePath(key);
|
||||
try {
|
||||
if (file.existsSync()) {
|
||||
final contents = await file.readAsString();
|
||||
return factory.fromJson(jsonDecode(contents) as Map<String, dynamic>) as T;
|
||||
}
|
||||
} catch (e) {
|
||||
Log.warn('Error reading file.: $e');
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,10 +41,14 @@ class Log {
|
|||
}
|
||||
|
||||
static void error(
|
||||
Object? messageInput, [
|
||||
Object? messageInput, {
|
||||
Object? error,
|
||||
StackTrace? stackTrace,
|
||||
]) {
|
||||
bool onlyIfSentryEnabled = false,
|
||||
}) {
|
||||
if (!AppState.allowErrorTrackingViaSentry && onlyIfSentryEnabled) {
|
||||
return;
|
||||
}
|
||||
final message = filterLogMessage('$messageInput');
|
||||
if (AppState.allowErrorTrackingViaSentry) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import 'package:provider/provider.dart';
|
|||
import 'package:twonly/src/localization/generated/app_localizations.dart';
|
||||
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
||||
import 'package:twonly/src/providers/settings.provider.dart';
|
||||
import 'package:twonly/src/services/backup.service.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
||||
|
|
@ -93,10 +94,10 @@ Future<String?> saveVideoToGallery(
|
|||
if (!hasAccess) {
|
||||
await Gal.requestAccess(toAlbum: true);
|
||||
}
|
||||
|
||||
|
||||
var pathToSave = videoPath;
|
||||
File? tempFile;
|
||||
|
||||
|
||||
try {
|
||||
if (name != null) {
|
||||
final file = File(videoPath);
|
||||
|
|
@ -405,3 +406,43 @@ String joinWithAnd(List<String> items, String andWord) {
|
|||
if (items.length == 1) return items.first;
|
||||
return '${items.sublist(0, items.length - 1).join(', ')} $andWord ${items.last}';
|
||||
}
|
||||
|
||||
String createEmailHint(String email) {
|
||||
final parts = email.split('@');
|
||||
if (parts.length != 2) return email;
|
||||
|
||||
final local = parts[0];
|
||||
final domain = parts[1];
|
||||
|
||||
final localMasked = local.length > 2
|
||||
? '${local[0]}${'*' * (local.length - 2)}${local[local.length - 1]}'
|
||||
: local;
|
||||
|
||||
final domainParts = domain.split('.');
|
||||
if (domainParts.isEmpty) return '$localMasked@$domain';
|
||||
|
||||
final domainName = domainParts[0];
|
||||
final domainNameMasked = domainName.length > 1
|
||||
? '${domainName[0]}${'*' * (domainName.length - 1)}'
|
||||
: domainName;
|
||||
|
||||
final restDomain = domainParts.skip(1).join('.');
|
||||
return '$localMasked@$domainNameMasked${restDomain.isNotEmpty ? '.$restDomain' : ''}';
|
||||
}
|
||||
|
||||
extension RecoveryErrorLocalization on RecoveryError {
|
||||
String toLocalizedString(BuildContext context) {
|
||||
switch (this) {
|
||||
case RecoveryError.noInternet:
|
||||
return context.lang.recoverErrorNoInternet;
|
||||
case RecoveryError.usernameNotValid:
|
||||
return context.lang.recoverErrorUsernameNotValid;
|
||||
case RecoveryError.passwordInvalid:
|
||||
return context.lang.recoverErrorPasswordInvalid;
|
||||
case RecoveryError.tryAgainLater:
|
||||
return context.lang.recoverErrorTryAgainLater;
|
||||
case RecoveryError.unkownError:
|
||||
return context.lang.recoverErrorUnknown;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,12 +16,14 @@ class AvatarIcon extends StatefulWidget {
|
|||
this.contactId,
|
||||
this.myAvatar = false,
|
||||
this.fontSize = 20,
|
||||
this.svg,
|
||||
this.color,
|
||||
});
|
||||
final Group? group;
|
||||
final int? contactId;
|
||||
final bool myAvatar;
|
||||
final double? fontSize;
|
||||
final String? svg;
|
||||
final Color? color;
|
||||
|
||||
@override
|
||||
|
|
@ -145,7 +147,12 @@ class _AvatarIconState extends State<AvatarIcon> {
|
|||
|
||||
Widget avatars = Container();
|
||||
|
||||
if (widget.myAvatar) {
|
||||
if (widget.svg != null) {
|
||||
avatars = SvgPicture.string(
|
||||
widget.svg!,
|
||||
errorBuilder: errorBuilder,
|
||||
);
|
||||
} else if (widget.myAvatar) {
|
||||
if (_myAvatarPath != null) {
|
||||
avatars = Image.file(
|
||||
File(_myAvatarPath!),
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import 'dart:io';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/utils/avatars.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/qr.utils.dart';
|
||||
|
|
@ -16,6 +18,65 @@ class ProfileQrCodeComp extends StatefulWidget {
|
|||
final double size;
|
||||
final bool showAvatar;
|
||||
|
||||
static Future<void> showSheet(
|
||||
BuildContext context, {
|
||||
Contact? contact,
|
||||
bool openToVerify = false,
|
||||
}) {
|
||||
return showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: context.color.surface,
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(24),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(width: double.infinity),
|
||||
Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: context.color.onSurface.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
openToVerify
|
||||
? (contact != null
|
||||
? context.lang.letUserScanQrCode(
|
||||
getContactDisplayName(contact),
|
||||
)
|
||||
: context.lang.letFriendScanQrToVerify)
|
||||
: (contact != null
|
||||
? context.lang.letUserScanQrCode(
|
||||
getContactDisplayName(contact),
|
||||
)
|
||||
: context.lang.addContactQrSheetSubtext),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: context.color.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const ProfileQrCodeComp(),
|
||||
const SizedBox(height: 34),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
State<ProfileQrCodeComp> createState() => _ProfileQrCodeCompState();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ class VerificationBadgeComp extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _VerificationBadgeCompState extends State<VerificationBadgeComp> {
|
||||
bool _isVerified = false;
|
||||
late bool _isVerified = widget.contact?.verified ?? true;
|
||||
bool _isSharedVerified = false;
|
||||
int _verifiedByTransferredTrustCount = 0;
|
||||
int _sharedByVerifiedCount = 0;
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@ import 'package:flutter/material.dart';
|
|||
import 'package:font_awesome_flutter/font_awesome_flutter.dart'
|
||||
show FaIcon, FontAwesomeIcons;
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/constants/routes.keys.dart';
|
||||
import 'package:twonly/src/services/profile.service.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/components/profile_qr_code.comp.dart';
|
||||
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||
import 'package:twonly/src/visual/elements/svg_icon.element.dart';
|
||||
import 'package:twonly/src/visual/themes/light.dart';
|
||||
|
|
@ -15,12 +16,62 @@ const colorVerificationBadgeYellow = Color.fromARGB(255, 0, 182, 238);
|
|||
class VerificationBadgeInfo extends StatelessWidget {
|
||||
const VerificationBadgeInfo({
|
||||
this.displayButtons = false,
|
||||
this.contact,
|
||||
super.key,
|
||||
});
|
||||
final bool displayButtons;
|
||||
final Contact? contact;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scanButton = MyButton(
|
||||
variant: MyButtonVariant.primaryDense,
|
||||
onPressed: () => context.push(
|
||||
Routes.cameraQRScanner,
|
||||
extra: {
|
||||
if (contact != null) 'contact': contact,
|
||||
'openToVerify': true,
|
||||
},
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const FaIcon(FontAwesomeIcons.camera),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
contact != null
|
||||
? context.lang.scanUserQrCode(
|
||||
getContactDisplayName(contact!),
|
||||
)
|
||||
: context.lang.scanNow,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
final openButton = MyButton(
|
||||
variant: MyButtonVariant.primaryDense,
|
||||
onPressed: () => ProfileQrCodeComp.showSheet(
|
||||
context,
|
||||
contact: contact,
|
||||
openToVerify: true,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const FaIcon(FontAwesomeIcons.qrcode),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
contact != null
|
||||
? context.lang.openOwnQrCode
|
||||
: context.lang.openQrCode,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
RichText(
|
||||
|
|
@ -40,56 +91,45 @@ class VerificationBadgeInfo extends StatelessWidget {
|
|||
icon: const SvgIcon(assetPath: SvgIcons.verifiedGreen, size: 40),
|
||||
description: context.lang.verificationBadgeGreenDesc,
|
||||
boldTextColor: primaryColor,
|
||||
onTap: () => context.push(Routes.cameraQRScanner),
|
||||
onTap: () => context.push(
|
||||
Routes.cameraQRScanner,
|
||||
extra: {
|
||||
if (contact != null) 'contact': contact,
|
||||
'openToVerify': true,
|
||||
},
|
||||
),
|
||||
),
|
||||
if (displayButtons)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 20),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IntrinsicWidth(
|
||||
child: MyButton(
|
||||
variant: MyButtonVariant.primaryDense,
|
||||
onPressed: () => context.push(Routes.cameraQRScanner),
|
||||
child: Row(
|
||||
children: [
|
||||
const FaIcon(FontAwesomeIcons.camera),
|
||||
const SizedBox(width: 6),
|
||||
Text(context.lang.scanNow),
|
||||
],
|
||||
),
|
||||
child: contact != null
|
||||
? Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
scanButton,
|
||||
const SizedBox(height: 8),
|
||||
openButton,
|
||||
],
|
||||
)
|
||||
: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IntrinsicWidth(child: scanButton),
|
||||
const SizedBox(width: 8),
|
||||
IntrinsicWidth(child: openButton),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
IntrinsicWidth(
|
||||
child: MyButton(
|
||||
variant: MyButtonVariant.primaryDense,
|
||||
onPressed: () => context.push(Routes.settingsPublicProfile),
|
||||
child: Row(
|
||||
children: [
|
||||
const FaIcon(FontAwesomeIcons.qrcode),
|
||||
const SizedBox(width: 6),
|
||||
Text(context.lang.openQrCode),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (userService.currentUser.securityProfile != SecurityProfile.strict ||
|
||||
userService.currentUser.isUserDiscoveryEnabled)
|
||||
_buildItem(
|
||||
context,
|
||||
icon: const SvgIcon(
|
||||
assetPath: SvgIcons.verifiedGreen,
|
||||
size: 40,
|
||||
color: colorVerificationBadgeYellow,
|
||||
),
|
||||
description: context.lang.verificationBadgeYellowDesc,
|
||||
boldTextColor: colorVerificationBadgeYellow,
|
||||
_buildItem(
|
||||
context,
|
||||
icon: const SvgIcon(
|
||||
assetPath: SvgIcons.verifiedGreen,
|
||||
size: 40,
|
||||
color: colorVerificationBadgeYellow,
|
||||
),
|
||||
description: context.lang.verificationBadgeYellowDesc,
|
||||
boldTextColor: colorVerificationBadgeYellow,
|
||||
),
|
||||
_buildItem(
|
||||
context,
|
||||
icon: const SvgIcon(assetPath: SvgIcons.verifiedRed, size: 40),
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ class GroupContextMenu extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final navigator = Navigator.of(context);
|
||||
return ContextMenu(
|
||||
items: [
|
||||
if (!group.archived)
|
||||
|
|
@ -27,9 +28,7 @@ class GroupContextMenu extends StatelessWidget {
|
|||
title: context.lang.contextMenuArchiveUser,
|
||||
onTap: () async {
|
||||
const update = GroupsCompanion(archived: Value(true));
|
||||
if (context.mounted) {
|
||||
await twonlyDB.groupsDao.updateGroup(group.groupId, update);
|
||||
}
|
||||
await twonlyDB.groupsDao.updateGroup(group.groupId, update);
|
||||
},
|
||||
icon: Icons.archive_outlined,
|
||||
),
|
||||
|
|
@ -38,15 +37,13 @@ class GroupContextMenu extends StatelessWidget {
|
|||
title: context.lang.contextMenuUndoArchiveUser,
|
||||
onTap: () async {
|
||||
const update = GroupsCompanion(archived: Value(false));
|
||||
if (context.mounted) {
|
||||
await twonlyDB.groupsDao.updateGroup(group.groupId, update);
|
||||
}
|
||||
await twonlyDB.groupsDao.updateGroup(group.groupId, update);
|
||||
},
|
||||
icon: Icons.unarchive_outlined,
|
||||
),
|
||||
ContextMenuItem(
|
||||
title: context.lang.contextMenuOpenChat,
|
||||
onTap: () => context.push(Routes.chatsMessages(group.groupId)),
|
||||
onTap: () => navigator.context.push(Routes.chatsMessages(group.groupId)),
|
||||
icon: FontAwesomeIcons.comments,
|
||||
),
|
||||
if (!group.archived)
|
||||
|
|
@ -56,9 +53,7 @@ class GroupContextMenu extends StatelessWidget {
|
|||
: context.lang.contextMenuPin,
|
||||
onTap: () async {
|
||||
final update = GroupsCompanion(pinned: Value(!group.pinned));
|
||||
if (context.mounted) {
|
||||
await twonlyDB.groupsDao.updateGroup(group.groupId, update);
|
||||
}
|
||||
await twonlyDB.groupsDao.updateGroup(group.groupId, update);
|
||||
},
|
||||
icon: group.pinned
|
||||
? FontAwesomeIcons.thumbtackSlash
|
||||
|
|
@ -69,9 +64,9 @@ class GroupContextMenu extends StatelessWidget {
|
|||
icon: FontAwesomeIcons.trashCan,
|
||||
onTap: () async {
|
||||
final ok = await showAlertDialog(
|
||||
context,
|
||||
context.lang.deleteTitle,
|
||||
context.lang.groupContextMenuDeleteGroup,
|
||||
navigator.context,
|
||||
navigator.context.lang.deleteTitle,
|
||||
navigator.context.lang.groupContextMenuDeleteGroup,
|
||||
);
|
||||
if (ok) {
|
||||
await twonlyDB.messagesDao.deleteMessagesByGroupId(group.groupId);
|
||||
|
|
|
|||
|
|
@ -17,12 +17,13 @@ class UserContextMenu extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final navigator = Navigator.of(context);
|
||||
return ContextMenu(
|
||||
minWidth: 150,
|
||||
items: [
|
||||
ContextMenuItem(
|
||||
title: context.lang.contextMenuUserProfile,
|
||||
onTap: () => context.push(Routes.profileContact(contact.userId)),
|
||||
onTap: () => navigator.context.push(Routes.profileContact(contact.userId)),
|
||||
icon: FontAwesomeIcons.user,
|
||||
),
|
||||
],
|
||||
|
|
|
|||
68
lib/src/visual/elements/contact_chip.element.dart
Normal file
68
lib/src/visual/elements/contact_chip.element.dart
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
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/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
||||
import 'package:twonly/src/visual/components/verification_badge.comp.dart';
|
||||
import 'package:twonly/src/visual/elements/reactive_tap_feedback.element.dart';
|
||||
import 'package:twonly/src/visual/views/contact/contact.view.dart';
|
||||
|
||||
class ContactChip extends StatelessWidget {
|
||||
const ContactChip({
|
||||
required this.contact,
|
||||
this.onTap,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Contact contact;
|
||||
final void Function(int)? onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ReactiveTapFeedback(
|
||||
onTap: () {
|
||||
if (onTap != null) {
|
||||
onTap!(contact.userId);
|
||||
} else {
|
||||
context.navPush(
|
||||
ContactView(
|
||||
contact.userId,
|
||||
key: ValueKey(contact.userId),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Chip(
|
||||
avatar: AvatarIcon(
|
||||
contactId: contact.userId,
|
||||
fontSize: 10,
|
||||
),
|
||||
label: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
getContactDisplayName(contact),
|
||||
style: const TextStyle(fontSize: 14),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
VerificationBadgeComp(
|
||||
contact: contact,
|
||||
size: 12,
|
||||
clickable: false,
|
||||
),
|
||||
if (onTap != null) ...[
|
||||
const SizedBox(width: 15),
|
||||
const FaIcon(
|
||||
FontAwesomeIcons.xmark,
|
||||
color: Colors.grey,
|
||||
size: 12,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/physics.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/elements/reactive_tap_feedback.element.dart';
|
||||
import 'package:twonly/src/visual/themes/light.dart';
|
||||
|
||||
enum MyButtonVariant {
|
||||
|
|
@ -32,72 +32,10 @@ class MyButton extends StatefulWidget {
|
|||
State<MyButton> createState() => _MyButtonState();
|
||||
}
|
||||
|
||||
class _MyButtonState extends State<MyButton>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller =
|
||||
AnimationController(
|
||||
vsync: this,
|
||||
lowerBound: double.negativeInfinity,
|
||||
upperBound: double.infinity,
|
||||
value: 0,
|
||||
)..addListener(() {
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onTapDown(TapDownDetails details) {
|
||||
if (widget.onPressed != null || widget.onLongPress != null) {
|
||||
_controller.animateTo(
|
||||
1,
|
||||
duration: const Duration(milliseconds: 60),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onTapUp(TapUpDetails details) {
|
||||
if (widget.onPressed != null || widget.onLongPress != null) {
|
||||
_bounce();
|
||||
}
|
||||
}
|
||||
|
||||
void _onTapCancel() {
|
||||
if (widget.onPressed != null || widget.onLongPress != null) {
|
||||
_bounce();
|
||||
}
|
||||
}
|
||||
|
||||
void _bounce() {
|
||||
const spring = SpringDescription(
|
||||
mass: 1,
|
||||
stiffness: 400,
|
||||
damping: 15,
|
||||
);
|
||||
final simulation = SpringSimulation(
|
||||
spring,
|
||||
_controller.value,
|
||||
0,
|
||||
_controller.velocity,
|
||||
);
|
||||
_controller.animateWith(simulation);
|
||||
}
|
||||
class _MyButtonState extends State<MyButton> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 0 (unpressed) -> scale 1.0
|
||||
// 1 (pressed) -> scale 0.98 (subtle bounce)
|
||||
final scale = 1.0 - (_controller.value * 0.02);
|
||||
final isEnabled = widget.onPressed != null || widget.onLongPress != null;
|
||||
final isDark = isDarkMode(context);
|
||||
final disabledBgColor = isDark
|
||||
|
|
@ -265,18 +203,11 @@ class _MyButtonState extends State<MyButton>
|
|||
child: widget.child,
|
||||
);
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTapDown: isEnabled ? _onTapDown : null,
|
||||
onTapUp: isEnabled ? _onTapUp : null,
|
||||
onTapCancel: isEnabled ? _onTapCancel : null,
|
||||
return ReactiveTapFeedback(
|
||||
onTap: widget.onPressed,
|
||||
onLongPress: widget.onLongPress,
|
||||
child: Transform.scale(
|
||||
scale: scale,
|
||||
child: AbsorbPointer(
|
||||
child: childButton,
|
||||
),
|
||||
child: AbsorbPointer(
|
||||
child: childButton,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/physics.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/elements/reactive_tap_feedback.element.dart';
|
||||
import 'package:twonly/src/visual/themes/light.dart';
|
||||
|
||||
enum MyIconButtonVariant {
|
||||
|
|
@ -26,69 +26,10 @@ class MyIconButton extends StatefulWidget {
|
|||
State<MyIconButton> createState() => _MyIconButtonState();
|
||||
}
|
||||
|
||||
class _MyIconButtonState extends State<MyIconButton>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
lowerBound: double.negativeInfinity,
|
||||
upperBound: double.infinity,
|
||||
value: 0,
|
||||
)..addListener(() {
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onTapDown(TapDownDetails details) {
|
||||
if (widget.onPressed != null || widget.onLongPress != null) {
|
||||
_controller.animateTo(
|
||||
1,
|
||||
duration: const Duration(milliseconds: 60),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onTapUp(TapUpDetails details) {
|
||||
if (widget.onPressed != null || widget.onLongPress != null) {
|
||||
_bounce();
|
||||
}
|
||||
}
|
||||
|
||||
void _onTapCancel() {
|
||||
if (widget.onPressed != null || widget.onLongPress != null) {
|
||||
_bounce();
|
||||
}
|
||||
}
|
||||
|
||||
void _bounce() {
|
||||
const spring = SpringDescription(
|
||||
mass: 1,
|
||||
stiffness: 400,
|
||||
damping: 15,
|
||||
);
|
||||
final simulation = SpringSimulation(
|
||||
spring,
|
||||
_controller.value,
|
||||
0,
|
||||
_controller.velocity,
|
||||
);
|
||||
_controller.animateWith(simulation);
|
||||
}
|
||||
class _MyIconButtonState extends State<MyIconButton> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scale = 1.0 - (_controller.value * 0.02);
|
||||
final isEnabled = widget.onPressed != null || widget.onLongPress != null;
|
||||
final isDark = isDarkMode(context);
|
||||
final disabledBgColor = isDark
|
||||
|
|
@ -127,18 +68,11 @@ class _MyIconButtonState extends State<MyIconButton>
|
|||
child: widget.icon,
|
||||
);
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTapDown: isEnabled ? _onTapDown : null,
|
||||
onTapUp: isEnabled ? _onTapUp : null,
|
||||
onTapCancel: isEnabled ? _onTapCancel : null,
|
||||
return ReactiveTapFeedback(
|
||||
onTap: widget.onPressed,
|
||||
onLongPress: widget.onLongPress,
|
||||
child: Transform.scale(
|
||||
scale: scale,
|
||||
child: AbsorbPointer(
|
||||
child: childButton,
|
||||
),
|
||||
child: AbsorbPointer(
|
||||
child: childButton,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/physics.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/elements/reactive_tap_feedback.element.dart';
|
||||
|
||||
class MyInput extends StatefulWidget {
|
||||
const MyInput({
|
||||
|
|
@ -18,6 +18,8 @@ class MyInput extends StatefulWidget {
|
|||
this.errorText,
|
||||
this.obscureText = false,
|
||||
this.dense = false,
|
||||
this.readOnly = false,
|
||||
this.fontWeight,
|
||||
super.key,
|
||||
});
|
||||
|
||||
|
|
@ -33,70 +35,16 @@ class MyInput extends StatefulWidget {
|
|||
final String? errorText;
|
||||
final bool obscureText;
|
||||
final bool dense;
|
||||
final bool readOnly;
|
||||
final FontWeight? fontWeight;
|
||||
|
||||
@override
|
||||
State<MyInput> createState() => _MyInputState();
|
||||
}
|
||||
|
||||
class _MyInputState extends State<MyInput> with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller =
|
||||
AnimationController(
|
||||
vsync: this,
|
||||
lowerBound: double.negativeInfinity,
|
||||
upperBound: double.infinity,
|
||||
value: 0,
|
||||
)..addListener(() {
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onTapDown(TapDownDetails details) {
|
||||
_controller.animateTo(
|
||||
1,
|
||||
duration: const Duration(milliseconds: 60),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
}
|
||||
|
||||
void _onTapUp(TapUpDetails details) {
|
||||
_bounce();
|
||||
}
|
||||
|
||||
void _onTapCancel() {
|
||||
_bounce();
|
||||
}
|
||||
|
||||
void _bounce() {
|
||||
const spring = SpringDescription(
|
||||
mass: 1,
|
||||
stiffness: 400,
|
||||
damping: 15,
|
||||
);
|
||||
final simulation = SpringSimulation(
|
||||
spring,
|
||||
_controller.value,
|
||||
0,
|
||||
_controller.velocity,
|
||||
);
|
||||
_controller.animateWith(simulation);
|
||||
}
|
||||
|
||||
class _MyInputState extends State<MyInput> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 0 (unpressed) -> scale 1.0
|
||||
// 1 (pressed) -> scale 0.98 (subtle bounce)
|
||||
final scale = 1.0 - (_controller.value * 0.02);
|
||||
final isDark = isDarkMode(context);
|
||||
|
||||
final inputFillColor = isDark
|
||||
|
|
@ -115,125 +63,121 @@ class _MyInputState extends State<MyInput> with SingleTickerProviderStateMixin {
|
|||
? Colors.white.withValues(alpha: 0.6)
|
||||
: Colors.black.withValues(alpha: 0.6);
|
||||
|
||||
return GestureDetector(
|
||||
return ReactiveTapFeedback(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTapDown: _onTapDown,
|
||||
onTapUp: _onTapUp,
|
||||
onTapCancel: _onTapCancel,
|
||||
child: Transform.scale(
|
||||
scale: scale,
|
||||
child: TextField(
|
||||
controller: widget.controller,
|
||||
onChanged: widget.onChanged,
|
||||
onSubmitted: widget.onSubmitted,
|
||||
onTapOutside: (event) {
|
||||
final pointer = event.pointer;
|
||||
final startPosition = event.position;
|
||||
var moved = false;
|
||||
alwaysAnimate: true,
|
||||
child: TextField(
|
||||
controller: widget.controller,
|
||||
readOnly: widget.readOnly,
|
||||
onChanged: widget.onChanged,
|
||||
onSubmitted: widget.onSubmitted,
|
||||
onTapOutside: (event) {
|
||||
final pointer = event.pointer;
|
||||
final startPosition = event.position;
|
||||
var moved = false;
|
||||
|
||||
void handlePointerEvent(PointerEvent routeEvent) {
|
||||
if (routeEvent is PointerMoveEvent) {
|
||||
if ((routeEvent.position - startPosition).distance > 10) {
|
||||
moved = true;
|
||||
}
|
||||
} else if (routeEvent is PointerUpEvent) {
|
||||
GestureBinding.instance.pointerRouter.removeRoute(
|
||||
pointer,
|
||||
handlePointerEvent,
|
||||
);
|
||||
if (!moved) {
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
}
|
||||
} else if (routeEvent is PointerCancelEvent) {
|
||||
GestureBinding.instance.pointerRouter.removeRoute(
|
||||
pointer,
|
||||
handlePointerEvent,
|
||||
);
|
||||
void handlePointerEvent(PointerEvent routeEvent) {
|
||||
if (routeEvent is PointerMoveEvent) {
|
||||
if ((routeEvent.position - startPosition).distance > 10) {
|
||||
moved = true;
|
||||
}
|
||||
} else if (routeEvent is PointerUpEvent) {
|
||||
GestureBinding.instance.pointerRouter.removeRoute(
|
||||
pointer,
|
||||
handlePointerEvent,
|
||||
);
|
||||
if (!moved) {
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
}
|
||||
} else if (routeEvent is PointerCancelEvent) {
|
||||
GestureBinding.instance.pointerRouter.removeRoute(
|
||||
pointer,
|
||||
handlePointerEvent,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
GestureBinding.instance.pointerRouter.addRoute(
|
||||
pointer,
|
||||
handlePointerEvent,
|
||||
);
|
||||
},
|
||||
inputFormatters: widget.inputFormatters,
|
||||
keyboardType: widget.keyboardType,
|
||||
autofocus: widget.autofocus,
|
||||
obscureText: widget.obscureText,
|
||||
style: TextStyle(
|
||||
GestureBinding.instance.pointerRouter.addRoute(
|
||||
pointer,
|
||||
handlePointerEvent,
|
||||
);
|
||||
},
|
||||
inputFormatters: widget.inputFormatters,
|
||||
keyboardType: widget.keyboardType,
|
||||
autofocus: widget.autofocus,
|
||||
obscureText: widget.obscureText,
|
||||
style: TextStyle(
|
||||
fontSize: widget.dense ? 16 : 18,
|
||||
fontWeight: widget.fontWeight ?? FontWeight.w500,
|
||||
color: isDark ? Colors.white : Colors.black87,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
isDense: widget.dense,
|
||||
hintText: widget.hintText,
|
||||
hintStyle: TextStyle(
|
||||
color: inputHintColor,
|
||||
fontSize: widget.dense ? 16 : 18,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: isDark ? Colors.white : Colors.black87,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
isDense: widget.dense,
|
||||
hintText: widget.hintText,
|
||||
hintStyle: TextStyle(
|
||||
color: inputHintColor,
|
||||
fontSize: widget.dense ? 16 : 18,
|
||||
filled: true,
|
||||
fillColor: inputFillColor,
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
vertical: widget.dense ? 13 : 18,
|
||||
horizontal: widget.dense ? 13 : 24,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(widget.dense ? 12 : 18),
|
||||
borderSide: BorderSide(
|
||||
color: inputBorderColor,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: inputFillColor,
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
vertical: widget.dense ? 13 : 18,
|
||||
horizontal: widget.dense ? 13 : 24,
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(widget.dense ? 12 : 18),
|
||||
borderSide: BorderSide(
|
||||
color: inputBorderColor,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(widget.dense ? 12 : 18),
|
||||
borderSide: BorderSide(
|
||||
color: inputBorderColor,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(widget.dense ? 12 : 18),
|
||||
borderSide: BorderSide(
|
||||
color: isDark ? Colors.white : Colors.black87,
|
||||
width: 2,
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(widget.dense ? 12 : 18),
|
||||
borderSide: BorderSide(
|
||||
color: inputBorderColor,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(widget.dense ? 12 : 18),
|
||||
borderSide: BorderSide(
|
||||
color: isDark ? Colors.white : Colors.black87,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(widget.dense ? 12 : 18),
|
||||
borderSide: const BorderSide(
|
||||
color: Colors.redAccent,
|
||||
),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(widget.dense ? 12 : 18),
|
||||
borderSide: const BorderSide(
|
||||
color: Colors.redAccent,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
errorStyle: const TextStyle(
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(widget.dense ? 12 : 18),
|
||||
borderSide: const BorderSide(
|
||||
color: Colors.redAccent,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
errorText: widget.errorText,
|
||||
prefixIcon: widget.prefixIcon != null
|
||||
? IconTheme(
|
||||
data: IconThemeData(
|
||||
color: prefixIconColor,
|
||||
),
|
||||
child: widget.prefixIcon!,
|
||||
)
|
||||
: null,
|
||||
suffixIcon: widget.suffixIcon != null
|
||||
? IconTheme(
|
||||
data: IconThemeData(
|
||||
color: isDark ? Colors.white : Colors.black87,
|
||||
),
|
||||
child: widget.suffixIcon!,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(widget.dense ? 12 : 18),
|
||||
borderSide: const BorderSide(
|
||||
color: Colors.redAccent,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
errorStyle: const TextStyle(
|
||||
color: Colors.redAccent,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
errorText: widget.errorText,
|
||||
prefixIcon: widget.prefixIcon != null
|
||||
? IconTheme(
|
||||
data: IconThemeData(
|
||||
color: prefixIconColor,
|
||||
),
|
||||
child: widget.prefixIcon!,
|
||||
)
|
||||
: null,
|
||||
suffixIcon: widget.suffixIcon != null
|
||||
? IconTheme(
|
||||
data: IconThemeData(
|
||||
color: isDark ? Colors.white : Colors.black87,
|
||||
),
|
||||
child: widget.suffixIcon!,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
108
lib/src/visual/elements/reactive_tap_feedback.element.dart
Normal file
108
lib/src/visual/elements/reactive_tap_feedback.element.dart
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/physics.dart';
|
||||
|
||||
class ReactiveTapFeedback extends StatefulWidget {
|
||||
const ReactiveTapFeedback({
|
||||
required this.child,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
this.behavior = HitTestBehavior.opaque,
|
||||
this.alwaysAnimate = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final VoidCallback? onTap;
|
||||
final VoidCallback? onLongPress;
|
||||
final HitTestBehavior behavior;
|
||||
final bool alwaysAnimate;
|
||||
|
||||
@override
|
||||
State<ReactiveTapFeedback> createState() => _ReactiveTapFeedbackState();
|
||||
}
|
||||
|
||||
class _ReactiveTapFeedbackState extends State<ReactiveTapFeedback>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
lowerBound: double.negativeInfinity,
|
||||
upperBound: double.infinity,
|
||||
value: 0,
|
||||
)..addListener(() {
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool get _shouldAnimate =>
|
||||
widget.alwaysAnimate ||
|
||||
widget.onTap != null ||
|
||||
widget.onLongPress != null;
|
||||
|
||||
void _onTapDown(TapDownDetails details) {
|
||||
if (_shouldAnimate) {
|
||||
_controller.animateTo(
|
||||
1,
|
||||
duration: const Duration(milliseconds: 60),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onTapUp(TapUpDetails details) {
|
||||
if (_shouldAnimate) {
|
||||
_bounce();
|
||||
}
|
||||
}
|
||||
|
||||
void _onTapCancel() {
|
||||
if (_shouldAnimate) {
|
||||
_bounce();
|
||||
}
|
||||
}
|
||||
|
||||
void _bounce() {
|
||||
const spring = SpringDescription(
|
||||
mass: 1,
|
||||
stiffness: 400,
|
||||
damping: 15,
|
||||
);
|
||||
final simulation = SpringSimulation(
|
||||
spring,
|
||||
_controller.value,
|
||||
0,
|
||||
_controller.velocity,
|
||||
);
|
||||
_controller.animateWith(simulation);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 0 (unpressed) -> scale 1.0
|
||||
// 1 (pressed) -> scale 0.98 (subtle bounce)
|
||||
final scale = 1.0 - (_controller.value * 0.02);
|
||||
|
||||
return GestureDetector(
|
||||
behavior: widget.behavior,
|
||||
onTapDown: _shouldAnimate ? _onTapDown : null,
|
||||
onTapUp: _shouldAnimate ? _onTapUp : null,
|
||||
onTapCancel: _shouldAnimate ? _onTapCancel : null,
|
||||
onTap: widget.onTap,
|
||||
onLongPress: widget.onLongPress,
|
||||
child: Transform.scale(
|
||||
scale: scale,
|
||||
child: widget.child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -28,7 +28,7 @@ class ScreenshotImageHelper {
|
|||
try {
|
||||
return imageBytes = await imageBytesFuture;
|
||||
} catch (e) {
|
||||
Log.error('Could not resolve imageBytesFuture: $e');
|
||||
Log.warn('Could not resolve imageBytesFuture: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -36,14 +36,14 @@ class ScreenshotImageHelper {
|
|||
try {
|
||||
return imageBytes = await file!.readAsBytes();
|
||||
} catch (e) {
|
||||
Log.error('Could not read bytes from file: $e');
|
||||
Log.warn('Could not read bytes from file: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (image == null) return null;
|
||||
final img = await image!.toByteData(format: io.ImageByteFormat.png);
|
||||
if (img == null) {
|
||||
Log.error('Got no image');
|
||||
Log.warn('Got no image');
|
||||
return null;
|
||||
}
|
||||
return imageBytes = img.buffer.asUint8List();
|
||||
|
|
@ -94,7 +94,7 @@ class ScreenshotController {
|
|||
});
|
||||
return completer.future;
|
||||
}
|
||||
Log.error(e);
|
||||
Log.warn('Could not capture screenshot: $e');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -289,12 +289,18 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
mc.cameraController == null) {
|
||||
return;
|
||||
}
|
||||
await mc.cameraController?.setZoomLevel(
|
||||
newScale.clamp(
|
||||
mc.selectedCameraDetails.minAvailableZoom,
|
||||
mc.selectedCameraDetails.maxAvailableZoom,
|
||||
),
|
||||
);
|
||||
try {
|
||||
if (mc.cameraController!.value.isInitialized) {
|
||||
await mc.cameraController!.setZoomLevel(
|
||||
newScale.clamp(
|
||||
mc.selectedCameraDetails.minAvailableZoom,
|
||||
mc.selectedCameraDetails.maxAvailableZoom,
|
||||
),
|
||||
);
|
||||
}
|
||||
} on CameraException catch (e) {
|
||||
Log.warn('Failed to set zoom level: $e');
|
||||
}
|
||||
setState(() {
|
||||
mc.selectedCameraDetails.scaleFactor = newScale;
|
||||
});
|
||||
|
|
@ -491,9 +497,16 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
(_basePanY - (details.localPosition.dy as double)) / 30)
|
||||
.clamp(1, mc.selectedCameraDetails.maxAvailableZoom);
|
||||
});
|
||||
await mc.cameraController!.setZoomLevel(
|
||||
mc.selectedCameraDetails.scaleFactor,
|
||||
);
|
||||
try {
|
||||
if (mc.cameraController != null &&
|
||||
mc.cameraController!.value.isInitialized) {
|
||||
await mc.cameraController!.setZoomLevel(
|
||||
mc.selectedCameraDetails.scaleFactor,
|
||||
);
|
||||
}
|
||||
} on CameraException catch (e) {
|
||||
Log.warn('Failed to set zoom level: $e');
|
||||
}
|
||||
if (!userService.currentUser.hasZoomed) {
|
||||
await UserService.update((u) => u.hasZoomed = true);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ 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/qr.pb.dart';
|
||||
import 'package:twonly/src/services/passwordless_recovery.service.dart'
|
||||
show PasswordlessRecoveryService;
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/qr.utils.dart';
|
||||
|
|
@ -52,6 +54,7 @@ class ScannedNewProfile {
|
|||
|
||||
class MainCameraController {
|
||||
void Function()? setState;
|
||||
void Function(Contact contact)? onVerificationSuccessDismissed;
|
||||
CameraController? cameraController;
|
||||
ScreenshotController screenshotController = ScreenshotController();
|
||||
SelectedCameraDetails selectedCameraDetails = SelectedCameraDetails();
|
||||
|
|
@ -178,7 +181,8 @@ class MainCameraController {
|
|||
|
||||
selectedCameraDetails.isZoomAble = false;
|
||||
|
||||
if (cameraController == null || !cameraController!.value.isInitialized) {
|
||||
final currentController = cameraController;
|
||||
if (currentController == null || !currentController.value.isInitialized) {
|
||||
final controllerToDispose = cameraController;
|
||||
cameraController = null;
|
||||
if (controllerToDispose != null) {
|
||||
|
|
@ -188,7 +192,7 @@ class MainCameraController {
|
|||
final hasMic = await micPermissionFuture;
|
||||
if (sessionId != _cameraSessionId) return;
|
||||
|
||||
cameraController = CameraController(
|
||||
final controller = CameraController(
|
||||
AppEnvironment.cameras[cameraId],
|
||||
ResolutionPreset.high,
|
||||
enableAudio: hasMic,
|
||||
|
|
@ -196,22 +200,60 @@ class MainCameraController {
|
|||
? ImageFormatGroup.nv21
|
||||
: ImageFormatGroup.bgra8888,
|
||||
);
|
||||
|
||||
var assignedToGlobal = false;
|
||||
try {
|
||||
_initializeFuture = cameraController?.initialize();
|
||||
_initializeFuture = controller.initialize();
|
||||
await _initializeFuture;
|
||||
await cameraController!.startImageStream(_processCameraImage);
|
||||
await cameraController!.setZoomLevel(selectedCameraDetails.scaleFactor);
|
||||
if (userService.currentUser.videoStabilizationEnabled && !kDebugMode) {
|
||||
await cameraController!.setVideoStabilizationMode(
|
||||
VideoStabilizationMode.level1,
|
||||
if (sessionId != _cameraSessionId) {
|
||||
unawaited(controller.dispose());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!controller.value.isInitialized) {
|
||||
throw CameraException(
|
||||
'Uninitialized CameraController',
|
||||
'CameraController was not initialized after initialize() completed.',
|
||||
);
|
||||
}
|
||||
|
||||
await controller.startImageStream(_processCameraImage);
|
||||
if (sessionId != _cameraSessionId) {
|
||||
unawaited(controller.dispose());
|
||||
return;
|
||||
}
|
||||
|
||||
await controller.setZoomLevel(selectedCameraDetails.scaleFactor);
|
||||
if (sessionId != _cameraSessionId) {
|
||||
unawaited(controller.dispose());
|
||||
return;
|
||||
}
|
||||
|
||||
if (userService.currentUser.videoStabilizationEnabled && !kDebugMode) {
|
||||
await controller.setVideoStabilizationMode(
|
||||
VideoStabilizationMode.level1,
|
||||
);
|
||||
if (sessionId != _cameraSessionId) {
|
||||
unawaited(controller.dispose());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
cameraController = controller;
|
||||
assignedToGlobal = true;
|
||||
} catch (e) {
|
||||
Log.error('Error initializing camera: $e');
|
||||
final controllerToDispose = cameraController;
|
||||
cameraController = null;
|
||||
if (controllerToDispose != null) {
|
||||
unawaited(controllerToDispose.dispose());
|
||||
if (e is CameraException) {
|
||||
Log.warn('Camera initialization failed (CameraException): $e');
|
||||
} else {
|
||||
Log.error('Error initializing camera: $e');
|
||||
}
|
||||
if (!assignedToGlobal) {
|
||||
unawaited(controller.dispose());
|
||||
} else {
|
||||
if (cameraController == controller) {
|
||||
cameraController = null;
|
||||
}
|
||||
unawaited(controller.dispose());
|
||||
}
|
||||
initCameraStarted = false;
|
||||
return;
|
||||
|
|
@ -219,35 +261,64 @@ class MainCameraController {
|
|||
} else {
|
||||
try {
|
||||
if (!isVideoRecording) {
|
||||
await cameraController!.stopImageStream();
|
||||
try {
|
||||
await currentController.stopImageStream();
|
||||
} catch (e) {
|
||||
Log.warn('Could not stop image stream: $e');
|
||||
}
|
||||
}
|
||||
if (sessionId != _cameraSessionId) return;
|
||||
|
||||
selectedCameraDetails.scaleFactor = 1;
|
||||
|
||||
await cameraController!.setZoomLevel(1);
|
||||
await cameraController!.setDescription(
|
||||
await currentController.setZoomLevel(1);
|
||||
if (sessionId != _cameraSessionId) return;
|
||||
|
||||
await currentController.setDescription(
|
||||
AppEnvironment.cameras[cameraId],
|
||||
);
|
||||
if (sessionId != _cameraSessionId) return;
|
||||
|
||||
if (!isVideoRecording) {
|
||||
await cameraController!.startImageStream(_processCameraImage);
|
||||
await currentController.startImageStream(_processCameraImage);
|
||||
}
|
||||
} catch (e) {
|
||||
Log.error('Error switching camera description: $e');
|
||||
if (e is CameraException) {
|
||||
Log.warn('Camera description switch failed (CameraException): $e');
|
||||
} else {
|
||||
Log.error('Error switching camera description: $e');
|
||||
}
|
||||
initCameraStarted = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (sessionId != _cameraSessionId) return;
|
||||
final controller = cameraController;
|
||||
if (controller == null) {
|
||||
initCameraStarted = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await cameraController!.lockCaptureOrientation(
|
||||
await controller.lockCaptureOrientation(
|
||||
DeviceOrientation.portraitUp,
|
||||
);
|
||||
await cameraController!.setFlashMode(
|
||||
if (sessionId != _cameraSessionId) return;
|
||||
|
||||
await controller.setFlashMode(
|
||||
selectedCameraDetails.isFlashOn ? FlashMode.always : FlashMode.off,
|
||||
);
|
||||
selectedCameraDetails.maxAvailableZoom = await cameraController!
|
||||
if (sessionId != _cameraSessionId) return;
|
||||
|
||||
selectedCameraDetails.maxAvailableZoom = await controller
|
||||
.getMaxZoomLevel();
|
||||
selectedCameraDetails.minAvailableZoom = await cameraController!
|
||||
if (sessionId != _cameraSessionId) return;
|
||||
|
||||
selectedCameraDetails.minAvailableZoom = await controller
|
||||
.getMinZoomLevel();
|
||||
if (sessionId != _cameraSessionId) return;
|
||||
|
||||
selectedCameraDetails
|
||||
..isZoomAble =
|
||||
selectedCameraDetails.maxAvailableZoom !=
|
||||
|
|
@ -263,12 +334,15 @@ class MainCameraController {
|
|||
initCameraStarted = false;
|
||||
setState?.call();
|
||||
} catch (e) {
|
||||
Log.error('Error post-initializing camera: $e');
|
||||
final controllerToDispose = cameraController;
|
||||
cameraController = null;
|
||||
if (controllerToDispose != null) {
|
||||
unawaited(controllerToDispose.dispose());
|
||||
if (e is CameraException) {
|
||||
Log.warn('Camera post-initialization failed (CameraException): $e');
|
||||
} else {
|
||||
Log.error('Error post-initializing camera: $e');
|
||||
}
|
||||
if (cameraController == controller) {
|
||||
cameraController = null;
|
||||
}
|
||||
unawaited(controller.dispose());
|
||||
initCameraStarted = false;
|
||||
return;
|
||||
}
|
||||
|
|
@ -348,8 +422,9 @@ class MainCameraController {
|
|||
}
|
||||
|
||||
InputImage? _inputImageFromCameraImage(CameraImage image) {
|
||||
if (cameraController == null) return null;
|
||||
final camera = cameraController!.description;
|
||||
final controller = cameraController;
|
||||
if (controller == null) return null;
|
||||
final camera = controller.description;
|
||||
final sensorOrientation = camera.sensorOrientation;
|
||||
|
||||
InputImageRotation? rotation;
|
||||
|
|
@ -358,7 +433,7 @@ class MainCameraController {
|
|||
rotation = InputImageRotationValue.fromRawValue(sensorOrientation);
|
||||
} else if (Platform.isAndroid) {
|
||||
var rotationCompensation =
|
||||
_orientations[cameraController!.value.deviceOrientation];
|
||||
_orientations[controller.value.deviceOrientation];
|
||||
if (rotationCompensation == null) return null;
|
||||
if (camera.lensDirection == CameraLensDirection.front) {
|
||||
// front-facing
|
||||
|
|
@ -408,14 +483,15 @@ class MainCameraController {
|
|||
if (_isBusy) return;
|
||||
_isBusy = true;
|
||||
final barcodes = await _barcodeScanner.processImage(inputImage);
|
||||
final controller = cameraController;
|
||||
if (inputImage.metadata?.size != null &&
|
||||
inputImage.metadata?.rotation != null &&
|
||||
cameraController != null) {
|
||||
controller != null) {
|
||||
final painter = BarcodeDetectorPainter(
|
||||
barcodes,
|
||||
inputImage.metadata!.size,
|
||||
inputImage.metadata!.rotation,
|
||||
cameraController!.description.lensDirection,
|
||||
controller.description.lensDirection,
|
||||
);
|
||||
qrCodePain = CustomPaint(painter: painter);
|
||||
|
||||
|
|
@ -431,6 +507,11 @@ class MainCameraController {
|
|||
if (barcode.displayValue == null) continue;
|
||||
final link = barcode.displayValue!;
|
||||
|
||||
if (link.startsWith(PasswordlessRecoveryService.linkPrefix)) {
|
||||
await PasswordlessRecoveryService.handleRecoveryLink(link);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (link.startsWith(QrCodeUtils.linkPrefix)) {
|
||||
if (_handledProfileLinks.contains(link)) continue;
|
||||
_handledProfileLinks.add(link);
|
||||
|
|
@ -476,6 +557,7 @@ class MainCameraController {
|
|||
final context = cameraPreviewKey.currentContext;
|
||||
if (verificationOk && context != null && context.mounted) {
|
||||
await VerificationSuccessDialog.show(context, contact);
|
||||
onVerificationSuccessDismissed?.call(contact);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
|
|
@ -501,9 +583,10 @@ class MainCameraController {
|
|||
if (_isBusyFaces) return;
|
||||
_isBusyFaces = true;
|
||||
final faces = await _faceDetector.processImage(inputImage);
|
||||
final controller = cameraController;
|
||||
if (inputImage.metadata?.size != null &&
|
||||
inputImage.metadata?.rotation != null &&
|
||||
cameraController != null) {
|
||||
controller != null) {
|
||||
if (faces.isNotEmpty) {
|
||||
CustomPainter? painter;
|
||||
switch (_currentFilterType) {
|
||||
|
|
@ -512,7 +595,7 @@ class MainCameraController {
|
|||
faces,
|
||||
inputImage.metadata!.size,
|
||||
inputImage.metadata!.rotation,
|
||||
cameraController!.description.lensDirection,
|
||||
controller.description.lensDirection,
|
||||
);
|
||||
case FaceFilterType.beardUpperLipGreen:
|
||||
painter = BeardFilterPainter(
|
||||
|
|
@ -520,7 +603,7 @@ class MainCameraController {
|
|||
faces,
|
||||
inputImage.metadata!.size,
|
||||
inputImage.metadata!.rotation,
|
||||
cameraController!.description.lensDirection,
|
||||
controller.description.lensDirection,
|
||||
);
|
||||
case FaceFilterType.none:
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ class PermissionHandlerViewState extends State<PermissionHandlerView>
|
|||
}
|
||||
|
||||
Future<void> _checkAndTriggerSuccess() async {
|
||||
if (!mounted) return;
|
||||
if (_isSuccessTriggered) return;
|
||||
final route = ModalRoute.of(context);
|
||||
if (route != null && !route.isCurrent) {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,23 @@
|
|||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/views/camera/camera_preview_components/camera_preview.dart';
|
||||
import 'package:twonly/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart';
|
||||
import 'package:twonly/src/visual/views/camera/camera_preview_components/main_camera_controller.dart';
|
||||
|
||||
class QrCodeScannerView extends StatefulWidget {
|
||||
const QrCodeScannerView({super.key});
|
||||
const QrCodeScannerView({
|
||||
this.contact,
|
||||
this.openToVerify = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Contact? contact;
|
||||
final bool openToVerify;
|
||||
|
||||
@override
|
||||
State<QrCodeScannerView> createState() => QrCodeScannerViewState();
|
||||
}
|
||||
|
|
@ -20,6 +31,17 @@ class QrCodeScannerViewState extends State<QrCodeScannerView> {
|
|||
_mainCameraController.setState = () {
|
||||
if (mounted) setState(() {});
|
||||
};
|
||||
if (widget.openToVerify && widget.contact != null) {
|
||||
_mainCameraController.onVerificationSuccessDismissed = (contact) {
|
||||
if (mounted) {
|
||||
Navigator.popUntil(context, (route) {
|
||||
final name = route.settings.name;
|
||||
if (name == null) return true;
|
||||
return !name.contains('qr_scanner') && !name.contains('verifybadge');
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
Permission.camera.isGranted.then((hasPermission) {
|
||||
if (hasPermission && mounted) {
|
||||
unawaited(_mainCameraController.selectCamera(0, true));
|
||||
|
|
@ -56,6 +78,31 @@ class QrCodeScannerViewState extends State<QrCodeScannerView> {
|
|||
isVisible: true,
|
||||
),
|
||||
),
|
||||
if (widget.openToVerify)
|
||||
Positioned(
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: MediaQuery.paddingOf(context).bottom + 20,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.7),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
widget.contact != null
|
||||
? context.lang.qrScannerVerifyUserHint(
|
||||
getContactDisplayName(widget.contact!),
|
||||
)
|
||||
: context.lang.qrScannerVerifyHint,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -499,7 +499,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
pixelRatio: pixelRatio,
|
||||
);
|
||||
if (image == null) {
|
||||
Log.error('screenshotController did not return image bytes');
|
||||
Log.warn('screenshotController did not return image bytes');
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -544,7 +544,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
if (image == null) return null;
|
||||
final bytes = await image.getBytes();
|
||||
if (bytes == null) {
|
||||
Log.error('imageBytes are empty');
|
||||
Log.warn('imageBytes are empty');
|
||||
return null;
|
||||
}
|
||||
if (media.type == MediaType.image || media.type == MediaType.gif) {
|
||||
|
|
@ -667,6 +667,25 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
}
|
||||
}
|
||||
|
||||
Widget _buildScreenshotViewer() {
|
||||
return Screenshot(
|
||||
controller: screenshotController,
|
||||
child: LayersViewer(
|
||||
layers: layers.where((x) => !x.isDeleted).toList(),
|
||||
onUpdate: () {
|
||||
for (final layer in layers) {
|
||||
layer.isEditing = false;
|
||||
if (layer.isDeleted) {
|
||||
removedLayers.add(layer);
|
||||
}
|
||||
}
|
||||
layers = layers.where((x) => !x.isDeleted).toList();
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
pixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||
|
|
@ -784,26 +803,27 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
width: currentImage.width / pixelRatio,
|
||||
child: Stack(
|
||||
children: [
|
||||
if (videoController != null)
|
||||
if (videoController != null &&
|
||||
videoController!.value.isInitialized)
|
||||
Positioned.fill(
|
||||
child: VideoPlayer(videoController!),
|
||||
),
|
||||
Screenshot(
|
||||
controller: screenshotController,
|
||||
child: LayersViewer(
|
||||
layers: layers.where((x) => !x.isDeleted).toList(),
|
||||
onUpdate: () {
|
||||
for (final layer in layers) {
|
||||
layer.isEditing = false;
|
||||
if (layer.isDeleted) {
|
||||
removedLayers.add(layer);
|
||||
}
|
||||
}
|
||||
layers = layers.where((x) => !x.isDeleted).toList();
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: AspectRatio(
|
||||
aspectRatio: videoController!.value.aspectRatio,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: VideoPlayer(videoController!),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: _buildScreenshotViewer(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
_buildScreenshotViewer(),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -50,7 +50,13 @@ Future<List<Sticker>> getStickerIndex() async {
|
|||
return res;
|
||||
}
|
||||
} catch (e) {
|
||||
Log.error('$e');
|
||||
if (e is http.ClientException ||
|
||||
e is SocketException ||
|
||||
e is TimeoutException) {
|
||||
Log.warn('Could not load stickers index: $e');
|
||||
} else {
|
||||
Log.error('Could not load stickers index: $e');
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import 'dart:async';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:mutex/mutex.dart';
|
||||
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/constants/routes.keys.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
|
|
@ -11,7 +11,6 @@ import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
|||
import 'package:twonly/src/database/tables/messages.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/download.api.dart';
|
||||
import 'package:twonly/src/services/profile.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';
|
||||
|
|
@ -36,13 +35,13 @@ class _UserListItem extends State<GroupListItemComp> {
|
|||
Message? _currentMessage;
|
||||
|
||||
List<Message> _messagesNotOpened = [];
|
||||
late StreamSubscription<List<Message>> _messagesNotOpenedStream;
|
||||
StreamSubscription<List<Message>>? _messagesNotOpenedStream;
|
||||
|
||||
Message? _lastMessage;
|
||||
Reaction? _lastReaction;
|
||||
late StreamSubscription<Message?> _lastMessageStream;
|
||||
late StreamSubscription<Reaction?> _lastReactionStream;
|
||||
late StreamSubscription<List<MediaFile>> _lastMediaFilesStream;
|
||||
StreamSubscription<Message?>? _lastMessageStream;
|
||||
StreamSubscription<Reaction?>? _lastReactionStream;
|
||||
StreamSubscription<List<MediaFile>>? _lastMediaFilesStream;
|
||||
|
||||
List<Message> _previewMessages = [];
|
||||
final List<MediaFile> _previewMediaFiles = [];
|
||||
|
|
@ -57,70 +56,76 @@ class _UserListItem extends State<GroupListItemComp> {
|
|||
|
||||
@override
|
||||
void dispose() {
|
||||
_messagesNotOpenedStream.cancel();
|
||||
_lastReactionStream.cancel();
|
||||
_lastMessageStream.cancel();
|
||||
_lastMediaFilesStream.cancel();
|
||||
_messagesNotOpenedStream?.cancel();
|
||||
_lastReactionStream?.cancel();
|
||||
_lastMessageStream?.cancel();
|
||||
_lastMediaFilesStream?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> initStreams() async {
|
||||
_lastMessageStream =
|
||||
(await twonlyDB.messagesDao.watchLastMessage(
|
||||
widget.group.groupId,
|
||||
)).listen((update) {
|
||||
protectUpdateState.protect(() async {
|
||||
await updateState(update, _messagesNotOpened);
|
||||
final lastMsgStream = await twonlyDB.messagesDao.watchLastMessage(
|
||||
widget.group.groupId,
|
||||
);
|
||||
if (!mounted) return;
|
||||
_lastMessageStream = lastMsgStream.listen((update) {
|
||||
_updateState(update, _messagesNotOpened);
|
||||
});
|
||||
|
||||
_lastReactionStream = twonlyDB.reactionsDao
|
||||
.watchLastReactions(widget.group.groupId)
|
||||
.listen((update) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_lastReaction = update;
|
||||
});
|
||||
});
|
||||
|
||||
_lastReactionStream = twonlyDB.reactionsDao.watchLastReactions(widget.group.groupId).listen((update) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_lastReaction = update;
|
||||
});
|
||||
});
|
||||
_messagesNotOpenedStream = twonlyDB.messagesDao
|
||||
.watchMessageNotOpened(widget.group.groupId)
|
||||
.listen((update) {
|
||||
_updateState(_lastMessage, update);
|
||||
});
|
||||
|
||||
_messagesNotOpenedStream = twonlyDB.messagesDao.watchMessageNotOpened(widget.group.groupId).listen((update) {
|
||||
protectUpdateState.protect(() async {
|
||||
await updateState(_lastMessage, update);
|
||||
});
|
||||
});
|
||||
|
||||
_lastMediaFilesStream = twonlyDB.mediaFilesDao.watchNewestMediaFiles().listen((mediaFiles) {
|
||||
if (!mounted) return;
|
||||
for (final mediaFile in mediaFiles) {
|
||||
final index = _previewMediaFiles.indexWhere(
|
||||
(t) => t.mediaId == mediaFile.mediaId,
|
||||
);
|
||||
if (index >= 0) {
|
||||
_previewMediaFiles[index] = mediaFile;
|
||||
}
|
||||
}
|
||||
setState(() {});
|
||||
});
|
||||
_lastMediaFilesStream = twonlyDB.mediaFilesDao
|
||||
.watchMediaFilesForGroup(widget.group.groupId)
|
||||
.listen((mediaFiles) {
|
||||
if (!mounted) return;
|
||||
for (final mediaFile in mediaFiles) {
|
||||
final index = _previewMediaFiles.indexWhere(
|
||||
(t) => t.mediaId == mediaFile.mediaId,
|
||||
);
|
||||
if (index >= 0) {
|
||||
_previewMediaFiles[index] = mediaFile;
|
||||
}
|
||||
}
|
||||
setState(() {});
|
||||
});
|
||||
|
||||
final groupContacts = await twonlyDB.groupsDao.getGroupContact(
|
||||
widget.group.groupId,
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (groupContacts.length == 1) {
|
||||
_receiverDeletedAccount = groupContacts.first.accountDeleted;
|
||||
}
|
||||
}
|
||||
|
||||
Mutex protectUpdateState = Mutex();
|
||||
|
||||
Future<void> updateState(
|
||||
void _updateState(
|
||||
Message? newLastMessage,
|
||||
List<Message> newMessagesNotOpened,
|
||||
) async {
|
||||
) {
|
||||
if (!mounted) return;
|
||||
if (newLastMessage == null) {
|
||||
// there are no messages at all
|
||||
_currentMessage = null;
|
||||
_previewMessages = [];
|
||||
} else if (newMessagesNotOpened.isNotEmpty) {
|
||||
// Filter for the preview non opened messages. First messages which where send but not yet opened by the other side.
|
||||
final receivedMessages = newMessagesNotOpened.where((x) => x.senderId != null).toList();
|
||||
// Filter for the preview non opened messages. First messages which where
|
||||
// send but not yet opened by the other side.
|
||||
final receivedMessages = newMessagesNotOpened
|
||||
.where((x) => x.senderId != null)
|
||||
.toList();
|
||||
|
||||
if (receivedMessages.isNotEmpty) {
|
||||
_previewMessages = receivedMessages;
|
||||
|
|
@ -131,8 +136,9 @@ class _UserListItem extends State<GroupListItemComp> {
|
|||
}
|
||||
} else {
|
||||
// there are no not opened messages show just the last message in the table
|
||||
// only shows the last message in case there was no newer messages which already got deleted
|
||||
// This prevents, that it will show that a images got stored 10 days ago...
|
||||
// only shows the last message in case there was no newer messages which
|
||||
// already got deleted. This prevents showing that an image got stored 10
|
||||
// days ago...
|
||||
if (newLastMessage.createdAt.isAfter(
|
||||
widget.group.lastMessageExchange.subtract(const Duration(days: 2)),
|
||||
)) {
|
||||
|
|
@ -144,7 +150,9 @@ class _UserListItem extends State<GroupListItemComp> {
|
|||
}
|
||||
}
|
||||
|
||||
final msgs = _previewMessages.where((x) => x.type == MessageType.media.name).toList();
|
||||
final msgs = _previewMessages
|
||||
.where((x) => x.type == MessageType.media.name)
|
||||
.toList();
|
||||
if (msgs.isNotEmpty &&
|
||||
msgs.first.type == MessageType.media.name &&
|
||||
!msgs.first.isDeletedFromSender &&
|
||||
|
|
@ -155,20 +163,30 @@ class _UserListItem extends State<GroupListItemComp> {
|
|||
_hasNonOpenedMediaFile = false;
|
||||
}
|
||||
|
||||
_lastMessage = newLastMessage;
|
||||
_messagesNotOpened = newMessagesNotOpened;
|
||||
setState(() {});
|
||||
|
||||
// Only fetch on first load when a mediaId is not yet cached.
|
||||
_fetchMissingMediaFiles();
|
||||
}
|
||||
|
||||
/// Fetches any media files referenced by preview messages but not yet in the
|
||||
/// local cache. Fire-and-forget; updates state when results arrive.
|
||||
Future<void> _fetchMissingMediaFiles() async {
|
||||
for (final message in _previewMessages) {
|
||||
if (message.mediaId != null && !_previewMediaFiles.any((t) => t.mediaId == message.mediaId)) {
|
||||
if (message.mediaId != null &&
|
||||
!_previewMediaFiles.any((t) => t.mediaId == message.mediaId)) {
|
||||
final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(
|
||||
message.mediaId!,
|
||||
);
|
||||
if (mediaFile != null) {
|
||||
_previewMediaFiles.add(mediaFile);
|
||||
if (mediaFile != null && mounted) {
|
||||
setState(() {
|
||||
_previewMediaFiles.add(mediaFile);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_lastMessage = newLastMessage;
|
||||
_messagesNotOpened = newMessagesNotOpened;
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
Future<void> onTap() async {
|
||||
|
|
@ -181,7 +199,9 @@ class _UserListItem extends State<GroupListItemComp> {
|
|||
}
|
||||
|
||||
if (_hasNonOpenedMediaFile) {
|
||||
final msgs = _previewMessages.where((x) => x.type == MessageType.media.name).toList();
|
||||
final msgs = _previewMessages
|
||||
.where((x) => x.type == MessageType.media.name)
|
||||
.toList();
|
||||
final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(
|
||||
msgs.first.mediaId!,
|
||||
);
|
||||
|
|
@ -221,7 +241,7 @@ class _UserListItem extends State<GroupListItemComp> {
|
|||
const SizedBox(width: 3),
|
||||
VerificationBadgeComp(
|
||||
group: widget.group,
|
||||
showOnlyIfVerified: userService.currentUser.securityProfile.showOnlyVerifiedInChatViewList,
|
||||
showOnlyIfVerified: true,
|
||||
clickable: false,
|
||||
size: 12,
|
||||
),
|
||||
|
|
@ -235,6 +255,7 @@ class _UserListItem extends State<GroupListItemComp> {
|
|||
: Row(
|
||||
children: [
|
||||
LastMessageTimeComp(
|
||||
key: ValueKey(widget.group.groupId),
|
||||
dateTime: widget.group.lastMessageExchange,
|
||||
),
|
||||
FlameCounterWidget(
|
||||
|
|
@ -256,7 +277,11 @@ class _UserListItem extends State<GroupListItemComp> {
|
|||
),
|
||||
const Text('•'),
|
||||
const SizedBox(width: 5),
|
||||
if (_currentMessage != null) LastMessageTimeComp(message: _currentMessage),
|
||||
if (_currentMessage != null)
|
||||
LastMessageTimeComp(
|
||||
key: ValueKey(widget.group.groupId),
|
||||
message: _currentMessage,
|
||||
),
|
||||
FlameCounterWidget(
|
||||
groupId: widget.group.groupId,
|
||||
prefix: true,
|
||||
|
|
@ -270,7 +295,9 @@ class _UserListItem extends State<GroupListItemComp> {
|
|||
widget.group.groupId,
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
await context.push(Routes.profileContact(contacts.first.userId));
|
||||
await context.push(
|
||||
Routes.profileContact(contacts.first.userId),
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
await context.push(Routes.profileGroup(widget.group.groupId));
|
||||
|
|
@ -283,7 +310,9 @@ class _UserListItem extends State<GroupListItemComp> {
|
|||
: IconButton(
|
||||
onPressed: () {
|
||||
if (_hasNonOpenedMediaFile) {
|
||||
context.push(Routes.chatsMessages(widget.group.groupId));
|
||||
context.push(
|
||||
Routes.chatsMessages(widget.group.groupId),
|
||||
);
|
||||
} else {
|
||||
context.push(
|
||||
Routes.chatsCameraSendTo,
|
||||
|
|
@ -292,7 +321,9 @@ class _UserListItem extends State<GroupListItemComp> {
|
|||
}
|
||||
},
|
||||
icon: FaIcon(
|
||||
_hasNonOpenedMediaFile ? FontAwesomeIcons.solidComments : FontAwesomeIcons.camera,
|
||||
_hasNonOpenedMediaFile
|
||||
? FontAwesomeIcons.solidComments
|
||||
: FontAwesomeIcons.camera,
|
||||
color: context.color.outline.withAlpha(150),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -19,41 +19,59 @@ class LastMessageTimeComp extends StatefulWidget {
|
|||
class _LastMessageTimeCompState extends State<LastMessageTimeComp> {
|
||||
Timer? updateTime;
|
||||
int lastMessageInSeconds = 0;
|
||||
DateTime? targetTime;
|
||||
StreamSubscription<MessageAction?>? _actionSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Change the color every 200 milliseconds
|
||||
updateTime = Timer.periodic(const Duration(milliseconds: 500), (
|
||||
timer,
|
||||
) async {
|
||||
if (widget.message != null) {
|
||||
final lastAction = await twonlyDB.messagesDao.getLastMessageAction(
|
||||
widget.message!.messageId,
|
||||
);
|
||||
lastMessageInSeconds = clock
|
||||
.now()
|
||||
.difference(lastAction?.actionAt ?? widget.message!.createdAt)
|
||||
.inSeconds;
|
||||
} else if (widget.dateTime != null) {
|
||||
lastMessageInSeconds = clock
|
||||
.now()
|
||||
.difference(widget.dateTime!)
|
||||
.inSeconds;
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
if (lastMessageInSeconds < 0) {
|
||||
lastMessageInSeconds = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
_loadTargetTime();
|
||||
|
||||
updateTime = Timer.periodic(
|
||||
const Duration(milliseconds: 500),
|
||||
(_) => _updateSeconds(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(LastMessageTimeComp oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.message?.messageId != widget.message?.messageId ||
|
||||
oldWidget.dateTime != widget.dateTime) {
|
||||
_loadTargetTime();
|
||||
}
|
||||
}
|
||||
|
||||
void _loadTargetTime() {
|
||||
_actionSubscription?.cancel();
|
||||
_actionSubscription = null;
|
||||
|
||||
if (widget.message != null) {
|
||||
_actionSubscription = twonlyDB.messagesDao
|
||||
.watchLastMessageAction(widget.message!.messageId)
|
||||
.listen((lastAction) {
|
||||
targetTime = lastAction?.actionAt ?? widget.message!.createdAt;
|
||||
_updateSeconds();
|
||||
});
|
||||
} else if (widget.dateTime != null) {
|
||||
targetTime = widget.dateTime;
|
||||
_updateSeconds();
|
||||
}
|
||||
}
|
||||
|
||||
void _updateSeconds() {
|
||||
if (targetTime == null || !mounted) return;
|
||||
|
||||
final seconds = clock.now().difference(targetTime!).inSeconds;
|
||||
setState(() {
|
||||
lastMessageInSeconds = seconds < 0 ? 0 : seconds;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
updateTime?.cancel();
|
||||
_actionSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,9 +20,9 @@ class _TypingIndicatorSubtitleCompState
|
|||
extends State<TypingIndicatorSubtitleComp> {
|
||||
List<GroupMember> _groupMembers = [];
|
||||
|
||||
late StreamSubscription<List<(Contact, GroupMember)>> membersSub;
|
||||
StreamSubscription<List<(Contact, GroupMember)>>? membersSub;
|
||||
|
||||
late Timer _periodicUpdate;
|
||||
Timer? _periodicUpdate;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -50,8 +50,8 @@ class _TypingIndicatorSubtitleCompState
|
|||
|
||||
@override
|
||||
void dispose() {
|
||||
membersSub.cancel();
|
||||
_periodicUpdate.cancel();
|
||||
membersSub?.cancel();
|
||||
_periodicUpdate?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import 'dart:collection';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:mutex/mutex.dart';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
|
|
@ -25,6 +24,7 @@ import 'package:twonly/src/visual/views/chats/chat_messages_components/blink.com
|
|||
import 'package:twonly/src/visual/views/chats/chat_messages_components/chat_group_action.dart';
|
||||
import 'package:twonly/src/visual/views/chats/chat_messages_components/chat_list_entry.dart';
|
||||
import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/chat_date_chip.dart';
|
||||
import 'package:twonly/src/visual/views/chats/chat_messages_components/in_chat_group_overview.dart';
|
||||
import 'package:twonly/src/visual/views/chats/chat_messages_components/message_input.dart';
|
||||
import 'package:twonly/src/visual/views/chats/chat_messages_components/response_container.dart';
|
||||
import 'package:twonly/src/visual/views/chats/chat_messages_components/typing_indicator.dart';
|
||||
|
|
@ -87,18 +87,8 @@ class _ChatMessagesViewState extends State<ChatMessagesView>
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
Mutex protectMessageUpdating = Mutex();
|
||||
|
||||
// @override
|
||||
// void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
// if (state == AppLifecycleState.resumed) {
|
||||
// protectMessageUpdating.protect(() async {
|
||||
// await setMessages(allMessages, groupActions);
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
bool _isViewActive() {
|
||||
if (!mounted) return false;
|
||||
return !AppState.isAppInBackground &&
|
||||
(ModalRoute.of(context)?.isCurrent ?? false);
|
||||
}
|
||||
|
|
@ -112,33 +102,29 @@ class _ChatMessagesViewState extends State<ChatMessagesView>
|
|||
_group = newGroup;
|
||||
});
|
||||
|
||||
protectMessageUpdating.protect(() async {
|
||||
if (groupActionsSub == null) {
|
||||
final actionsStream = twonlyDB.groupsDao.watchGroupActions(
|
||||
newGroup.groupId,
|
||||
);
|
||||
groupActionsSub = actionsStream.listen((update) async {
|
||||
groupActions = update;
|
||||
await setMessages(allMessages, update);
|
||||
});
|
||||
if (groupActionsSub == null) {
|
||||
final actionsStream = twonlyDB.groupsDao.watchGroupActions(
|
||||
newGroup.groupId,
|
||||
);
|
||||
groupActionsSub = actionsStream.listen((update) async {
|
||||
groupActions = update;
|
||||
await setMessages(allMessages, update);
|
||||
});
|
||||
|
||||
final contactsStream = twonlyDB.contactsDao.watchAllContacts();
|
||||
contactSub = contactsStream.listen((contacts) {
|
||||
for (final contact in contacts) {
|
||||
userIdToContact[contact.userId] = contact;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
final contactsStream = twonlyDB.contactsDao.watchAllContacts();
|
||||
contactSub = contactsStream.listen((contacts) {
|
||||
for (final contact in contacts) {
|
||||
userIdToContact[contact.userId] = contact;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
final msgStream = await twonlyDB.messagesDao.watchByGroupId(widget.groupId);
|
||||
messageSub = msgStream.listen((update) async {
|
||||
allMessages = update;
|
||||
await protectMessageUpdating.protect(() async {
|
||||
await setMessages(update, groupActions);
|
||||
_hasReceivedFirstMessageBatch = true;
|
||||
});
|
||||
await setMessages(update, groupActions);
|
||||
_hasReceivedFirstMessageBatch = true;
|
||||
});
|
||||
|
||||
final groupContacts = await twonlyDB.groupsDao.getGroupContact(
|
||||
|
|
@ -165,7 +151,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView>
|
|||
List<GroupHistory> groupActions,
|
||||
) async {
|
||||
if (_isViewActive()) {
|
||||
await flutterLocalNotificationsPlugin.cancelAll();
|
||||
unawaited(flutterLocalNotificationsPlugin.cancelAll());
|
||||
}
|
||||
|
||||
for (final msg in newMessages) {
|
||||
|
|
@ -229,18 +215,34 @@ class _ChatMessagesViewState extends State<ChatMessagesView>
|
|||
|
||||
if (_isViewActive()) {
|
||||
for (final contactId in openedMessages.keys) {
|
||||
await notifyContactAboutOpeningMessage(
|
||||
contactId,
|
||||
openedMessages[contactId]!,
|
||||
unawaited(
|
||||
notifyContactAboutOpeningMessage(
|
||||
contactId,
|
||||
openedMessages[contactId]!,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final wasSentByMe =
|
||||
_hasReceivedFirstMessageBatch &&
|
||||
newMessages.isNotEmpty &&
|
||||
newMessages.last.senderId == null;
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
messages = chatItems.reversed.toList();
|
||||
});
|
||||
|
||||
if (wasSentByMe && itemScrollController.isAttached) {
|
||||
unawaited(
|
||||
itemScrollController.scrollTo(
|
||||
index: 0,
|
||||
duration: const Duration(milliseconds: 150),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final items = await MemoryItem.convertFromMessages(storedMediaFiles);
|
||||
if (!mounted) return;
|
||||
galleryItems = items.values.toList();
|
||||
|
|
@ -325,64 +327,72 @@ class _ChatMessagesViewState extends State<ChatMessagesView>
|
|||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ScrollablePositionedList.builder(
|
||||
reverse: true,
|
||||
itemCount: messages.length + 1 + 1,
|
||||
itemScrollController: itemScrollController,
|
||||
itemBuilder: (context, i) {
|
||||
if (i == 0) {
|
||||
return userService.currentUser.typingIndicators
|
||||
? TypingIndicator(group: group)
|
||||
: Container();
|
||||
}
|
||||
i -= 1;
|
||||
if (i == messages.length) {
|
||||
return const Padding(
|
||||
padding: EdgeInsetsGeometry.only(top: 10),
|
||||
);
|
||||
}
|
||||
if (messages[i].isDate) {
|
||||
return ChatDateChip(
|
||||
item: messages[i],
|
||||
);
|
||||
} else if (messages[i].isGroupAction) {
|
||||
return ChatGroupAction(
|
||||
key: Key(messages[i].groupAction!.groupHistoryId),
|
||||
action: messages[i].groupAction!,
|
||||
);
|
||||
} else {
|
||||
final chatMessage = messages[i].message!;
|
||||
return BlinkWidget(
|
||||
key: Key('blink_${chatMessage.messageId}'),
|
||||
enabled: focusedScrollItem == i,
|
||||
child: AnimatedNewMessage(
|
||||
key: Key('anim_${chatMessage.messageId}'),
|
||||
messageId: chatMessage.messageId,
|
||||
animateIds: _animateMessageIds,
|
||||
child: ChatListEntry(
|
||||
key: Key(chatMessage.messageId),
|
||||
message: messages[i].message!,
|
||||
nextMessage: (i > 0)
|
||||
? messages[i - 1].message
|
||||
: null,
|
||||
prevMessage: ((i + 1) < messages.length)
|
||||
? messages[i + 1].message
|
||||
: null,
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: ScrollablePositionedList.builder(
|
||||
shrinkWrap: true,
|
||||
reverse: true,
|
||||
itemCount: messages.length + 1 + 1,
|
||||
itemScrollController: itemScrollController,
|
||||
itemBuilder: (context, i) {
|
||||
if (i == 0) {
|
||||
return userService.currentUser.typingIndicators
|
||||
? TypingIndicator(group: group)
|
||||
: Container();
|
||||
}
|
||||
i -= 1;
|
||||
if (i == messages.length) {
|
||||
return Padding(
|
||||
key: Key('overview_${group.groupId}'),
|
||||
padding: const EdgeInsets.only(top: 10),
|
||||
child: InChatGroupOverview(
|
||||
group: group,
|
||||
galleryItems: galleryItems,
|
||||
userIdToContact: userIdToContact,
|
||||
scrollToMessage: scrollToMessage,
|
||||
onResponseTriggered: () {
|
||||
setState(() {
|
||||
quotesMessage = chatMessage;
|
||||
});
|
||||
textFieldFocus?.requestFocus();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
if (messages[i].isDate) {
|
||||
return ChatDateChip(
|
||||
item: messages[i],
|
||||
);
|
||||
} else if (messages[i].isGroupAction) {
|
||||
return ChatGroupAction(
|
||||
key: Key(messages[i].groupAction!.groupHistoryId),
|
||||
action: messages[i].groupAction!,
|
||||
);
|
||||
} else {
|
||||
final chatMessage = messages[i].message!;
|
||||
return BlinkWidget(
|
||||
key: Key('blink_${chatMessage.messageId}'),
|
||||
enabled: focusedScrollItem == i,
|
||||
child: AnimatedNewMessage(
|
||||
key: Key('anim_${chatMessage.messageId}'),
|
||||
messageId: chatMessage.messageId,
|
||||
animateIds: _animateMessageIds,
|
||||
child: ChatListEntry(
|
||||
key: Key(chatMessage.messageId),
|
||||
message: messages[i].message!,
|
||||
nextMessage: (i > 0)
|
||||
? messages[i - 1].message
|
||||
: null,
|
||||
prevMessage: ((i + 1) < messages.length)
|
||||
? messages[i + 1].message
|
||||
: null,
|
||||
group: group,
|
||||
galleryItems: galleryItems,
|
||||
userIdToContact: userIdToContact,
|
||||
scrollToMessage: scrollToMessage,
|
||||
onResponseTriggered: () {
|
||||
setState(() {
|
||||
quotesMessage = chatMessage;
|
||||
});
|
||||
textFieldFocus?.requestFocus();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
if (quotesMessage != null)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,221 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/constants/routes.keys.dart';
|
||||
import 'package:twonly/src/database/daos/key_verification.dao.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
||||
import 'package:twonly/src/visual/components/verification_badge.comp.dart';
|
||||
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||
|
||||
class InChatGroupOverview extends StatefulWidget {
|
||||
const InChatGroupOverview({
|
||||
required this.group,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Group group;
|
||||
|
||||
@override
|
||||
State<InChatGroupOverview> createState() => _InChatGroupOverviewState();
|
||||
}
|
||||
|
||||
class _InChatGroupOverviewState extends State<InChatGroupOverview>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
Contact? _directContact;
|
||||
StreamSubscription<dynamic>? _verificationSub;
|
||||
StreamSubscription<List<(Contact, DateTime)>>? _transferredTrustSub;
|
||||
StreamSubscription<int>? _unverifiedCountSub;
|
||||
bool _isVerified = true;
|
||||
int _unverifiedCount = 0;
|
||||
List<(KeyVerification, Contact?)> _verifications = [];
|
||||
List<(Contact, DateTime)> _transferredTrust = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initVerificationCheck();
|
||||
}
|
||||
|
||||
void _updateVerificationState() {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isVerified =
|
||||
_directContact?.verified == true ||
|
||||
_verifications.isNotEmpty ||
|
||||
_transferredTrust.isNotEmpty;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _initVerificationCheck() async {
|
||||
if (widget.group.isDirectChat) {
|
||||
final contacts = await twonlyDB.groupsDao.getGroupContact(
|
||||
widget.group.groupId,
|
||||
);
|
||||
if (contacts.isNotEmpty) {
|
||||
_directContact = contacts.first;
|
||||
_verificationSub = twonlyDB.keyVerificationDao
|
||||
.watchContactVerification(_directContact!.userId)
|
||||
.listen((verifications) {
|
||||
_verifications = verifications;
|
||||
_updateVerificationState();
|
||||
});
|
||||
_transferredTrustSub = twonlyDB.keyVerificationDao
|
||||
.watchTransferredTrustVerifications(_directContact!.userId)
|
||||
.listen((transferredTrust) {
|
||||
_transferredTrust = transferredTrust;
|
||||
_updateVerificationState();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
_verificationSub = twonlyDB.keyVerificationDao
|
||||
.watchAllGroupMembersVerified(widget.group.groupId)
|
||||
.listen((status) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isVerified = status == VerificationStatus.trusted;
|
||||
});
|
||||
}
|
||||
});
|
||||
_unverifiedCountSub = twonlyDB.keyVerificationDao
|
||||
.watchUnverifiedGroupMembersCount(widget.group.groupId)
|
||||
.listen((count) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_unverifiedCount = count;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_verificationSub?.cancel();
|
||||
_transferredTrustSub?.cancel();
|
||||
_unverifiedCountSub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onVerifyPressed() {
|
||||
if (widget.group.isDirectChat && _directContact != null) {
|
||||
context.push(
|
||||
Routes.settingsHelpFaqVerifyBadge,
|
||||
extra: _directContact,
|
||||
);
|
||||
} else {
|
||||
context.push(Routes.profileGroup(widget.group.groupId));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14),
|
||||
child: Center(
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
alignment: Alignment.topCenter,
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 40),
|
||||
constraints: const BoxConstraints(maxWidth: 250),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outlineVariant,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
padding: const EdgeInsets.only(
|
||||
top: 56,
|
||||
left: 14,
|
||||
right: 14,
|
||||
bottom: 14,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
widget.group.groupName,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
VerificationBadgeComp(
|
||||
group: widget.group,
|
||||
size: 20,
|
||||
clickable: false,
|
||||
),
|
||||
],
|
||||
),
|
||||
if (!_isVerified) ...[
|
||||
const SizedBox(height: 5),
|
||||
Card(
|
||||
elevation: 0,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.error.withValues(alpha: 0.1),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 14,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
widget.group.isDirectChat
|
||||
? context.lang.inChatContactNotVerified
|
||||
: context.lang.groupMembersNotVerified(
|
||||
_unverifiedCount,
|
||||
),
|
||||
style: TextStyle(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onErrorContainer,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
MyButton(
|
||||
variant: MyButtonVariant.secondaryDense,
|
||||
onPressed: _onVerifyPressed,
|
||||
child: Text(context.lang.unverifiedWarningButton),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
child: SizedBox(
|
||||
width: 80,
|
||||
height: 80,
|
||||
child: AvatarIcon(group: widget.group, fontSize: 40),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -99,6 +99,7 @@ class MessageContextMenu extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final navigator = Navigator.of(context);
|
||||
return ContextMenu(
|
||||
items: [
|
||||
if (!message.isDeletedFromSender)
|
||||
|
|
@ -107,7 +108,7 @@ class MessageContextMenu extends StatelessWidget {
|
|||
onTap: () async {
|
||||
final layer =
|
||||
await showModalBottomSheet(
|
||||
context: context,
|
||||
context: navigator.context,
|
||||
backgroundColor: Colors.black,
|
||||
builder: (context) {
|
||||
return const EmojiPickerBottom();
|
||||
|
|
@ -138,7 +139,7 @@ class MessageContextMenu extends StatelessWidget {
|
|||
if (mediaFileService?.canBeOpenedAgain ?? false)
|
||||
ContextMenuItem(
|
||||
title: context.lang.contextMenuViewAgain,
|
||||
onTap: () => reopenMediaFile(context),
|
||||
onTap: () => reopenMediaFile(navigator.context),
|
||||
icon: FontAwesomeIcons.clockRotateLeft,
|
||||
),
|
||||
if (!message.isDeletedFromSender)
|
||||
|
|
@ -155,7 +156,7 @@ class MessageContextMenu extends StatelessWidget {
|
|||
ContextMenuItem(
|
||||
title: context.lang.edit,
|
||||
onTap: () async {
|
||||
await editTextMessage(context, message);
|
||||
await editTextMessage(navigator.context, message);
|
||||
},
|
||||
icon: FontAwesomeIcons.pencil,
|
||||
),
|
||||
|
|
@ -172,13 +173,13 @@ class MessageContextMenu extends StatelessWidget {
|
|||
title: context.lang.delete,
|
||||
onTap: () async {
|
||||
final delete = await showAlertDialog(
|
||||
context,
|
||||
context.lang.deleteTitle,
|
||||
navigator.context,
|
||||
navigator.context.lang.deleteTitle,
|
||||
null,
|
||||
customOk:
|
||||
(message.senderId == null && !message.isDeletedFromSender)
|
||||
? context.lang.deleteOkBtnForAll
|
||||
: context.lang.deleteOkBtnForMe,
|
||||
? navigator.context.lang.deleteOkBtnForAll
|
||||
: navigator.context.lang.deleteOkBtnForMe,
|
||||
);
|
||||
if (delete) {
|
||||
if (message.senderId == null && !message.isDeletedFromSender) {
|
||||
|
|
@ -209,8 +210,7 @@ class MessageContextMenu extends StatelessWidget {
|
|||
ContextMenuItem(
|
||||
title: context.lang.info,
|
||||
onTap: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
await navigator.push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return MessageInfoView(
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import 'package:twonly/src/visual/views/chats/chat_messages_components/bottom_sh
|
|||
import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/chat_audio_entry.dart';
|
||||
import 'package:twonly/src/visual/views/chats/chat_messages_components/message_input_components/ask_for_friend_promotions.comp.dart';
|
||||
import 'package:twonly/src/visual/views/chats/chat_messages_components/message_input_components/sparks.comp.dart';
|
||||
import 'package:twonly/src/visual/views/chats/chat_messages_components/message_input_components/unverified_contact_warning.comp.dart';
|
||||
import 'package:twonly/src/visual/views/chats/chat_messages_components/message_input_components/user_discovery_manual_approval.comp.dart';
|
||||
import 'package:twonly/src/visual/views/contact/contact_components/restore_flame.comp.dart';
|
||||
|
||||
|
|
@ -283,11 +282,9 @@ class _MessageInputState extends State<MessageInput> {
|
|||
flameOnRightSide: true,
|
||||
),
|
||||
),
|
||||
UnverifiedContactWarningComp(
|
||||
group: widget.group,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 10, bottom: 10),
|
||||
child: Row(
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 10, bottom: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
|
|
@ -589,7 +586,6 @@ class _MessageInputState extends State<MessageInput> {
|
|||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Offstage(
|
||||
offstage: !_emojiShowing,
|
||||
child: EmojiPicker(
|
||||
|
|
|
|||
|
|
@ -1,142 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/constants/routes.keys.dart';
|
||||
import 'package:twonly/src/database/daos/key_verification.dao.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/services/profile.service.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/components/verification_badge.comp.dart';
|
||||
|
||||
class UnverifiedContactWarningComp extends StatelessWidget {
|
||||
const UnverifiedContactWarningComp({
|
||||
required this.group,
|
||||
required this.child,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Group group;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StreamBuilder<void>(
|
||||
stream: userService.onUserUpdated,
|
||||
builder: (context, _) {
|
||||
if (!userService
|
||||
.currentUser
|
||||
.securityProfile
|
||||
.showWarningForNonVerifiedContacts) {
|
||||
return child;
|
||||
}
|
||||
return StreamBuilder<VerificationStatus>(
|
||||
stream: twonlyDB.keyVerificationDao.watchAllGroupMembersVerified(
|
||||
group.groupId,
|
||||
),
|
||||
builder: (context, snapshot) {
|
||||
final status = snapshot.data;
|
||||
if (status == null || status == VerificationStatus.trusted) {
|
||||
return child;
|
||||
}
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: context.color.errorContainer.withValues(alpha: 0.5),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(
|
||||
color: context.color.error.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 10, 12, 8),
|
||||
child: Row(
|
||||
children: [
|
||||
VerificationBadgeComp(
|
||||
group: group,
|
||||
size: 24,
|
||||
clickable: false,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
group.isDirectChat
|
||||
? context.lang.unverifiedWarningDirectTitle
|
||||
: context.lang.unverifiedWarningGroupTitle,
|
||||
style: TextStyle(
|
||||
color: context.color.onErrorContainer,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 13,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
style: TextStyle(
|
||||
color: context.color.onErrorContainer,
|
||||
fontSize: 11,
|
||||
),
|
||||
children: formattedText(
|
||||
context,
|
||||
context.lang.unverifiedWarningBody,
|
||||
textColor: context.color.onErrorContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
SizedBox(
|
||||
height: 30,
|
||||
child: FilledButton.tonal(
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: context.color.onErrorContainer,
|
||||
foregroundColor: context.color.errorContainer,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
),
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
if (group.isDirectChat) {
|
||||
await context.push(
|
||||
Routes.settingsHelpFaqVerifyBadge,
|
||||
);
|
||||
} else {
|
||||
await context.push(
|
||||
Routes.profileGroup(group.groupId),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Text(context.lang.unverifiedWarningButton),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 5,
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -137,6 +137,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
final Mutex _messageUpdateLock = Mutex();
|
||||
|
||||
bool _isViewActive() {
|
||||
if (!mounted) return false;
|
||||
return !AppState.isAppInBackground &&
|
||||
(ModalRoute.of(context)?.isCurrent ?? false);
|
||||
}
|
||||
|
|
@ -358,12 +359,17 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
if (!mounted) return;
|
||||
|
||||
if (!currentMediaLocal.tempPath.existsSync()) {
|
||||
Log.error('Temp media file not found...');
|
||||
Log.warn(
|
||||
'Temp media file not found for media ID: ${currentMediaLocal.mediaFile.mediaId}',
|
||||
);
|
||||
await handleMediaError(currentMediaLocal.mediaFile);
|
||||
return advanceToNextMediaOrExit();
|
||||
}
|
||||
|
||||
// The server can now delete the encrypted bytes, as the users has sucessfully opened it.
|
||||
Log.info(
|
||||
'Calling downloadDone for media ID: ${currentMediaLocal.mediaFile.mediaId}',
|
||||
);
|
||||
unawaited(
|
||||
apiService.downloadDone(currentMediaLocal.mediaFile.downloadToken!),
|
||||
);
|
||||
|
|
@ -468,7 +474,11 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
..play();
|
||||
})
|
||||
.catchError((Object err, StackTrace st) {
|
||||
Log.error('Video player initialization error', err, st);
|
||||
Log.error(
|
||||
'Video player initialization error',
|
||||
error: err,
|
||||
stackTrace: st,
|
||||
);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
|
@ -511,12 +521,16 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
}
|
||||
|
||||
Future<void> onPressedSaveToGallery() async {
|
||||
final media = currentMedia;
|
||||
final msg = currentMessage;
|
||||
if (media == null || msg == null) return;
|
||||
|
||||
setState(() {
|
||||
imageSaving = true;
|
||||
});
|
||||
await currentMedia!.storeMediaFile();
|
||||
await media.storeMediaFile();
|
||||
await twonlyDB.messagesDao.updateMessageId(
|
||||
currentMessage!.messageId,
|
||||
msg.messageId,
|
||||
const MessagesCompanion(
|
||||
mediaStored: Value(true),
|
||||
),
|
||||
|
|
@ -526,7 +540,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
pb.EncryptedContent(
|
||||
mediaUpdate: pb.EncryptedContent_MediaUpdate(
|
||||
type: pb.EncryptedContent_MediaUpdate_Type.STORED,
|
||||
targetMessageId: currentMessage!.messageId,
|
||||
targetMessageId: msg.messageId,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -553,11 +567,14 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (currentMedia != null &&
|
||||
currentMessage != null &&
|
||||
!currentMedia!.mediaFile.requiresAuthentication &&
|
||||
currentMedia!.mediaFile.displayLimitInMilliseconds == null)
|
||||
MyIconButton(
|
||||
variant: MyIconButtonVariant.secondary,
|
||||
onPressed: (currentMedia == null) ? null : onPressedSaveToGallery,
|
||||
onPressed: (currentMedia == null || currentMessage == null)
|
||||
? null
|
||||
: onPressedSaveToGallery,
|
||||
icon: imageSaving
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
|
|
|
|||
|
|
@ -21,16 +21,14 @@ class MediaContentRenderer extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
if (videoController != null)
|
||||
if (videoController != null && videoController!.value.isInitialized)
|
||||
Positioned.fill(
|
||||
child: PhotoView.customChild(
|
||||
initialScale: PhotoViewComputedScale.contained,
|
||||
minScale: PhotoViewComputedScale.contained,
|
||||
backgroundDecoration: const BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
),
|
||||
child: VideoPlayer(
|
||||
videoController!,
|
||||
child: Center(
|
||||
child: AspectRatio(
|
||||
aspectRatio: videoController!.value.aspectRatio,
|
||||
child: VideoPlayer(
|
||||
videoController!,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue