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": {
"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",

View File

@ -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<ValidContactInvitation?> validateInvitation(
{required Uint8List inviteData,
required GetEncryptionKeyCallback getEncryptionKeyCallback}) async {
Future<ValidContactInvitation?> 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;

View File

@ -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', locator));
..add(DiagnosticsProperty<Locator>('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<void> show(
{required BuildContext context,
required Locator locator,
required InvitationGeneratorCubit Function(BuildContext) create,
required String message}) async {
static Future<void> show({
required BuildContext context,
required Locator locator,
required InvitationGeneratorCubit Function(BuildContext) create,
required String message,
}) async {
final fingerprint =
locator<AccountInfoCubit>().state.identityPublicKey.toString();
await showPopControlDialog<void>(
context: context,
builder: (context) => BlocProvider(
@ -141,6 +160,7 @@ class ContactInvitationDisplayDialog extends StatelessWidget {
child: ContactInvitationDisplayDialog._(
locator: locator,
message: message,
fingerprint: fingerprint,
)));
}
}

View File

@ -37,8 +37,7 @@ class CreateInvitationDialog extends StatefulWidget {
}
class CreateInvitationDialogState extends State<CreateInvitationDialog> {
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<CreateInvitationDialog> {
@override
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();
}
@ -152,13 +157,13 @@ class CreateInvitationDialogState extends State<CreateInvitationDialog> {
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

View File

@ -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<InvitationDialog> {
ValidContactInvitation? _validInvitation;
bool _isValidating = false;
bool _isAccepting = false;
@override
void initState() {
super.initState();
}
bool get isValidating => _isValidating;
bool get isAccepting => _isAccepting;
Future<void> _onCancel() async {
final navigator = Navigator.of(context);
_cancelRequest.cancel();
setState(() {
_isAccepting = false;
});
navigator.pop();
}
Future<void> _onAccept() async {
final navigator = Navigator.of(context);
@ -153,6 +156,7 @@ class InvitationDialogState extends State<InvitationDialog> {
final validatedContactInvitation =
await contactInvitationListCubit.validateInvitation(
inviteData: inviteData,
cancelRequest: _cancelRequest,
getEncryptionKeyCallback:
(cs, encryptionKeyType, encryptedSecret) async {
String encryptionKey;
@ -234,6 +238,9 @@ class InvitationDialogState extends State<InvitationDialog> {
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<InvitationDialog> {
_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<InvitationDialog> {
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<InvitationDialog> {
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);

View File

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

View File

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

View File

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

View File

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

View File

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