diff --git a/lib/contact_invitation/models/valid_contact_invitation.dart b/lib/contact_invitation/models/valid_contact_invitation.dart index 2a3d140..639af7f 100644 --- a/lib/contact_invitation/models/valid_contact_invitation.dart +++ b/lib/contact_invitation/models/valid_contact_invitation.dart @@ -73,13 +73,9 @@ class ValidContactInvitation { ..identitySignature = identitySignature.toProto(); // Write the acceptance to the inbox - if (await contactRequestInbox.tryWriteProtobuf( - proto.SignedContactResponse.fromBuffer, - signedContactResponse, - subkey: 1) != - null) { - throw Exception('failed to accept contact invitation'); - } + await contactRequestInbox + .eventualWriteProtobuf(signedContactResponse, subkey: 1); + return AcceptedContact( remoteProfile: _contactRequestPrivate.profile, remoteIdentity: _contactIdentityMaster, @@ -129,13 +125,8 @@ class ValidContactInvitation { ..identitySignature = identitySignature.toProto(); // Write the rejection to the inbox - if (await contactRequestInbox.tryWriteProtobuf( - proto.SignedContactResponse.fromBuffer, signedContactResponse, - subkey: 1) != - null) { - log.error('failed to reject contact invitation'); - return false; - } + await contactRequestInbox.eventualWriteProtobuf(signedContactResponse, + subkey: 1); return true; }); } diff --git a/lib/contact_invitation/views/invitation_dialog.dart b/lib/contact_invitation/views/invitation_dialog.dart index 4e9e5e1..4d304ab 100644 --- a/lib/contact_invitation/views/invitation_dialog.dart +++ b/lib/contact_invitation/views/invitation_dialog.dart @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_translate/flutter_translate.dart'; +import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; import '../../contacts/contacts.dart'; @@ -222,6 +223,16 @@ class InvitationDialogState extends State { _validInvitation = null; widget.onValidationFailed(); }); + } on VeilidAPIException { + final errorText = translate('invitation_dialog.invalid_invitation'); + if (mounted) { + showErrorToast(context, errorText); + } + setState(() { + _isValidating = false; + _validInvitation = null; + widget.onValidationFailed(); + }); } on Exception catch (e) { log.debug('exception: $e', e); setState(() { @@ -233,6 +244,48 @@ class InvitationDialogState extends State { } } + List _buildPreAccept() => [ + if (!_isValidating && _validInvitation == null) + widget.buildInviteControl(context, this, _validateInviteData), + if (_isValidating) + Column(children: [ + Text(translate('invitation_dialog.validating')) + .paddingLTRB(0, 0, 0, 16), + buildProgressIndicator().paddingAll(16), + ]).toCenter(), + if (_validInvitation == null && + !_isValidating && + widget.inviteControlIsValid()) + Column(children: [ + Text(translate('invitation_dialog.invalid_invitation')), + const Icon(Icons.error).paddingAll(16) + ]).toCenter(), + if (_validInvitation != null && !_isValidating) + Column(children: [ + Container( + constraints: const BoxConstraints(maxHeight: 64), + width: double.infinity, + child: + ProfileWidget(profile: _validInvitation!.remoteProfile)) + .paddingLTRB(0, 0, 0, 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ElevatedButton.icon( + icon: const Icon(Icons.check_circle), + label: Text(translate('button.accept')), + onPressed: _onAccept, + ).paddingLTRB(0, 0, 8, 0), + ElevatedButton.icon( + icon: const Icon(Icons.cancel), + label: Text(translate('button.reject')), + onPressed: _onReject, + ).paddingLTRB(8, 0, 0, 0) + ], + ), + ]) + ]; + @override // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { @@ -240,63 +293,20 @@ class InvitationDialogState extends State { // final scale = theme.extension()!; // final textTheme = theme.textTheme; // final height = MediaQuery.of(context).size.height; + final dismissible = !_isAccepting && !_isValidating; - if (_isAccepting) { - return SizedBox( - height: 300, - width: 300, - child: buildProgressIndicator().toCenter()) - .paddingAll(16); - } - return ConstrainedBox( + final dialog = ConstrainedBox( constraints: const BoxConstraints(maxHeight: 400, maxWidth: 400), child: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, - children: [ - widget.buildInviteControl(context, this, _validateInviteData), - if (_isValidating) - Column(children: [ - Text(translate('invitation_dialog.validating')) - .paddingLTRB(0, 0, 0, 16), - buildProgressIndicator().paddingAll(16), - ]).toCenter(), - if (_validInvitation == null && - !_isValidating && - widget.inviteControlIsValid()) - Column(children: [ - Text(translate('invitation_dialog.invalid_invitation')), - const Icon(Icons.error) - ]).paddingAll(16).toCenter(), - if (_validInvitation != null && !_isValidating) - Column(children: [ - Container( - constraints: const BoxConstraints(maxHeight: 64), - width: double.infinity, - child: ProfileWidget( - profile: _validInvitation!.remoteProfile)) - .paddingLTRB(0, 0, 0, 8), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - ElevatedButton.icon( - icon: const Icon(Icons.check_circle), - label: Text(translate('button.accept')), - onPressed: _onAccept, - ), - ElevatedButton.icon( - icon: const Icon(Icons.cancel), - label: Text(translate('button.reject')), - onPressed: _onReject, - ) - ], - ), - ]) - ]), + children: _isAccepting + ? [buildProgressIndicator().paddingAll(16)] + : _buildPreAccept()), ), ); + return PopControl(dismissible: dismissible, child: dialog); } @override diff --git a/lib/contact_invitation/views/paste_invitation_dialog.dart b/lib/contact_invitation/views/paste_invitation_dialog.dart index 50afb51..75a6208 100644 --- a/lib/contact_invitation/views/paste_invitation_dialog.dart +++ b/lib/contact_invitation/views/paste_invitation_dialog.dart @@ -17,10 +17,13 @@ class PasteInvitationDialog extends StatefulWidget { PasteInvitationDialogState createState() => PasteInvitationDialogState(); static Future show(BuildContext context) async { - await StyledDialog.show( + final modalContext = context; + + await showPopControlDialog( context: context, - title: translate('paste_invitation_dialog.title'), - child: PasteInvitationDialog(modalContext: context)); + builder: (context) => StyledDialog( + title: translate('paste_invitation_dialog.title'), + child: PasteInvitationDialog(modalContext: modalContext))); } final BuildContext modalContext; @@ -67,8 +70,13 @@ class PasteInvitationDialogState extends State { .sublist(firstline, lastline) .join() .replaceAll(RegExp(r'[^A-Za-z0-9\-_]'), ''); - final inviteData = base64UrlNoPadDecode(inviteDataBase64); + var inviteData = Uint8List(0); + try { + inviteData = base64UrlNoPadDecode(inviteDataBase64); + } on Exception { + // + } await validateInviteData(inviteData: inviteData); } @@ -105,7 +113,7 @@ class PasteInvitationDialogState extends State { return Column(mainAxisSize: MainAxisSize.min, children: [ Text( translate('paste_invitation_dialog.paste_invite_here'), - ).paddingLTRB(0, 0, 0, 8), + ).paddingLTRB(0, 0, 0, 16), Container( constraints: const BoxConstraints(maxHeight: 200), child: TextField( diff --git a/lib/contact_invitation/views/scan_invitation_dialog.dart b/lib/contact_invitation/views/scan_invitation_dialog.dart index 3df0053..03f6101 100644 --- a/lib/contact_invitation/views/scan_invitation_dialog.dart +++ b/lib/contact_invitation/views/scan_invitation_dialog.dart @@ -110,10 +110,12 @@ class ScanInvitationDialog extends StatefulWidget { ScanInvitationDialogState createState() => ScanInvitationDialogState(); static Future show(BuildContext context) async { - await StyledDialog.show( + final modalContext = context; + await showPopControlDialog( context: context, - title: translate('scan_invitation_dialog.title'), - child: ScanInvitationDialog(modalContext: context)); + builder: (context) => StyledDialog( + title: translate('scan_invitation_dialog.title'), + child: ScanInvitationDialog(modalContext: modalContext))); } final BuildContext modalContext; diff --git a/lib/init.dart b/lib/init.dart index 24ec467..cd01f97 100644 --- a/lib/init.dart +++ b/lib/init.dart @@ -24,7 +24,8 @@ class VeilidChatGlobalInit { await ProcessorRepository.instance.startup(); // DHT Record Pool - await DHTRecordPool.init(); + await DHTRecordPool.init( + logger: (message) => log.debug('DHTRecordPool: $message')); } // Initialize repositories diff --git a/lib/tools/pop_control.dart b/lib/tools/pop_control.dart index 8ef984f..29dc562 100644 --- a/lib/tools/pop_control.dart +++ b/lib/tools/pop_control.dart @@ -22,7 +22,9 @@ class PopControl extends StatelessWidget { final route = ModalRoute.of(context); if (route != null && route is PopControlDialogRoute) { - route.barrierDismissible = dismissible; + WidgetsBinding.instance.addPostFrameCallback((_) { + route.barrierDismissible = dismissible; + }); } return PopScope( @@ -65,6 +67,7 @@ class PopControlDialogRoute extends DialogRoute { set barrierDismissible(bool d) { _barrierDismissible = d; + changedInternalState(); } bool _barrierDismissible; diff --git a/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.dart b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.dart index 27e26b8..2c37cdd 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_record/dht_record_pool.dart @@ -17,6 +17,8 @@ part 'dht_record.dart'; const int watchBackoffMultiplier = 2; const int watchBackoffMax = 30; +typedef DHTRecordPoolLogger = void Function(String message); + /// Record pool that managed DHTRecords and allows for tagged deletion /// String versions of keys due to IMap<> json unsupported in key @freezed @@ -90,6 +92,12 @@ class OpenedRecordInfo { defaultRoutingContext: defaultRoutingContext); SharedDHTRecordData shared; Set records = {}; + + String get debugNames { + final r = records.toList() + ..sort((a, b) => a.key.toString().compareTo(b.key.toString())); + return '[${r.map((x) => x.debugName).join(',')}]'; + } } class DHTRecordPool with TableDBBacked { @@ -100,6 +108,9 @@ class DHTRecordPool with TableDBBacked { _routingContext = routingContext, _veilid = veilid; + // Logger + DHTRecordPoolLogger? _logger; + // Persistent DHT record list DHTRecordPoolAllocations _state; // Create/open Mutex @@ -136,15 +147,21 @@ class DHTRecordPool with TableDBBacked { static DHTRecordPool get instance => _singleton!; - static Future init() async { + static Future init({DHTRecordPoolLogger? logger}) async { final routingContext = await Veilid.instance.routingContext(); final globalPool = DHTRecordPool._(Veilid.instance, routingContext); - globalPool._state = await globalPool.load(); + globalPool + .._logger = logger + .._state = await globalPool.load(); _singleton = globalPool; } Veilid get veilid => _veilid; + void log(String message) { + _logger?.call(message); + } + Future _recordCreateInner( {required String debugName, required VeilidRoutingContext dhtctx, @@ -156,6 +173,8 @@ class DHTRecordPool with TableDBBacked { // Create the record final recordDescriptor = await dhtctx.createDHTRecord(schema); + log('createDHTRecord: debugName=$debugName key=${recordDescriptor.key}'); + // Reopen if a writer is specified to ensure // we switch the default writer if (writer != null) { @@ -185,6 +204,8 @@ class DHTRecordPool with TableDBBacked { TypedKey? parent}) async { assert(_mutex.isLocked, 'should be locked here'); + log('openDHTRecord: debugName=$debugName key=$recordKey'); + // If we are opening a key that already exists // make sure we are using the same parent if one was specified _validateParentInner(parent, recordKey); @@ -238,6 +259,9 @@ class DHTRecordPool with TableDBBacked { Future _recordClosed(DHTRecord record) async { await _mutex.protect(() async { final key = record.key; + + log('closeDHTRecord: debugName=${record.debugName} key=$key'); + final openedRecordInfo = _opened[key]; if (openedRecordInfo == null || !openedRecordInfo.records.remove(record)) { @@ -284,6 +308,8 @@ class DHTRecordPool with TableDBBacked { } Future _deleteInner(TypedKey recordKey) async { + log('deleteDHTRecord: key=$recordKey'); + // Remove this child from parents await _removeDependenciesInner([recordKey]); await _routingContext.deleteDHTRecord(recordKey); @@ -676,9 +702,14 @@ class DHTRecordPool with TableDBBacked { var success = false; try { success = await dhtctx.cancelDHTWatch(openedRecordKey); + + log('cancelDHTWatch: key=$openedRecordKey, success=$success, ' + 'debugNames=${openedRecordInfo.debugNames}'); + openedRecordInfo.shared.needsWatchStateUpdate = false; - } on VeilidAPIException { + } on VeilidAPIException catch (e) { // Failed to cancel DHT watch, try again next tick + log('Exception in watch cancel: $e'); } return success; }); @@ -687,12 +718,21 @@ class DHTRecordPool with TableDBBacked { // Record needs new watch var success = false; try { + final subkeys = watchState.subkeys?.toList(); + final count = watchState.count; + final expiration = watchState.expiration; + final realExpiration = await dhtctx.watchDHTValues( openedRecordKey, subkeys: watchState.subkeys?.toList(), count: watchState.count, expiration: watchState.expiration); + log('watchDHTValues: key=$openedRecordKey, subkeys=$subkeys, ' + 'count=$count, expiration=$expiration, ' + 'realExpiration=$realExpiration, ' + 'debugNames=${openedRecordInfo.debugNames}'); + // Update watch states with real expiration if (realExpiration.value != BigInt.zero) { openedRecordInfo.shared.needsWatchStateUpdate = false; @@ -700,8 +740,9 @@ class DHTRecordPool with TableDBBacked { openedRecordInfo.records, realExpiration); success = true; } - } on VeilidAPIException { + } on VeilidAPIException catch (e) { // Failed to cancel DHT watch, try again next tick + log('Exception in watch update: $e'); } return success; });