accept via paste

This commit is contained in:
Christien Rioux 2023-08-05 01:00:46 -04:00
parent 7496a1a2a7
commit 8bb8285e50
27 changed files with 829 additions and 324 deletions

View file

@ -81,52 +81,48 @@ class ChatComponentState extends ConsumerState<ChatComponent> {
return DefaultTextStyle(
style: textTheme.bodySmall!,
child: Align(
alignment: AlignmentDirectional.centerEnd,
child: Container(
decoration: BoxDecoration(
color: scale.primaryScale.appBackground,
),
child: Stack(
alignment: AlignmentDirectional.centerEnd,
child: Stack(
children: [
Column(
children: [
Column(
children: [
Container(
height: 48,
decoration: BoxDecoration(
color: scale.primaryScale.subtleBackground,
),
child: Align(
alignment: AlignmentDirectional.centerStart,
child: Padding(
padding: const EdgeInsetsDirectional.fromSTEB(
16, 0, 16, 0),
child: Text("current contact",
textAlign: TextAlign.start,
style: textTheme.titleMedium),
),
),
Container(
height: 48,
decoration: BoxDecoration(
color: scale.primaryScale.subtleBackground,
),
child: Align(
alignment: AlignmentDirectional.centerStart,
child: Padding(
padding:
const EdgeInsetsDirectional.fromSTEB(16, 0, 16, 0),
child: Text("current contact",
textAlign: TextAlign.start,
style: textTheme.titleMedium),
),
Expanded(
child: Container(
decoration: const BoxDecoration(),
child: Chat(
theme: chatTheme,
messages: _messages,
//onAttachmentPressed: _handleAttachmentPressed,
//onMessageTap: _handleMessageTap,
//onPreviewDataFetched: _handlePreviewDataFetched,
),
),
Expanded(
child: Container(
decoration: const BoxDecoration(),
child: Chat(
theme: chatTheme,
messages: _messages,
//onAttachmentPressed: _handleAttachmentPressed,
//onMessageTap: _handleMessageTap,
//onPreviewDataFetched: _handlePreviewDataFetched,
onSendPressed: _handleSendPressed,
showUserAvatars: true,
showUserNames: true,
user: _user,
),
),
onSendPressed: _handleSendPressed,
showUserAvatars: true,
showUserNames: true,
user: _user,
),
],
),
),
],
),
)));
],
),
));
}
}

View file

@ -23,7 +23,7 @@ class ContactInvitationDisplayDialog extends ConsumerStatefulWidget {
final String name;
final String message;
final Future<Uint8List> generator;
final FutureOr<Uint8List> generator;
@override
ContactInvitationDisplayDialogState createState() =>
@ -35,7 +35,7 @@ class ContactInvitationDisplayDialog extends ConsumerStatefulWidget {
properties
..add(StringProperty('name', name))
..add(StringProperty('message', message))
..add(DiagnosticsProperty<Future<Uint8List>?>('generator', generator));
..add(DiagnosticsProperty<FutureOr<Uint8List>?>('generator', generator));
}
}
@ -65,9 +65,9 @@ class ContactInvitationDisplayDialogState
repeat: true);
final msg = message.isNotEmpty ? '$message\n' : '';
return '$msg'
'--- BEGIN VEILIDCHAT CONTACT INVITE ---\n'
'--- BEGIN VEILIDCHAT CONTACT INVITE ----\n'
'$invite\n'
'--- END VEILIDCHAT CONTACT INVITE ---\n';
'---- END VEILIDCHAT CONTACT INVITE -----\n';
}
@override
@ -92,8 +92,6 @@ class ContactInvitationDisplayDialogState
Navigator.of(context).pop();
return const Text('');
}
final compressedData =
Uint8List.fromList(BZip2Encoder().encode(data));
return Form(
key: formKey,
child: Column(
@ -113,7 +111,7 @@ class ContactInvitationDisplayDialogState
child: QrImageView.withQr(
size: 300,
qr: QrCode.fromUint8List(
data: compressedData,
data: data,
errorCorrectLevel:
QrErrorCorrectLevel.L))),
FittedBox(
@ -132,8 +130,8 @@ class ContactInvitationDisplayDialogState
translate(
'send_invite_dialog.invitation_copied'));
await Clipboard.setData(ClipboardData(
text: makeTextInvite(
widget.message, compressedData)));
text:
makeTextInvite(widget.message, data)));
},
),
]));

