ui improvements for invitations

This commit is contained in:
Christien Rioux 2024-07-27 18:36:06 -04:00
parent d962f98786
commit 01c6490ec4
10 changed files with 179 additions and 77 deletions

View File

@ -122,7 +122,9 @@
}, },
"create_invitation_dialog": { "create_invitation_dialog": {
"title": "Create Contact Invitation", "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)", "enter_message_hint": "Enter message for contact (optional)",
"message_to_contact": "Message to send with invitation (not encrypted)", "message_to_contact": "Message to send with invitation (not encrypted)",
"generate": "Generate Invitation", "generate": "Generate Invitation",
@ -148,6 +150,7 @@
"failed_to_reject": "Failed to reject contact invitation", "failed_to_reject": "Failed to reject contact invitation",
"invalid_invitation": "Invalid invitation", "invalid_invitation": "Invalid invitation",
"try_again_online": "Invitation could not be reached, try again when online", "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_pin": "Contact invitation is protected with a PIN",
"protected_with_password": "Contact invitation is protected with a password", "protected_with_password": "Contact invitation is protected with a password",
"invalid_pin": "Invalid PIN", "invalid_pin": "Invalid PIN",
@ -155,7 +158,7 @@
}, },
"waiting_invitation": { "waiting_invitation": {
"accepted": "Contact invitation accepted from {name}", "accepted": "Contact invitation accepted from {name}",
"reject": "Contact invitation was rejected" "rejected": "Contact invitation was rejected"
}, },
"paste_invitation_dialog": { "paste_invitation_dialog": {
"title": "Paste Contact Invite", "title": "Paste Contact Invite",
@ -225,6 +228,10 @@
"in_app": "In-app", "in_app": "In-app",
"push": "Push", "push": "Push",
"in_app_or_push": "In-app or Push", "in_app_or_push": "In-app or Push",
"notifications": "Notifications",
"event": "Event",
"sound": "Sound",
"delivery": "Delivery",
"enable_badge": "Enable icon 'badge' bubble", "enable_badge": "Enable icon 'badge' bubble",
"enable_notifications": "Enable notifications", "enable_notifications": "Enable notifications",
"message_notification_content": "Message notification content", "message_notification_content": "Message notification content",

View File

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:async_tools/async_tools.dart';
import 'package:bloc_advanced_tools/bloc_advanced_tools.dart'; import 'package:bloc_advanced_tools/bloc_advanced_tools.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
@ -214,9 +215,11 @@ class ContactInvitationListCubit
} }
} }
Future<ValidContactInvitation?> validateInvitation( Future<ValidContactInvitation?> validateInvitation({
{required Uint8List inviteData, required Uint8List inviteData,
required GetEncryptionKeyCallback getEncryptionKeyCallback}) async { required GetEncryptionKeyCallback getEncryptionKeyCallback,
required CancelRequest cancelRequest,
}) async {
final pool = DHTRecordPool.instance; final pool = DHTRecordPool.instance;
// Get contact request inbox from invitation // Get contact request inbox from invitation
@ -245,15 +248,18 @@ class ContactInvitationListCubit
contactRequestInboxKey) != contactRequestInboxKey) !=
-1; -1;
await (await pool.openRecordRead(contactRequestInboxKey, await (await pool
debugName: 'ContactInvitationListCubit::validateInvitation::' .openRecordRead(contactRequestInboxKey,
'ContactRequestInbox', debugName: 'ContactInvitationListCubit::validateInvitation::'
parent: pool.getParentRecordKey(contactRequestInboxKey) ?? 'ContactRequestInbox',
_accountInfo.accountRecordKey)) parent: pool.getParentRecordKey(contactRequestInboxKey) ??
_accountInfo.accountRecordKey)
.withCancel(cancelRequest))
.maybeDeleteScope(!isSelf, (contactRequestInbox) async { .maybeDeleteScope(!isSelf, (contactRequestInbox) async {
// //
final contactRequest = await contactRequestInbox final contactRequest = await contactRequestInbox
.getProtobuf(proto.ContactRequest.fromBuffer); .getProtobuf(proto.ContactRequest.fromBuffer)
.withCancel(cancelRequest);
final cs = await pool.veilid.getCryptoSystem(contactRequestInboxKey.kind); final cs = await pool.veilid.getCryptoSystem(contactRequestInboxKey.kind);
@ -281,7 +287,8 @@ class ContactInvitationListCubit
// Fetch the account master // Fetch the account master
final contactSuperIdentity = await SuperIdentity.open( final contactSuperIdentity = await SuperIdentity.open(
superRecordKey: contactSuperIdentityRecordKey); superRecordKey: contactSuperIdentityRecordKey)
.withCancel(cancelRequest);
// Verify // Verify
final idcs = await contactSuperIdentity.currentInstance.cryptoSystem; final idcs = await contactSuperIdentity.currentInstance.cryptoSystem;

View File

@ -11,6 +11,7 @@ import 'package:provider/provider.dart';
import 'package:qr_flutter/qr_flutter.dart'; import 'package:qr_flutter/qr_flutter.dart';
import 'package:veilid_support/veilid_support.dart'; import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart';
import '../../notifications/notifications.dart'; import '../../notifications/notifications.dart';
import '../../proto/proto.dart' as proto; import '../../proto/proto.dart' as proto;
import '../../theme/theme.dart'; import '../../theme/theme.dart';
@ -20,17 +21,20 @@ class ContactInvitationDisplayDialog extends StatelessWidget {
const ContactInvitationDisplayDialog._({ const ContactInvitationDisplayDialog._({
required this.locator, required this.locator,
required this.message, required this.message,
required this.fingerprint,
}); });
final Locator locator; final Locator locator;
final String message; final String message;
final String fingerprint;
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties); super.debugFillProperties(properties);
properties properties
..add(StringProperty('message', message)) ..add(StringProperty('message', message))
..add(DiagnosticsProperty<Locator>('locator', locator)); ..add(DiagnosticsProperty<Locator>('locator', locator))
..add(StringProperty('fingerprint', fingerprint));
} }
String makeTextInvite(String message, Uint8List data) { String makeTextInvite(String message, Uint8List data) {
@ -38,10 +42,12 @@ class ContactInvitationDisplayDialog extends StatelessWidget {
base64UrlNoPadEncode(data), '\n', 40, base64UrlNoPadEncode(data), '\n', 40,
repeat: true); repeat: true);
final msg = message.isNotEmpty ? '$message\n' : ''; final msg = message.isNotEmpty ? '$message\n' : '';
return '$msg' return '$msg'
'--- BEGIN VEILIDCHAT CONTACT INVITE ----\n' '--- BEGIN VEILIDCHAT CONTACT INVITE ----\n'
'$invite\n' '$invite\n'
'---- END VEILIDCHAT CONTACT INVITE -----\n'; '---- END VEILIDCHAT CONTACT INVITE -----\n'
'Fingerprint:\n$fingerprint\n';
} }
@override @override
@ -97,18 +103,27 @@ class ContactInvitationDisplayDialog extends StatelessWidget {
.copyWith(color: Colors.black))) .copyWith(color: Colors.black)))
.paddingAll(8), .paddingAll(8),
FittedBox( FittedBox(
child: QrImageView.withQr( child: QrImageView.withQr(
size: 300, size: 300,
qr: QrCode.fromUint8List( qr: QrCode.fromUint8List(
data: data.$1, data: data.$1,
errorCorrectLevel: errorCorrectLevel:
QrErrorCorrectLevel.L))) QrErrorCorrectLevel.L)),
.expanded(), ).expanded(),
Text(message, Text(message,
softWrap: true, softWrap: true,
style: textTheme.labelLarge! style: textTheme.labelLarge!
.copyWith(color: Colors.black)) .copyWith(color: Colors.black))
.paddingAll(8), .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( ElevatedButton.icon(
icon: const Icon(Icons.copy), icon: const Icon(Icons.copy),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
@ -129,11 +144,15 @@ class ContactInvitationDisplayDialog extends StatelessWidget {
error: errorPage))))); error: errorPage)))));
} }
static Future<void> show( static Future<void> show({
{required BuildContext context, required BuildContext context,
required Locator locator, required Locator locator,
required InvitationGeneratorCubit Function(BuildContext) create, required InvitationGeneratorCubit Function(BuildContext) create,
required String message}) async { required String message,
}) async {
final fingerprint =
locator<AccountInfoCubit>().state.identityPublicKey.toString();
await showPopControlDialog<void>( await showPopControlDialog<void>(
context: context, context: context,
builder: (context) => BlocProvider( builder: (context) => BlocProvider(
@ -141,6 +160,7 @@ class ContactInvitationDisplayDialog extends StatelessWidget {
child: ContactInvitationDisplayDialog._( child: ContactInvitationDisplayDialog._(
locator: locator, locator: locator,
message: message, message: message,
fingerprint: fingerprint,
))); )));
} }
} }

