layout work

This commit is contained in:
Christien Rioux 2023-09-23 12:56:54 -04:00
parent accd79c82d
commit 95e5306eb3
11 changed files with 657 additions and 216 deletions

View file

@ -33,17 +33,23 @@ class ContactInvitationListWidgetState
return Container(
width: double.infinity,
constraints: const BoxConstraints(minHeight: 64, maxHeight: 200),
decoration: ShapeDecoration(
color: scale.primaryScale.subtleBorder,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
)),
constraints: const BoxConstraints(maxHeight: 200),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: double.infinity,
decoration: ShapeDecoration(
color: scale.grayScale.appBackground,
color: scale.primaryScale.subtleBackground,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
)),
borderRadius: BorderRadius.circular(16),
side: BorderSide(
color: scale.primaryScale.subtleBorder, width: 4))),
child: ListView.builder(
controller: _scrollController,
itemCount: widget.contactInvitationRecordList.length,
@ -69,7 +75,7 @@ class ContactInvitationListWidgetState
return index;
},
shrinkWrap: true,
)).paddingLTRB(8, 0, 8, 8).flexible()
))
],
),
);

View file

@ -30,21 +30,28 @@ class ContactListWidget extends ConsumerWidget {
return Container(
width: double.infinity,
decoration: ShapeDecoration(
color: scale.primaryScale.subtleBorder,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
)),
constraints: const BoxConstraints(
minHeight: 64,
),
child: Column(children: [
Text(
'Contacts',
style: textTheme.bodyLarge,
).paddingAll(8),
translate('contact_list.title'),
style: textTheme.titleMedium!
.copyWith(color: scale.primaryScale.subtleText),
).paddingLTRB(4, 4, 4, 0),
Container(
width: double.infinity,
decoration: ShapeDecoration(
color: scale.grayScale.appBackground,
color: scale.primaryScale.subtleBackground,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
)),
borderRadius: BorderRadius.circular(16),
side: BorderSide(
color: scale.primaryScale.subtleBorder, width: 4))),
child: (contactList.isEmpty)
? const EmptyContactListWidget().toCenter()
: SearchableList<proto.Contact>(
@ -76,6 +83,6 @@ class ContactListWidget extends ConsumerWidget {
),
).expanded()
]),
).paddingLTRB(8, 0, 8, 65);
).paddingLTRB(8, 0, 8, 8);
}
}

View file