View file

@ -1,9 +1,14 @@
import 'package:flutter/foundation.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:veilid/veilid.dart';
import '../../entities/proto.dart' as proto;
import '../providers/account.dart';
import '../providers/contact.dart';
import '../tools/tools.dart';
import 'contact_invitation_display.dart';
class ContactInvitationItemWidget extends ConsumerWidget {
const ContactInvitationItemWidget(
@ -11,69 +16,110 @@ class ContactInvitationItemWidget extends ConsumerWidget {
final proto.ContactInvitationRecord contactInvitationRecord;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<proto.ContactInvitationRecord>(
'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(),
final theme = Theme.of(context);
final textTheme = theme.textTheme;
final scale = theme.extension<ScaleScheme>()!;
// A pane can dismiss the Slidable.
//dismissible: DismissiblePane(onDismissed: () {}),
return Container(
margin: const EdgeInsets.fromLTRB(4, 4, 4, 0),
clipBehavior: Clip.antiAlias,
decoration: ShapeDecoration(
color: scale.tertiaryScale.subtleBackground,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
)),
child: Slidable(
// Specify a key if the Slidable is dismissible.
key: ObjectKey(contactInvitationRecord),
endActionPane: ActionPane(
// A motion is a widget used to control how the pane animates.
motion: const DrawerMotion(),
// 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',
// 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) async {
final activeAccountInfo =
await ref.read(fetchActiveAccountProvider.future);
if (activeAccountInfo != null) {
await deleteContactInvitation(
activeAccountInfo: activeAccountInfo,
contactInvitationRecord: contactInvitationRecord);
ref.invalidate(fetchContactInvitationRecordsProvider);
}
},
backgroundColor: scale.tertiaryScale.background,
foregroundColor: scale.tertiaryScale.text,
icon: Icons.delete,
label: translate('button.delete'),
padding: const EdgeInsets.all(2)),
],
),
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',
// ),
// ],
// ),
// startActionPane: 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)));
// The child of the Slidable is what the user sees when the
// component is not dragged.
child: ListTile(
//title: Text(translate('contact_list.invitation')),
onTap: () async {
final activeAccountInfo =
await ref.read(fetchActiveAccountProvider.future);
if (activeAccountInfo != null) {
// ignore: use_build_context_synchronously
if (!context.mounted) {
return;
}
await showDialog<void>(
context: context,
builder: (context) => ContactInvitationDisplayDialog(
name: activeAccountInfo.localAccount.name,
message: contactInvitationRecord.message,
generator: Uint8List.fromList(
contactInvitationRecord.invitation),
));
}
},
subtitle: Text(contactInvitationRecord.message.isEmpty
? translate('contact_list.invitation')
: contactInvitationRecord.message),
iconColor: scale.tertiaryScale.background,
textColor: scale.tertiaryScale.text,
//Text(Timestamp.fromInt64(contactInvitationRecord.expiration) / ),
leading: const Icon(Icons.person_add))));
}
}

View file

@ -1,54 +1,75 @@
import 'package:awesome_extensions/awesome_extensions.dart';
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});
class ContactInvitationListWidget extends ConsumerStatefulWidget {
ContactInvitationListWidget({
required this.contactInvitationRecordList,
super.key,
});
final IList<proto.ContactInvitationRecord> contactInvitationRecordList;
@override
ContactInvitationListWidgetState createState() =>
ContactInvitationListWidgetState();
}
class ContactInvitationListWidgetState
extends ConsumerState<ContactInvitationListWidget> {
final ScrollController _scrollController = ScrollController();
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context, WidgetRef ref) {
Widget build(BuildContext context) {
final theme = Theme.of(context);
final textTheme = theme.textTheme;
//final scale = theme.extension<ScaleScheme>()!;
//final textTheme = theme.textTheme;
final scale = theme.extension<ScaleScheme>()!;
return Container(
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
),
width: double.infinity,
constraints: const BoxConstraints(minHeight: 64, maxHeight: 200),
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;
})
Container(
width: double.infinity,
decoration: ShapeDecoration(
color: scale.grayScale.appBackground,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
)),
child: ListView.builder(
controller: _scrollController,
itemCount: widget.contactInvitationRecordList.length,
itemBuilder: (context, index) {
if (index < 0 ||
index >= widget.contactInvitationRecordList.length) {
return null;
}
return ContactInvitationItemWidget(
contactInvitationRecord:
widget.contactInvitationRecordList[index],
key: ObjectKey(
widget.contactInvitationRecordList[index]))
.paddingAll(2);
},
findChildIndexCallback: (key) {
final index = widget.contactInvitationRecordList.indexOf(
(key as ObjectKey).value!
as proto.ContactInvitationRecord);
if (index == -1) {
return null;
}
return index;
},
shrinkWrap: true,
)).paddingLTRB(8, 0, 8, 8).flexible()
],
),
);