View File

@ -37,8 +37,7 @@ class CreateInvitationDialog extends StatefulWidget {
} }
class CreateInvitationDialogState extends State<CreateInvitationDialog> { class CreateInvitationDialogState extends State<CreateInvitationDialog> {
final _messageTextController = TextEditingController( late final TextEditingController _messageTextController;
text: translate('create_invitation_dialog.connect_with_me'));
EncryptionKeyType _encryptionKeyType = EncryptionKeyType.none; EncryptionKeyType _encryptionKeyType = EncryptionKeyType.none;
String _encryptionKey = ''; String _encryptionKey = '';
@ -46,6 +45,12 @@ class CreateInvitationDialogState extends State<CreateInvitationDialog> {
@override @override
void initState() { void initState() {
final accountInfo = widget.locator<AccountRecordCubit>().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(); super.initState();
} }
@ -152,13 +157,13 @@ class CreateInvitationDialogState extends State<CreateInvitationDialog> {
message: _messageTextController.text, message: _messageTextController.text,
expiration: _expiration); expiration: _expiration);
navigator.pop();
await ContactInvitationDisplayDialog.show( await ContactInvitationDisplayDialog.show(
context: context, context: context,
locator: widget.locator, locator: widget.locator,
message: _messageTextController.text, message: _messageTextController.text,
create: (context) => InvitationGeneratorCubit(generator)); create: (context) => InvitationGeneratorCubit(generator));
navigator.pop();
} }
@override @override

