invitation work

This commit is contained in:
Christien Rioux 2023-08-04 01:00:38 -04:00
parent 95ed8b28a0
commit 7496a1a2a7
22 changed files with 591 additions and 47 deletions

View file

@ -1,14 +1,17 @@
import 'dart:async';
import 'dart:ffi';
import 'package:archive/archive.dart';
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:basic_utils/basic_utils.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:qr_flutter/qr_flutter.dart';
import '../tools/tools.dart';
import '../veilid_support/veilid_support.dart';
class ContactInvitationDisplayDialog extends ConsumerStatefulWidget {
const ContactInvitationDisplayDialog({
@ -40,14 +43,14 @@ class ContactInvitationDisplayDialogState
extends ConsumerState<ContactInvitationDisplayDialog> {
final focusNode = FocusNode();
final formKey = GlobalKey<FormState>();
late final FutureProvider<Uint8List?> _generateFutureProvider;
late final AutoDisposeFutureProvider<Uint8List?> _generateFutureProvider;
@override
void initState() {
super.initState();
_generateFutureProvider =
FutureProvider<Uint8List>((ref) async => widget.generator);
AutoDisposeFutureProvider<Uint8List>((ref) async => widget.generator);
}
@override
@ -56,11 +59,24 @@ class ContactInvitationDisplayDialogState
super.dispose();
}
String makeTextInvite(String message, Uint8List data) {
final invite = StringUtils.addCharAtPosition(
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';
}
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
final textTheme = theme.textTheme;
final signedContactInvitationBytesV = ref.watch(_generateFutureProvider);
final cardsize = MediaQuery.of(context).size.shortestSide - 24;
@ -71,17 +87,62 @@ class ContactInvitationDisplayDialogState
height: cardsize,
child: signedContactInvitationBytesV.when(
loading: () => waitingPage(context),
data: (data) => Form(
key: formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [Text("Contact Invitation")])),
data: (data) {
if (data == null) {
Navigator.of(context).pop();
return const Text('');
}
final compressedData =
Uint8List.fromList(BZip2Encoder().encode(data));
return Form(
key: formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
FittedBox(
fit: BoxFit.scaleDown,
child: Text(
translate(
'send_invite_dialog.contact_invitation'),
style: textTheme.headlineMedium!
.copyWith(color: Colors.black))),
ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 300, minHeight: 300),
child: QrImageView.withQr(
size: 300,
qr: QrCode.fromUint8List(
data: compressedData,
errorCorrectLevel:
QrErrorCorrectLevel.L))),
FittedBox(
fit: BoxFit.scaleDown,
child: Text(widget.message,
softWrap: true,
style: textTheme.headlineSmall!
.copyWith(color: Colors.black))),
ElevatedButton.icon(
icon: const Icon(Icons.copy),
label: Text(translate(
'send_invite_dialog.copy_invitation')),
onPressed: () async {
showInfoToast(
context,
translate(
'send_invite_dialog.invitation_copied'));
await Clipboard.setData(ClipboardData(
text: makeTextInvite(
widget.message, compressedData)));
},
),
]));
},
error: (e, s) {
Navigator.of(context).pop();
showErrorToast(
context, "Failed to generate contact invitation: $e");
return Text("");
showErrorToast(context,
translate('send_invite_dialog.failed_to_generate'));
return const Text('');
})));
}

View file

@ -0,0 +1,79 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:veilid/veilid.dart';
import '../../entities/proto.dart' as proto;
class ContactInvitationItemWidget extends ConsumerWidget {
const ContactInvitationItemWidget(
{required this.contactInvitationRecord, super.key});
final proto.ContactInvitationRecord contactInvitationRecord;
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context, WidgetRef ref) {
return Slidable(
// Specify a key if the Slidable is dismissible.
key: ObjectKey(contactInvitationRecord),
// The start action pane is the one at the left or the top side.
startActionPane: ActionPane(
// A motion is a widget used to control how the pane animates.
motion: const DrawerMotion(),
// A pane can dismiss the Slidable.
//dismissible: DismissiblePane(onDismissed: () {}),
// All actions are defined in the children parameter.
children: [
// A SlidableAction can have an icon and/or a label.
SlidableAction(
onPressed: (context) => (),
backgroundColor: Color(0xFFFE4A49),
foregroundColor: Colors.white,
icon: Icons.delete,
label: 'Delete',
),
SlidableAction(
onPressed: (context) => (),
backgroundColor: Color(0xFF21B7CA),
foregroundColor: Colors.white,
icon: Icons.edit,
label: 'Edit',
),
],
),
// The end action pane is the one at the right or the bottom side.
// endActionPane: ActionPane(
// motion: const DrawerMotion(),
// children: [
// SlidableAction(
// // An action can be bigger than the others.
// flex: 2,
// onPressed: (context) => (),
// backgroundColor: Color(0xFF7BC043),
// foregroundColor: Colors.white,
// icon: Icons.archive,
// label: 'Archive',
// ),
// SlidableAction(
// onPressed: (context) => (),
// backgroundColor: Color(0xFF0392CF),
// foregroundColor: Colors.white,
// icon: Icons.save,
// label: 'Save',
// ),
// ],
// ),
// The child of the Slidable is what the user sees when the
// component is not dragged.
child: ListTile(
title: Text(translate('contact_list.invitation')),
subtitle: Text(contactInvitationRecord.message),
//Text(Timestamp.fromInt64(contactInvitationRecord.expiration) / ),
leading: Icon(Icons.person_add)));
}
}

View file