View file

@ -1,7 +1,7 @@
import 'package:awesome_extensions/awesome_extensions.dart';
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';
@ -19,48 +19,55 @@ class ContactListWidget extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
final textTheme = theme.textTheme;
//final scale = theme.extension<ScaleScheme>()!;
final scale = theme.extension<ScaleScheme>()!;
return Container(
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
width: double.infinity,
constraints: const BoxConstraints(
minHeight: 64,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Contacts',
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,
child: Column(children: [
Text(
'Contacts',
style: textTheme.bodyLarge,
).paddingAll(8),
Container(
width: double.infinity,
decoration: ShapeDecoration(
color: scale.grayScale.appBackground,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
)),
child: (contactList.isEmpty)
? const EmptyContactListWidget().toCenter()
: 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();
},
inputDecoration: InputDecoration(
labelText: translate('contact_list.search'),
fillColor: Colors.white,
focusedBorder: OutlineInputBorder(
borderSide: const BorderSide(
color: Colors.blue,
),
borderRadius: BorderRadius.circular(10),
),
),
),
borderRadius: BorderRadius.circular(10),
),
),
),
],
),
);
).expanded()
]),
).paddingLTRB(8, 0, 8, 65);
}
}

View file

@ -2,33 +2,33 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_translate/flutter_translate.dart';
import '../tools/tools.dart';
class EmptyContactListWidget extends ConsumerWidget {
const EmptyContactListWidget({super.key});
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context, WidgetRef ref) {
//
return Container(
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.person_add_sharp,
color: Theme.of(context).disabledColor,
size: 48,
final theme = Theme.of(context);
final textTheme = theme.textTheme;
final scale = theme.extension<ScaleScheme>()!;
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.person_add_sharp,
color: scale.primaryScale.border,
size: 48,
),
Text(
translate('empty_contact_list.invite_people'),
style: textTheme.bodyMedium?.copyWith(
color: scale.primaryScale.border,
),
Text(
translate('empty_contact_list.invite_people'),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).disabledColor,
),
),
],
),
),
],
);
}
}

View file