View File

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:async_tools/async_tools.dart';
import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -61,17 +62,19 @@ class InvitationDialog extends StatefulWidget {
} }
class InvitationDialogState extends State<InvitationDialog> { class InvitationDialogState extends State<InvitationDialog> {
ValidContactInvitation? _validInvitation;
bool _isValidating = false;
bool _isAccepting = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
} }
bool get isValidating => _isValidating; Future<void> _onCancel() async {
bool get isAccepting => _isAccepting; final navigator = Navigator.of(context);
_cancelRequest.cancel();
setState(() {
_isAccepting = false;
});
navigator.pop();
}
Future<void> _onAccept() async { Future<void> _onAccept() async {
final navigator = Navigator.of(context); final navigator = Navigator.of(context);
@ -153,6 +156,7 @@ class InvitationDialogState extends State<InvitationDialog> {
final validatedContactInvitation = final validatedContactInvitation =
await contactInvitationListCubit.validateInvitation( await contactInvitationListCubit.validateInvitation(
inviteData: inviteData, inviteData: inviteData,
cancelRequest: _cancelRequest,
getEncryptionKeyCallback: getEncryptionKeyCallback:
(cs, encryptionKeyType, encryptedSecret) async { (cs, encryptionKeyType, encryptedSecret) async {
String encryptionKey; String encryptionKey;
@ -234,6 +238,9 @@ class InvitationDialogState extends State<InvitationDialog> {
late final String errorText; late final String errorText;
if (e is VeilidAPIExceptionTryAgain) { if (e is VeilidAPIExceptionTryAgain) {
errorText = translate('invitation_dialog.try_again_online'); errorText = translate('invitation_dialog.try_again_online');
}
if (e is VeilidAPIExceptionKeyNotFound) {
errorText = translate('invitation_dialog.key_not_found');
} else { } else {
errorText = translate('invitation_dialog.invalid_invitation'); errorText = translate('invitation_dialog.invalid_invitation');
} }
@ -245,6 +252,12 @@ class InvitationDialogState extends State<InvitationDialog> {
_validInvitation = null; _validInvitation = null;
widget.onValidationFailed(); widget.onValidationFailed();
}); });
} on CancelException {
setState(() {
_isValidating = false;
_validInvitation = null;
widget.onValidationCancelled();
});
} on Exception catch (e) { } on Exception catch (e) {
log.debug('exception: $e', e); log.debug('exception: $e', e);
setState(() { setState(() {
@ -264,6 +277,11 @@ class InvitationDialogState extends State<InvitationDialog> {
Text(translate('invitation_dialog.validating')) Text(translate('invitation_dialog.validating'))
.paddingLTRB(0, 0, 0, 16), .paddingLTRB(0, 0, 0, 16),
buildProgressIndicator().paddingAll(16), buildProgressIndicator().paddingAll(16),
ElevatedButton.icon(
icon: const Icon(Icons.cancel),
label: Text(translate('button.cancel')),
onPressed: _onCancel,
).paddingAll(16),
]).toCenter(), ]).toCenter(),
if (_validInvitation == null && if (_validInvitation == null &&
!_isValidating && !_isValidating &&
@ -315,13 +333,25 @@ class InvitationDialogState extends State<InvitationDialog> {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: _isAccepting children: _isAccepting
? [buildProgressIndicator().paddingAll(16)] ? [
buildProgressIndicator().paddingAll(16),
]
: _buildPreAccept()), : _buildPreAccept()),
), ),
); );
return PopControl(dismissible: dismissible, child: dialog); 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 @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties); super.debugFillProperties(properties);

View File

@ -52,7 +52,11 @@ Widget buildSettingsPageNotificationPreferences(
out.add(DropdownMenuItem( out.add(DropdownMenuItem(
value: x.$1, value: x.$1,
enabled: x.$2, enabled: x.$2,
child: Text(x.$3, style: textTheme.labelSmall))); child: Text(
x.$3,
style: textTheme.labelSmall,
textAlign: TextAlign.center,
)));
} }
return out; return out;
} }
@ -71,7 +75,11 @@ Widget buildSettingsPageNotificationPreferences(
out.add(DropdownMenuItem( out.add(DropdownMenuItem(
value: x.$1, value: x.$1,
enabled: x.$2, enabled: x.$2,
child: Text(x.$3, style: textTheme.labelSmall))); child: Text(
x.$3,
style: textTheme.labelSmall,
textAlign: TextAlign.center,
)));
} }
return out; return out;
} }
@ -100,17 +108,23 @@ Widget buildSettingsPageNotificationPreferences(
out.add(DropdownMenuItem( out.add(DropdownMenuItem(
value: x.$1, value: x.$1,
enabled: x.$2, enabled: x.$2,
child: Text(x.$3, style: textTheme.labelSmall))); child: Text(
x.$3,
style: textTheme.labelSmall,
textAlign: TextAlign.center,
)));
} }
return out; return out;
} }
return DecoratedBox( return InputDecorator(
decoration: ShapeDecoration( decoration: InputDecoration(
shape: RoundedRectangleBorder( labelText: translate('settings_page.notifications'),
side: BorderSide(width: 2, color: scale.primaryScale.border), border: OutlineInputBorder(
borderRadius: borderRadius: BorderRadius.circular(8 * scaleConfig.borderRadiusScale),
BorderRadius.circular(8 * scaleConfig.borderRadiusScale))), borderSide: BorderSide(width: 2, color: scale.primaryScale.border),
),
),
child: Column(mainAxisSize: MainAxisSize.min, children: [ child: Column(mainAxisSize: MainAxisSize.min, children: [
// Display Beta Warning // Display Beta Warning
FormBuilderCheckbox( FormBuilderCheckbox(
@ -175,12 +189,35 @@ Widget buildSettingsPageNotificationPreferences(
await updatePreferences(newNotificationsPreference); await updatePreferences(newNotificationsPreference);
}, },
items: messageNotificationContentItems(), items: messageNotificationContentItems(),
).paddingAll(8), ).paddingLTRB(0, 4, 0, 4),
// Notifications // Notifications
Table( Table(
defaultVerticalAlignment: TableCellVerticalAlignment.middle, defaultVerticalAlignment: TableCellVerticalAlignment.middle,
children: [ 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: [ TableRow(children: [
// Invitation accepted // Invitation accepted
Text( Text(
@ -216,7 +253,7 @@ Widget buildSettingsPageNotificationPreferences(
await updatePreferences(newNotificationsPreference); await updatePreferences(newNotificationsPreference);
}, },
items: soundEffectItems(), items: soundEffectItems(),
).paddingAll(4) ).paddingLTRB(4, 4, 0, 4)
]), ]),
// Message received // Message received
TableRow(children: [ TableRow(children: [
@ -253,7 +290,7 @@ Widget buildSettingsPageNotificationPreferences(
await updatePreferences(newNotificationsPreference); await updatePreferences(newNotificationsPreference);
}, },
items: soundEffectItems(), items: soundEffectItems(),
).paddingAll(4) ).paddingLTRB(4, 4, 0, 4)
]), ]),
// Message sent // Message sent
@ -277,9 +314,9 @@ Widget buildSettingsPageNotificationPreferences(
await updatePreferences(newNotificationsPreference); await updatePreferences(newNotificationsPreference);
}, },
items: soundEffectItems(), items: soundEffectItems(),
).paddingAll(4) ).paddingLTRB(4, 4, 0, 4)
]), ]),
]).paddingAll(8) ])
]).paddingAll(8), ]).paddingAll(8),
); );
} }