@ -0,0 +1,56 @@
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:searchable_listview/searchable_listview.dart';
import '../../entities/proto.dart' as proto;
import '../tools/tools.dart';
import 'contact_invitation_item_widget.dart';
import 'contact_item_widget.dart';
import 'empty_contact_list_widget.dart';
class ContactInvitationListWidget extends ConsumerWidget {
const ContactInvitationListWidget(
{required this.contactInvitationRecordList, super.key});
final IList<proto.ContactInvitationRecord> contactInvitationRecordList;
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
final textTheme = theme.textTheme;
//final scale = theme.extension<ScaleScheme>()!;
return Container(
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Contacts',
style: textTheme.bodyMedium,
),
ListView.builder(itemBuilder: (context, index) {
if (index < 0 || index >= contactInvitationRecordList.length) {
return null;
}
return ContactInvitationItemWidget(
contactInvitationRecord: contactInvitationRecordList[index],
key: ObjectKey(contactInvitationRecordList[index]));
}, findChildIndexCallback: (key) {
final index = contactInvitationRecordList.indexOf(
(key as ObjectKey).value! as proto.ContactInvitationRecord);
if (index == -1) {
return null;
}
return index;
})
],
),
);
}
}

View file

@ -0,0 +1,75 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import '../../entities/proto.dart' as proto;
class ContactItemWidget extends ConsumerWidget {
const ContactItemWidget({required this.contact, super.key});
final proto.Contact contact;
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context, WidgetRef ref) {
return Slidable(
// Specify a key if the Slidable is dismissible.
key: ObjectKey(contact),
// The start action pane is the one at the left or the top side.
startActionPane: ActionPane(
// A motion is a widget used to control how the pane animates.
motion: const DrawerMotion(),
// A pane can dismiss the Slidable.
//dismissible: DismissiblePane(onDismissed: () {}),
// All actions are defined in the children parameter.
children: [
// A SlidableAction can have an icon and/or a label.
SlidableAction(
onPressed: (context) => (),
backgroundColor: Color(0xFFFE4A49),
foregroundColor: Colors.white,
icon: Icons.delete,
label: 'Delete',
),
SlidableAction(
onPressed: (context) => (),
backgroundColor: Color(0xFF21B7CA),
foregroundColor: Colors.white,
icon: Icons.edit,
label: 'Edit',
),
],
),
// The end action pane is the one at the right or the bottom side.
// endActionPane: ActionPane(
// motion: const DrawerMotion(),
// children: [
// SlidableAction(
// // An action can be bigger than the others.
// flex: 2,
// onPressed: (context) => (),
// backgroundColor: Color(0xFF7BC043),
// foregroundColor: Colors.white,
// icon: Icons.archive,
// label: 'Archive',
// ),
// SlidableAction(
// onPressed: (context) => (),
// backgroundColor: Color(0xFF0392CF),
// foregroundColor: Colors.white,
// icon: Icons.save,
// label: 'Save',
// ),
// ],
// ),
// The child of the Slidable is what the user sees when the
// component is not dragged.
child: ListTile(
title: Text(contact.editedProfile.name),
subtitle: Text(contact.editedProfile.title),
leading: Icon(Icons.person)));
}
}

View file

@ -1,20 +1,25 @@
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:searchable_listview/searchable_listview.dart';
import '../../entities/proto.dart' as proto;
import '../tools/tools.dart';
import 'contact_item_widget.dart';
import 'empty_contact_list_widget.dart';
class ContactListWidget extends ConsumerWidget {
const ContactListWidget({required this.contactList, super.key});
final List<proto.Contact> contactList;
final IList<proto.Contact> contactList;
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context, WidgetRef ref) {
//
if (contactList.isEmpty) {
return const EmptyContactListWidget();
}
final theme = Theme.of(context);
final textTheme = theme.textTheme;
//final scale = theme.extension<ScaleScheme>()!;
return Container(
decoration: BoxDecoration(
@ -23,16 +28,36 @@ class ContactListWidget extends ConsumerWidget {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.group_add,
color: Theme.of(context).disabledColor,
size: 48,
),
Text(
'Contacts',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).disabledColor,
style: textTheme.bodyMedium,
),
SearchableList<proto.Contact>(
initialList: contactList.toList(),
builder: (contact) => ContactItemWidget(contact: contact),
filter: (value) {
final lowerValue = value.toLowerCase();
return contactList
.where((element) =>
element.editedProfile.name
.toLowerCase()
.contains(lowerValue) ||
element.editedProfile.title
.toLowerCase()
.contains(lowerValue))
.toList();
},
emptyWidget: const EmptyContactListWidget(),
inputDecoration: InputDecoration(
labelText: translate('contact_list.search'),
fillColor: Colors.white,
focusedBorder: OutlineInputBorder(
borderSide: const BorderSide(
color: Colors.blue,
),
borderRadius: BorderRadius.circular(10),
),
),
),
],
),

View file

@ -104,6 +104,7 @@ class SendInviteDialogState extends ConsumerState<SendInviteDialog> {
activeAccountInfo: activeAccountInfo,
encryptionKeyType: _encryptionKeyType,
encryptionKey: _encryptionKey,
message: _messageTextController.text,
expiration: _expiration);
// ignore: use_build_context_synchronously
if (!context.mounted) {
@ -119,6 +120,7 @@ class SendInviteDialogState extends ConsumerState<SendInviteDialog> {
// if (ret == null) {
// return;
// }
ref.invalidate(fetchContactInvitationRecordsProvider);
navigator.pop();
}