@ -0,0 +1,251 @@
import 'dart:async';
import 'dart:typed_data';
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 '../entities/proto.dart' as proto;
import '../providers/account.dart';
import '../providers/contact.dart';
import '../tools/tools.dart';
import '../veilid_support/veilid_support.dart';
import 'contact_invitation_display.dart';
import 'enter_pin.dart';
class PasteInviteDialog extends ConsumerStatefulWidget {
const PasteInviteDialog({super.key});
@override
PasteInviteDialogState createState() => PasteInviteDialogState();
}
class PasteInviteDialogState extends ConsumerState<PasteInviteDialog> {
final _pasteTextController = TextEditingController();
final _messageTextController = TextEditingController();
EncryptionKeyType _encryptionKeyType = EncryptionKeyType.none;
String _encryptionKey = '';
Timestamp? _expiration;
proto.SignedContactInvitation? _validInvitation;
@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> _onPasteChanged(String text) async {
try {
final lines = text.split('\n');
if (lines.isEmpty) {
_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) {
_validInvitation = null;
return;
}
final inviteDataBase64 = lines.sublist(firstline, lastline).join();
final inviteData = base64UrlNoPadDecode(inviteDataBase64);
final signedContactInvitation =
proto.SignedContactInvitation.fromBuffer(inviteData);
final contactInvitationBytes =
Uint8List.fromList(signedContactInvitation.contactInvitation);
final contactInvitation =
proto.ContactInvitation.fromBuffer(contactInvitationBytes);
final contactRequestInboxKey = proto.TypedKeyProto.fromProto(
contactInvitation.contactRequestInboxKey);
// Open context request inbox subkey zero to get the contact request object
final pool = await DHTRecordPool.instance();
await (await pool.openRead(contactRequestInboxKey))
.deleteScope((contactRequestInbox) async {
//
final contactRequest = await contactRequestInbox
.getProtobuf(proto.ContactRequest.fromBuffer);
// Decrypt contact request private
final encryptionKeyType =
EncryptionKeyType.fromProto(contactRequest!.encryptionKeyType);
late final SecretKey writerSecret;
switch (encryptionKeyType) {
case EncryptionKeyType.none:
writerSecret = SecretKey.fromBytes(
Uint8List.fromList(contactInvitation.writerSecret));
case EncryptionKeyType.pin:
//
case EncryptionKeyType.password:
//
}
final cs =
await pool.veilid.getCryptoSystem(contactRequestInboxKey.kind);
final contactRequestPrivateBytes = await cs.decryptNoAuthWithNonce(
Uint8List.fromList(contactRequest.private), writerSecret);
final contactRequestPrivate =
proto.ContactRequestPrivate.fromBuffer(contactRequestPrivateBytes);
final contactIdentityMasterRecordKey = proto.TypedKeyProto.fromProto(
contactRequestPrivate.identityMasterRecordKey);
// Fetch the account master
final contactIdentityMaster = await openIdentityMaster(
identityMasterRecordKey: contactIdentityMasterRecordKey);
// Verify
final signature = proto.SignatureProto.fromProto(
signedContactInvitation.identitySignature);
await cs.verify(contactIdentityMaster.identityPublicKey,
contactInvitationBytes, signature);
// Verify expiration
//xxx
_validInvitation = signedContactInvitation;
});
} on Exception catch (_) {
_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;
return SizedBox(
height: 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(
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 (_validInvitation != null)
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton.icon(
icon: const Icon(Icons.check_circle),
label: Text(translate('button.accept')),
onPressed: () {
//
},
),
ElevatedButton.icon(
icon: const Icon(Icons.cancel),
label: Text(translate('button.reject')),
onPressed: () {
//
},
)
],
),
TextField(
enabled: false,
controller: _messageTextController,
style: Theme.of(context).textTheme.bodySmall,
).paddingAll(8),
],
),
),
);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<TextEditingController>(
'messageTextController', _messageTextController));
}
}

View file

@ -1,7 +1,10 @@
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 '../tools/tools.dart';
class ProfileWidget extends ConsumerWidget {
const ProfileWidget({
required this.name,
@ -15,16 +18,24 @@ class ProfileWidget extends ConsumerWidget {
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context, WidgetRef ref) {
// final logins = ref.watch(loginsProvider);
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
final textTheme = theme.textTheme;
return ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 300),
child: Column(children: [
Text('Profile', style: Theme.of(context).textTheme.headlineMedium),
Text(name, style: Theme.of(context).textTheme.bodyMedium),
return Container(
width: double.infinity,
decoration: ShapeDecoration(
color: scale.primaryScale.subtleBackground,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: scale.primaryScale.border))),
child: Column(mainAxisSize: MainAxisSize.min, children: [
Text(name, style: Theme.of(context).textTheme.headlineSmall)
.paddingAll(8),
if (title != null && title!.isNotEmpty)
Text(title!, style: Theme.of(context).textTheme.bodySmall),
]));
Text(title!, style: Theme.of(context).textTheme.bodyMedium)
.paddingLTRB(8, 0, 8, 8),
])).paddingAll(8);
}
@override

View file

@ -4,6 +4,7 @@ import 'dart:typed_data';
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:quickalert/quickalert.dart';
@ -143,6 +144,9 @@ class SendInviteDialogState extends ConsumerState<SendInviteDialog> {
).paddingAll(8),
TextField(
controller: _messageTextController,
inputFormatters: [
LengthLimitingTextInputFormatter(256),
],
decoration: InputDecoration(
border: const OutlineInputBorder(),
hintText: translate('send_invite_dialog.enter_message_hint'),