@ -19,13 +19,13 @@ class EmptyContactListWidget extends ConsumerWidget {
children: [
Icon(
Icons.person_add_sharp,
color: scale.primaryScale.border,
color: scale.primaryScale.subtleBorder,
size: 48,
),
Text(
translate('contact_list.invite_people'),
style: textTheme.bodyMedium?.copyWith(
color: scale.primaryScale.border,
color: scale.primaryScale.subtleBorder,
),
),
],

View file

@ -21,6 +21,41 @@ class PasteInviteDialog extends ConsumerStatefulWidget {
@override
PasteInviteDialogState createState() => PasteInviteDialogState();
static Future<void> show(BuildContext context) async {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
final textTheme = theme.textTheme;
await showDialog<void>(
context: context,
// ignore: prefer_expression_function_bodies
builder: (context) {
return AlertDialog(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: const BorderRadius.all(Radius.circular(16)),
side: BorderSide(width: 4, color: scale.primaryScale.border),
),
contentPadding: EdgeInsets.zero,
backgroundColor: scale.primaryScale.border,
title: Text(
translate('paste_invite_dialog.title'),
style: textTheme.titleMedium,
textAlign: TextAlign.center,
),
titlePadding: EdgeInsets.fromLTRB(4, 4, 4, 0),
content: DecoratedBox(
decoration: ShapeDecoration(
color: scale.primaryScale.subtleBackground,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(
width: 4, color: scale.primaryScale.border),
)),
child: const PasteInviteDialog().paddingAll(4)));
});
}
}
class PasteInviteDialogState extends ConsumerState<PasteInviteDialog> {
@ -240,16 +275,16 @@ class PasteInviteDialogState extends ConsumerState<PasteInviteDialog> {
return SizedBox(height: 400, child: waitingPage(context));
}
return ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 400),
constraints: const BoxConstraints(maxHeight: 400, maxWidth: 400),
child: SingleChildScrollView(
padding: const EdgeInsets.all(8),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
translate('paste_invite_dialog.paste_invite_here'),
).paddingAll(8),
).paddingLTRB(0, 0, 0, 8),
Container(
constraints: const BoxConstraints(maxHeight: 200),
child: TextField(
@ -267,12 +302,13 @@ class PasteInviteDialogState extends ConsumerState<PasteInviteDialog> {
'---- END VEILIDCHAT CONTACT INVITE -----\n',
//labelText: translate('paste_invite_dialog.paste')
),
).paddingAll(8)),
)).paddingLTRB(0, 0, 0, 8),
if (_validatingPaste)
Column(children: [
Text(translate('paste_invite_dialog.validating')),
Text(translate('paste_invite_dialog.validating'))
.paddingLTRB(0, 0, 0, 8),
buildProgressIndicator(context),
]),
]).paddingAll(16).toCenter(),
if (_validInvitation == null &&
!_validatingPaste &&
_pasteTextController.text.isNotEmpty)
@ -282,10 +318,15 @@ class PasteInviteDialogState extends ConsumerState<PasteInviteDialog> {
]).paddingAll(16).toCenter(),
if (_validInvitation != null && !_validatingPaste)
Column(children: [
ProfileWidget(
name: _validInvitation!.contactRequestPrivate.profile.name,
title:
_validInvitation!.contactRequestPrivate.profile.title),
Container(
constraints: const BoxConstraints(maxHeight: 64),
width: double.infinity,
child: ProfileWidget(
name: _validInvitation!
.contactRequestPrivate.profile.name,
title: _validInvitation!
.contactRequestPrivate.profile.title))
.paddingLTRB(0, 0, 0, 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [

View file

@ -24,26 +24,23 @@ class ProfileWidget extends ConsumerWidget {
final scale = theme.extension<ScaleScheme>()!;
final textTheme = theme.textTheme;
return Container(
width: double.infinity,
decoration: ShapeDecoration(
color: scale.primaryScale.subtleBackground,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: scale.primaryScale.border))),
child: Row(children: [
Column(mainAxisSize: MainAxisSize.min, children: [
Text(name, style: textTheme.headlineSmall).paddingAll(8),
if (title != null && title!.isNotEmpty)
Text(title!, style: textTheme.bodyMedium).paddingLTRB(8, 0, 8, 8),
]).expanded(),
IconButton(
icon: const Icon(Icons.settings),
tooltip: translate('app_bar.settings_tooltip'),
onPressed: () async {
context.go('/home/settings');
})
])).paddingAll(8);
return DecoratedBox(
decoration: ShapeDecoration(
color: scale.primaryScale.subtleBorder,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(
width: 0, color: scale.primaryScale.subtleBorder))),
child: Column(children: [
Text(
name,
style: textTheme.headlineSmall,
textAlign: TextAlign.left,
).paddingAll(4),
if (title != null && title!.isNotEmpty)
Text(title!, style: textTheme.bodyMedium).paddingLTRB(4, 0, 4, 4),
]),
);
}
@override

View file

@ -0,0 +1,315 @@
import 'dart:async';
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:quickalert/quickalert.dart';
import '../entities/local_account.dart';
import '../providers/account.dart';
import '../providers/contact.dart';
import '../providers/contact_invite.dart';
import '../tools/tools.dart';
import '../veilid_support/veilid_support.dart';
import 'enter_pin.dart';
import 'profile_widget.dart';
class ScanInviteDialog extends ConsumerStatefulWidget {
const ScanInviteDialog({super.key});
@override
ScanInviteDialogState createState() => ScanInviteDialogState();
}
class ScanInviteDialogState extends ConsumerState<ScanInviteDialog> {
final _pasteTextController = TextEditingController();
EncryptionKeyType _encryptionKeyType = EncryptionKeyType.none;
String _encryptionKey = '';
Timestamp? _expiration;
ValidContactInvitation? _validInvitation;
bool _validatingPaste = false;
bool _isAccepting = false;
@override
void initState() {
super.initState();
}
// Future<void> _onNoneEncryptionSelected(bool selected) async {
// setState(() {
// if (selected) {
// _encryptionKeyType = EncryptionKeyType.none;
// }
// });
// }
// Future<void> _onPinEncryptionSelected(bool selected) async {
// final description = translate('receive_invite_dialog.pin_description');
// final pin = await showDialog<String>(
// context: context,
// builder: (context) => EnterPinDialog(description: description));
// if (pin == null) {
// return;
// }
// // ignore: use_build_context_synchronously
// if (!context.mounted) {
// return;
// }
// final matchpin = await showDialog<String>(
// context: context,
// builder: (context) => EnterPinDialog(
// matchPin: pin,
// description: description,
// ));
// if (matchpin == null) {
// return;
// } else if (pin == matchpin) {
// setState(() {
// _encryptionKeyType = EncryptionKeyType.pin;
// _encryptionKey = pin;
// });
// } else {
// // ignore: use_build_context_synchronously
// if (!context.mounted) {
// return;
// }
// showErrorToast(
// context, translate('receive_invite_dialog.pin_does_not_match'));
// setState(() {
// _encryptionKeyType = EncryptionKeyType.none;
// _encryptionKey = '';
// });
// }
// }
// Future<void> _onPasswordEncryptionSelected(bool selected) async {
// setState(() {
// if (selected) {
// _encryptionKeyType = EncryptionKeyType.password;
// }
// });
// }
Future<void> _onAccept() async {
final navigator = Navigator.of(context);
setState(() {
_isAccepting = true;
});
final activeAccountInfo = await ref.read(fetchActiveAccountProvider.future);
if (activeAccountInfo == null) {
setState(() {
_isAccepting = false;
});
navigator.pop();
return;
}
final validInvitation = _validInvitation;
if (validInvitation != null) {
final acceptedContact =
await acceptContactInvitation(activeAccountInfo, validInvitation);
if (acceptedContact != null) {
await createContact(
activeAccountInfo: activeAccountInfo,
profile: acceptedContact.profile,
remoteIdentity: acceptedContact.remoteIdentity,
remoteConversationRecordKey:
acceptedContact.remoteConversationRecordKey,
localConversationRecordKey:
acceptedContact.localConversationRecordKey,
);
ref
..invalidate(fetchContactInvitationRecordsProvider)
..invalidate(fetchContactListProvider);
} else {
if (context.mounted) {
showErrorToast(context, 'paste_invite_dialog.failed_to_accept');
}
}
}
setState(() {
_isAccepting = false;
});
navigator.pop();
}
Future<void> _onReject() async {
final navigator = Navigator.of(context);
setState(() {
_isAccepting = true;
});
final activeAccountInfo = await ref.read(fetchActiveAccountProvider.future);
if (activeAccountInfo == null) {
setState(() {
_isAccepting = false;
});
navigator.pop();
return;
}
final validInvitation = _validInvitation;
if (validInvitation != null) {
if (await rejectContactInvitation(activeAccountInfo, validInvitation)) {
// do nothing right now
} else {
if (context.mounted) {
showErrorToast(context, 'paste_invite_dialog.failed_to_reject');
}
}
}
setState(() {
_isAccepting = false;
});
navigator.pop();
}
Future<void> _onPasteChanged(String text) async {
try {
final lines = text.split('\n');
if (lines.isEmpty) {
setState(() {
_validatingPaste = false;
_validInvitation = null;
});
return;
}
var firstline =
lines.indexWhere((element) => element.contains('BEGIN VEILIDCHAT'));
firstline += 1;
var lastline =
lines.indexWhere((element) => element.contains('END VEILIDCHAT'));
if (lastline == -1) {
lastline = lines.length;
}
if (lastline <= firstline) {
setState(() {
_validatingPaste = false;
_validInvitation = null;
});
return;
}
final inviteDataBase64 = lines.sublist(firstline, lastline).join();
final inviteData = base64UrlNoPadDecode(inviteDataBase64);
setState(() {
_validatingPaste = true;
_validInvitation = null;
});
final validatedContactInvitation = await validateContactInvitation(
inviteData, (encryptionKeyType, encryptedSecret) async {
switch (encryptionKeyType) {
case EncryptionKeyType.none:
return SecretKey.fromBytes(encryptedSecret);
case EncryptionKeyType.pin:
//xxx
return SecretKey.fromBytes(encryptedSecret);
case EncryptionKeyType.password:
//xxx
return SecretKey.fromBytes(encryptedSecret);
}
});
// Verify expiration
// xxx
setState(() {
_validatingPaste = false;
_validInvitation = validatedContactInvitation;
});
} on Exception catch (_) {
setState(() {
_validatingPaste = false;
_validInvitation = null;
});
}
}
@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 height = MediaQuery.of(context).size.height;
if (_isAccepting) {
return SizedBox(height: 400, child: waitingPage(context));
}
return ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 400),
child: SingleChildScrollView(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
translate('paste_invite_dialog.paste_invite_here'),
).paddingAll(8),
Container(
constraints: const BoxConstraints(maxHeight: 200),
child: TextField(
enabled: !_validatingPaste,
onChanged: _onPasteChanged,
style: textTheme.labelSmall!
.copyWith(fontFamily: 'Victor Mono', fontSize: 11),
keyboardType: TextInputType.multiline,
maxLines: null,
controller: _pasteTextController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText: '--- BEGIN VEILIDCHAT CONTACT INVITE ----\n'
'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n'
'---- END VEILIDCHAT CONTACT INVITE -----\n',
//labelText: translate('paste_invite_dialog.paste')
),
).paddingAll(8)),
if (_validatingPaste)
Column(children: [
Text(translate('paste_invite_dialog.validating')),
buildProgressIndicator(context),
]),
if (_validInvitation == null &&
!_validatingPaste &&
_pasteTextController.text.isNotEmpty)
Column(children: [
Text(translate('paste_invite_dialog.invalid_invitation')),
const Icon(Icons.error)
]).paddingAll(16).toCenter(),
if (_validInvitation != null && !_validatingPaste)
Column(children: [
ProfileWidget(
name: _validInvitation!.contactRequestPrivate.profile.name,
title:
_validInvitation!.contactRequestPrivate.profile.title),
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,
)
],
),
])
],
),
),
);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
}
}

View file

@ -20,6 +20,26 @@ class SendInviteDialog extends ConsumerStatefulWidget {
@override
SendInviteDialogState createState() => SendInviteDialogState();
static Future<void> show(BuildContext context) async {
await showDialog<void>(
context: context,
// ignore: prefer_expression_function_bodies
builder: (context) {
return AlertDialog(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(20)),
),
contentPadding: const EdgeInsets.only(
top: 10,
),
title: Text(
translate('send_invite_dialog.title'),
style: const TextStyle(fontSize: 24),
),
content: const SendInviteDialog());
});
}
}
class SendInviteDialogState extends ConsumerState<SendInviteDialog> {
@ -161,7 +181,7 @@ class SendInviteDialogState extends ConsumerState<SendInviteDialog> {
onSelected: _onNoneEncryptionSelected,
),
ChoiceChip(
label: Text(translate('send_invite_dialog.numeric_pin')),
label: Text(translate('send_invite_dialog.pin')),
selected: _encryptionKeyType == EncryptionKeyType.pin,
onSelected: _onPinEncryptionSelected,
),