watch value fix, invitation fix, logging

This commit is contained in:
Christien Rioux 2024-04-06 22:36:30 -04:00
parent b3e9cbd4f3
commit 8335e36876
7 changed files with 133 additions and 77 deletions

View File

@ -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;
});
}

View File

@ -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<InvitationDialog> {
_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<InvitationDialog> {
}
}
List<Widget> _buildPreAccept() => <Widget>[
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<InvitationDialog> {
// final scale = theme.extension<ScaleScheme>()!;
// 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>[
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

View File

@ -17,10 +17,13 @@ class PasteInvitationDialog extends StatefulWidget {
PasteInvitationDialogState createState() => PasteInvitationDialogState();
static Future<void> show(BuildContext context) async {
await StyledDialog.show<void>(
final modalContext = context;
await showPopControlDialog<void>(
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<PasteInvitationDialog> {
.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<PasteInvitationDialog> {
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(

View File

@ -110,10 +110,12 @@ class ScanInvitationDialog extends StatefulWidget {
ScanInvitationDialogState createState() => ScanInvitationDialogState();
static Future<void> show(BuildContext context) async {
await StyledDialog.show<void>(
final modalContext = context;
await showPopControlDialog<void>(
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;

View File

@ -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

View File

@ -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<T> extends DialogRoute<T> {
set barrierDismissible(bool d) {
_barrierDismissible = d;
changedInternalState();
}
bool _barrierDismissible;

View File

@ -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<DHTRecord> 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<DHTRecordPoolAllocations> {
@ -100,6 +108,9 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
_routingContext = routingContext,
_veilid = veilid;
// Logger
DHTRecordPoolLogger? _logger;
// Persistent DHT record list
DHTRecordPoolAllocations _state;
// Create/open Mutex
@ -136,15 +147,21 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
static DHTRecordPool get instance => _singleton!;
static Future<void> init() async {
static Future<void> 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<OpenedRecordInfo> _recordCreateInner(
{required String debugName,
required VeilidRoutingContext dhtctx,
@ -156,6 +173,8 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
// 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<DHTRecordPoolAllocations> {
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<DHTRecordPoolAllocations> {
Future<void> _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<DHTRecordPoolAllocations> {
}
Future<void> _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<DHTRecordPoolAllocations> {
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<DHTRecordPoolAllocations> {
// 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<DHTRecordPoolAllocations> {
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;
});