View File

@ -36,10 +36,9 @@ packages:
async_tools: async_tools:
dependency: "direct main" dependency: "direct main"
description: description:
name: async_tools path: "../../../dart_async_tools"
sha256: "375da1e5b51974a9e84469b7630f36d1361fb53d3fcc818a5a0121ab51fe0343" relative: true
url: "https://pub.dev" source: path
source: hosted
version: "0.1.4" version: "0.1.4"
bloc: bloc:
dependency: "direct main" dependency: "direct main"
@ -52,10 +51,9 @@ packages:
bloc_advanced_tools: bloc_advanced_tools:
dependency: "direct main" dependency: "direct main"
description: description:
name: bloc_advanced_tools path: "../../../bloc_advanced_tools"
sha256: "0599d860eb096c5b12457c60bdf7f66bfcb7171bc94ccf2c7c752b6a716a79f9" relative: true
url: "https://pub.dev" source: path
source: hosted
version: "0.1.4" version: "0.1.4"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive

View File

@ -26,11 +26,11 @@ dependencies:
# veilid: ^0.0.1 # veilid: ^0.0.1
path: ../../../veilid/veilid-flutter path: ../../../veilid/veilid-flutter
# dependency_overrides: dependency_overrides:
# async_tools: async_tools:
# path: ../../../dart_async_tools path: ../../../dart_async_tools
# bloc_advanced_tools: bloc_advanced_tools:
# path: ../../../bloc_advanced_tools path: ../../../bloc_advanced_tools
dev_dependencies: dev_dependencies:
build_runner: ^2.4.10 build_runner: ^2.4.10

