diff --git a/assets/i18n/en.json b/assets/i18n/en.json index 0fad430..22feb44 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -122,7 +122,9 @@ }, "create_invitation_dialog": { "title": "Create Contact Invitation", - "connect_with_me": "Connect with me on VeilidChat!", + "me": "me", + "fingerprint": "Fingerprint:", + "connect_with_me": "Connect with {name} on VeilidChat!", "enter_message_hint": "Enter message for contact (optional)", "message_to_contact": "Message to send with invitation (not encrypted)", "generate": "Generate Invitation", @@ -148,6 +150,7 @@ "failed_to_reject": "Failed to reject contact invitation", "invalid_invitation": "Invalid invitation", "try_again_online": "Invitation could not be reached, try again when online", + "key_not_found": "Invitation could not be found, it may not be on the network yet", "protected_with_pin": "Contact invitation is protected with a PIN", "protected_with_password": "Contact invitation is protected with a password", "invalid_pin": "Invalid PIN", @@ -155,7 +158,7 @@ }, "waiting_invitation": { "accepted": "Contact invitation accepted from {name}", - "reject": "Contact invitation was rejected" + "rejected": "Contact invitation was rejected" }, "paste_invitation_dialog": { "title": "Paste Contact Invite", @@ -225,6 +228,10 @@ "in_app": "In-app", "push": "Push", "in_app_or_push": "In-app or Push", + "notifications": "Notifications", + "event": "Event", + "sound": "Sound", + "delivery": "Delivery", "enable_badge": "Enable icon 'badge' bubble", "enable_notifications": "Enable notifications", "message_notification_content": "Message notification content", diff --git a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart index f3e9521..f5663af 100644 --- a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart +++ b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:async_tools/async_tools.dart'; import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:fixnum/fixnum.dart'; @@ -214,9 +215,11 @@ class ContactInvitationListCubit } } - Future validateInvitation( - {required Uint8List inviteData, - required GetEncryptionKeyCallback getEncryptionKeyCallback}) async { + Future validateInvitation({ + required Uint8List inviteData, + required GetEncryptionKeyCallback getEncryptionKeyCallback, + required CancelRequest cancelRequest, + }) async { final pool = DHTRecordPool.instance; // Get contact request inbox from invitation @@ -245,15 +248,18 @@ class ContactInvitationListCubit contactRequestInboxKey) != -1; - await (await pool.openRecordRead(contactRequestInboxKey, - debugName: 'ContactInvitationListCubit::validateInvitation::' - 'ContactRequestInbox', - parent: pool.getParentRecordKey(contactRequestInboxKey) ?? - _accountInfo.accountRecordKey)) + await (await pool + .openRecordRead(contactRequestInboxKey, + debugName: 'ContactInvitationListCubit::validateInvitation::' + 'ContactRequestInbox', + parent: pool.getParentRecordKey(contactRequestInboxKey) ?? + _accountInfo.accountRecordKey) + .withCancel(cancelRequest)) .maybeDeleteScope(!isSelf, (contactRequestInbox) async { // final contactRequest = await contactRequestInbox - .getProtobuf(proto.ContactRequest.fromBuffer); + .getProtobuf(proto.ContactRequest.fromBuffer) + .withCancel(cancelRequest); final cs = await pool.veilid.getCryptoSystem(contactRequestInboxKey.kind); @@ -281,7 +287,8 @@ class ContactInvitationListCubit // Fetch the account master final contactSuperIdentity = await SuperIdentity.open( - superRecordKey: contactSuperIdentityRecordKey); + superRecordKey: contactSuperIdentityRecordKey) + .withCancel(cancelRequest); // Verify final idcs = await contactSuperIdentity.currentInstance.cryptoSystem; diff --git a/lib/contact_invitation/views/contact_invitation_display.dart b/lib/contact_invitation/views/contact_invitation_display.dart index d816fc3..83b80d0 100644 --- a/lib/contact_invitation/views/contact_invitation_display.dart +++ b/lib/contact_invitation/views/contact_invitation_display.dart @@ -11,6 +11,7 @@ import 'package:provider/provider.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:veilid_support/veilid_support.dart'; +import '../../account_manager/account_manager.dart'; import '../../notifications/notifications.dart'; import '../../proto/proto.dart' as proto; import '../../theme/theme.dart'; @@ -20,17 +21,20 @@ class ContactInvitationDisplayDialog extends StatelessWidget { const ContactInvitationDisplayDialog._({ required this.locator, required this.message, + required this.fingerprint, }); final Locator locator; final String message; + final String fingerprint; @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties ..add(StringProperty('message', message)) - ..add(DiagnosticsProperty('locator', locator)); + ..add(DiagnosticsProperty('locator', locator)) + ..add(StringProperty('fingerprint', fingerprint)); } String makeTextInvite(String message, Uint8List data) { @@ -38,10 +42,12 @@ class ContactInvitationDisplayDialog extends StatelessWidget { base64UrlNoPadEncode(data), '\n', 40, repeat: true); final msg = message.isNotEmpty ? '$message\n' : ''; + return '$msg' '--- BEGIN VEILIDCHAT CONTACT INVITE ----\n' '$invite\n' - '---- END VEILIDCHAT CONTACT INVITE -----\n'; + '---- END VEILIDCHAT CONTACT INVITE -----\n' + 'Fingerprint:\n$fingerprint\n'; } @override @@ -97,18 +103,27 @@ class ContactInvitationDisplayDialog extends StatelessWidget { .copyWith(color: Colors.black))) .paddingAll(8), FittedBox( - child: QrImageView.withQr( - size: 300, - qr: QrCode.fromUint8List( - data: data.$1, - errorCorrectLevel: - QrErrorCorrectLevel.L))) - .expanded(), + child: QrImageView.withQr( + size: 300, + qr: QrCode.fromUint8List( + data: data.$1, + errorCorrectLevel: + QrErrorCorrectLevel.L)), + ).expanded(), Text(message, softWrap: true, style: textTheme.labelLarge! .copyWith(color: Colors.black)) .paddingAll(8), + Text( + '${translate('create_invitation_dialog.fingerprint')}\n' + '$fingerprint', + softWrap: true, + textAlign: TextAlign.center, + style: textTheme.labelSmall!.copyWith( + color: Colors.black, + fontFamily: 'Source Code Pro')) + .paddingAll(2), ElevatedButton.icon( icon: const Icon(Icons.copy), style: ElevatedButton.styleFrom( @@ -129,11 +144,15 @@ class ContactInvitationDisplayDialog extends StatelessWidget { error: errorPage))))); } - static Future show( - {required BuildContext context, - required Locator locator, - required InvitationGeneratorCubit Function(BuildContext) create, - required String message}) async { + static Future show({ + required BuildContext context, + required Locator locator, + required InvitationGeneratorCubit Function(BuildContext) create, + required String message, + }) async { + final fingerprint = + locator().state.identityPublicKey.toString(); + await showPopControlDialog( context: context, builder: (context) => BlocProvider( @@ -141,6 +160,7 @@ class ContactInvitationDisplayDialog extends StatelessWidget { child: ContactInvitationDisplayDialog._( locator: locator, message: message, + fingerprint: fingerprint, ))); } } diff --git a/lib/contact_invitation/views/create_invitation_dialog.dart b/lib/contact_invitation/views/create_invitation_dialog.dart index 9117d03..5711a32 100644 --- a/lib/contact_invitation/views/create_invitation_dialog.dart +++ b/lib/contact_invitation/views/create_invitation_dialog.dart @@ -37,8 +37,7 @@ class CreateInvitationDialog extends StatefulWidget { } class CreateInvitationDialogState extends State { - final _messageTextController = TextEditingController( - text: translate('create_invitation_dialog.connect_with_me')); + late final TextEditingController _messageTextController; EncryptionKeyType _encryptionKeyType = EncryptionKeyType.none; String _encryptionKey = ''; @@ -46,6 +45,12 @@ class CreateInvitationDialogState extends State { @override void initState() { + final accountInfo = widget.locator().state; + final name = accountInfo.asData?.value.profile.name ?? + translate('create_invitation_dialog.me'); + _messageTextController = TextEditingController( + text: translate('create_invitation_dialog.connect_with_me', + args: {'name': name})); super.initState(); } @@ -152,13 +157,13 @@ class CreateInvitationDialogState extends State { message: _messageTextController.text, expiration: _expiration); + navigator.pop(); + await ContactInvitationDisplayDialog.show( context: context, locator: widget.locator, message: _messageTextController.text, create: (context) => InvitationGeneratorCubit(generator)); - - navigator.pop(); } @override diff --git a/lib/contact_invitation/views/invitation_dialog.dart b/lib/contact_invitation/views/invitation_dialog.dart index 5e1ece6..385cbcb 100644 --- a/lib/contact_invitation/views/invitation_dialog.dart +++ b/lib/contact_invitation/views/invitation_dialog.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:async_tools/async_tools.dart'; import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -61,17 +62,19 @@ class InvitationDialog extends StatefulWidget { } class InvitationDialogState extends State { - ValidContactInvitation? _validInvitation; - bool _isValidating = false; - bool _isAccepting = false; - @override void initState() { super.initState(); } - bool get isValidating => _isValidating; - bool get isAccepting => _isAccepting; + Future _onCancel() async { + final navigator = Navigator.of(context); + _cancelRequest.cancel(); + setState(() { + _isAccepting = false; + }); + navigator.pop(); + } Future _onAccept() async { final navigator = Navigator.of(context); @@ -153,6 +156,7 @@ class InvitationDialogState extends State { final validatedContactInvitation = await contactInvitationListCubit.validateInvitation( inviteData: inviteData, + cancelRequest: _cancelRequest, getEncryptionKeyCallback: (cs, encryptionKeyType, encryptedSecret) async { String encryptionKey; @@ -234,6 +238,9 @@ class InvitationDialogState extends State { late final String errorText; if (e is VeilidAPIExceptionTryAgain) { errorText = translate('invitation_dialog.try_again_online'); + } + if (e is VeilidAPIExceptionKeyNotFound) { + errorText = translate('invitation_dialog.key_not_found'); } else { errorText = translate('invitation_dialog.invalid_invitation'); } @@ -245,6 +252,12 @@ class InvitationDialogState extends State { _validInvitation = null; widget.onValidationFailed(); }); + } on CancelException { + setState(() { + _isValidating = false; + _validInvitation = null; + widget.onValidationCancelled(); + }); } on Exception catch (e) { log.debug('exception: $e', e); setState(() { @@ -264,6 +277,11 @@ class InvitationDialogState extends State { Text(translate('invitation_dialog.validating')) .paddingLTRB(0, 0, 0, 16), buildProgressIndicator().paddingAll(16), + ElevatedButton.icon( + icon: const Icon(Icons.cancel), + label: Text(translate('button.cancel')), + onPressed: _onCancel, + ).paddingAll(16), ]).toCenter(), if (_validInvitation == null && !_isValidating && @@ -315,13 +333,25 @@ class InvitationDialogState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: _isAccepting - ? [buildProgressIndicator().paddingAll(16)] + ? [ + buildProgressIndicator().paddingAll(16), + ] : _buildPreAccept()), ), ); return PopControl(dismissible: dismissible, child: dialog); } + //////////////////////////////////////////////////////////////////////////// + + ValidContactInvitation? _validInvitation; + bool _isValidating = false; + bool _isAccepting = false; + final _cancelRequest = CancelRequest(); + + bool get isValidating => _isValidating; + bool get isAccepting => _isAccepting; + @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); diff --git a/lib/notifications/views/notifications_preferences.dart b/lib/notifications/views/notifications_preferences.dart index 12f4667..5967522 100644 --- a/lib/notifications/views/notifications_preferences.dart +++ b/lib/notifications/views/notifications_preferences.dart @@ -52,7 +52,11 @@ Widget buildSettingsPageNotificationPreferences( out.add(DropdownMenuItem( value: x.$1, enabled: x.$2, - child: Text(x.$3, style: textTheme.labelSmall))); + child: Text( + x.$3, + style: textTheme.labelSmall, + textAlign: TextAlign.center, + ))); } return out; } @@ -71,7 +75,11 @@ Widget buildSettingsPageNotificationPreferences( out.add(DropdownMenuItem( value: x.$1, enabled: x.$2, - child: Text(x.$3, style: textTheme.labelSmall))); + child: Text( + x.$3, + style: textTheme.labelSmall, + textAlign: TextAlign.center, + ))); } return out; } @@ -100,17 +108,23 @@ Widget buildSettingsPageNotificationPreferences( out.add(DropdownMenuItem( value: x.$1, enabled: x.$2, - child: Text(x.$3, style: textTheme.labelSmall))); + child: Text( + x.$3, + style: textTheme.labelSmall, + textAlign: TextAlign.center, + ))); } return out; } - return DecoratedBox( - decoration: ShapeDecoration( - shape: RoundedRectangleBorder( - side: BorderSide(width: 2, color: scale.primaryScale.border), - borderRadius: - BorderRadius.circular(8 * scaleConfig.borderRadiusScale))), + return InputDecorator( + decoration: InputDecoration( + labelText: translate('settings_page.notifications'), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8 * scaleConfig.borderRadiusScale), + borderSide: BorderSide(width: 2, color: scale.primaryScale.border), + ), + ), child: Column(mainAxisSize: MainAxisSize.min, children: [ // Display Beta Warning FormBuilderCheckbox( @@ -175,12 +189,35 @@ Widget buildSettingsPageNotificationPreferences( await updatePreferences(newNotificationsPreference); }, items: messageNotificationContentItems(), - ).paddingAll(8), + ).paddingLTRB(0, 4, 0, 4), // Notifications Table( defaultVerticalAlignment: TableCellVerticalAlignment.middle, children: [ + TableRow(children: [ + Text(translate('settings_page.event'), + textAlign: TextAlign.center, + style: textTheme.titleMedium!.copyWith( + color: scale.primaryScale.border, + decorationColor: scale.primaryScale.border, + decoration: TextDecoration.underline)) + .paddingAll(8), + Text(translate('settings_page.delivery'), + textAlign: TextAlign.center, + style: textTheme.titleMedium!.copyWith( + color: scale.primaryScale.border, + decorationColor: scale.primaryScale.border, + decoration: TextDecoration.underline)) + .paddingAll(8), + Text(translate('settings_page.sound'), + textAlign: TextAlign.center, + style: textTheme.titleMedium!.copyWith( + color: scale.primaryScale.border, + decorationColor: scale.primaryScale.border, + decoration: TextDecoration.underline)) + .paddingAll(8), + ]), TableRow(children: [ // Invitation accepted Text( @@ -216,7 +253,7 @@ Widget buildSettingsPageNotificationPreferences( await updatePreferences(newNotificationsPreference); }, items: soundEffectItems(), - ).paddingAll(4) + ).paddingLTRB(4, 4, 0, 4) ]), // Message received TableRow(children: [ @@ -253,7 +290,7 @@ Widget buildSettingsPageNotificationPreferences( await updatePreferences(newNotificationsPreference); }, items: soundEffectItems(), - ).paddingAll(4) + ).paddingLTRB(4, 4, 0, 4) ]), // Message sent @@ -277,9 +314,9 @@ Widget buildSettingsPageNotificationPreferences( await updatePreferences(newNotificationsPreference); }, items: soundEffectItems(), - ).paddingAll(4) + ).paddingLTRB(4, 4, 0, 4) ]), - ]).paddingAll(8) + ]) ]).paddingAll(8), ); } diff --git a/packages/veilid_support/pubspec.lock b/packages/veilid_support/pubspec.lock index 7f08359..e68b6db 100644 --- a/packages/veilid_support/pubspec.lock +++ b/packages/veilid_support/pubspec.lock @@ -36,10 +36,9 @@ packages: async_tools: dependency: "direct main" description: - name: async_tools - sha256: "375da1e5b51974a9e84469b7630f36d1361fb53d3fcc818a5a0121ab51fe0343" - url: "https://pub.dev" - source: hosted + path: "../../../dart_async_tools" + relative: true + source: path version: "0.1.4" bloc: dependency: "direct main" @@ -52,10 +51,9 @@ packages: bloc_advanced_tools: dependency: "direct main" description: - name: bloc_advanced_tools - sha256: "0599d860eb096c5b12457c60bdf7f66bfcb7171bc94ccf2c7c752b6a716a79f9" - url: "https://pub.dev" - source: hosted + path: "../../../bloc_advanced_tools" + relative: true + source: path version: "0.1.4" boolean_selector: dependency: transitive diff --git a/packages/veilid_support/pubspec.yaml b/packages/veilid_support/pubspec.yaml index 90d7ad8..51c3e60 100644 --- a/packages/veilid_support/pubspec.yaml +++ b/packages/veilid_support/pubspec.yaml @@ -26,11 +26,11 @@ dependencies: # veilid: ^0.0.1 path: ../../../veilid/veilid-flutter -# dependency_overrides: -# async_tools: -# path: ../../../dart_async_tools -# bloc_advanced_tools: -# path: ../../../bloc_advanced_tools +dependency_overrides: + async_tools: + path: ../../../dart_async_tools + bloc_advanced_tools: + path: ../../../bloc_advanced_tools dev_dependencies: build_runner: ^2.4.10 diff --git a/pubspec.lock b/pubspec.lock index 2b3149b..6d78407 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -84,10 +84,9 @@ packages: async_tools: dependency: "direct main" description: - name: async_tools - sha256: "375da1e5b51974a9e84469b7630f36d1361fb53d3fcc818a5a0121ab51fe0343" - url: "https://pub.dev" - source: hosted + path: "../dart_async_tools" + relative: true + source: path version: "0.1.4" awesome_extensions: dependency: "direct main" @@ -140,10 +139,9 @@ packages: bloc_advanced_tools: dependency: "direct main" description: - name: bloc_advanced_tools - sha256: "0599d860eb096c5b12457c60bdf7f66bfcb7171bc94ccf2c7c752b6a716a79f9" - url: "https://pub.dev" - source: hosted + path: "../bloc_advanced_tools" + relative: true + source: path version: "0.1.4" blurry_modal_progress_hud: dependency: "direct main" diff --git a/pubspec.yaml b/pubspec.yaml index fea8acb..2c0b8eb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -107,11 +107,11 @@ dependencies: xterm: ^4.0.0 zxing2: ^0.2.3 -# dependency_overrides: -# async_tools: -# path: ../dart_async_tools -# bloc_advanced_tools: -# path: ../bloc_advanced_tools +dependency_overrides: + async_tools: + path: ../dart_async_tools + bloc_advanced_tools: + path: ../bloc_advanced_tools # flutter_chat_ui: # path: ../flutter_chat_ui