View File

@ -84,10 +84,9 @@ packages:
async_tools: async_tools:
dependency: "direct main" dependency: "direct main"
description: description:
name: async_tools path: "../dart_async_tools"
sha256: "375da1e5b51974a9e84469b7630f36d1361fb53d3fcc818a5a0121ab51fe0343" relative: true
url: "https://pub.dev" source: path
source: hosted
version: "0.1.4" version: "0.1.4"
awesome_extensions: awesome_extensions:
dependency: "direct main" dependency: "direct main"
@ -140,10 +139,9 @@ packages:
bloc_advanced_tools: bloc_advanced_tools:
dependency: "direct main" dependency: "direct main"
description: description:
name: bloc_advanced_tools path: "../bloc_advanced_tools"
sha256: "0599d860eb096c5b12457c60bdf7f66bfcb7171bc94ccf2c7c752b6a716a79f9" relative: true
url: "https://pub.dev" source: path
source: hosted
version: "0.1.4" version: "0.1.4"
blurry_modal_progress_hud: blurry_modal_progress_hud:
dependency: "direct main" dependency: "direct main"

View File

@ -107,11 +107,11 @@ dependencies:
xterm: ^4.0.0 xterm: ^4.0.0
zxing2: ^0.2.3 zxing2: ^0.2.3
# dependency_overrides: dependency_overrides:
# async_tools: async_tools:
# path: ../dart_async_tools path: ../dart_async_tools
# bloc_advanced_tools: bloc_advanced_tools:
# path: ../bloc_advanced_tools path: ../bloc_advanced_tools
# flutter_chat_ui: # flutter_chat_ui:
# path: ../flutter_chat_ui # path: ../flutter_chat_ui