break everything

This commit is contained in:
Christien Rioux 2023-12-26 20:26:54 -05:00
parent e898074387
commit 29210c89d2
121 changed files with 2892 additions and 2608 deletions

View file

@ -0,0 +1,55 @@
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:circular_profile_avatar/circular_profile_avatar.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:window_manager/window_manager.dart';
import '../entities/local_account.dart';
import '../providers/logins.dart';
class AccountBubble extends ConsumerWidget {
const AccountBubble({required this.account, super.key});
final LocalAccount account;
@override
Widget build(BuildContext context, WidgetRef ref) {
windowManager.setTitleBarStyle(TitleBarStyle.normal);
final logins = ref.watch(loginsProvider);
return ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 300),
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
Expanded(
flex: 4,
child: CircularProfileAvatar('',
child: Container(color: Theme.of(context).disabledColor))),
const Expanded(child: Text('Placeholder'))
]));
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<LocalAccount>('account', account));
}
}
class AddAccountBubble extends ConsumerWidget {
const AddAccountBubble({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
windowManager.setTitleBarStyle(TitleBarStyle.normal);
final logins = ref.watch(loginsProvider);
return Column(mainAxisAlignment: MainAxisAlignment.center, children: [
CircularProfileAvatar('',
borderWidth: 4,
borderColor: Theme.of(context).unselectedWidgetColor,
child: Container(
color: Colors.blue, child: const Icon(Icons.add, size: 50))),
const Text('Add Account').paddingLTRB(0, 4, 0, 0)
]);
}
}

View file

@ -0,0 +1,69 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class BottomSheetActionButton extends ConsumerStatefulWidget {
const BottomSheetActionButton(
{required this.bottomSheetBuilder,
required this.builder,
this.foregroundColor,
this.backgroundColor,
this.shape,
super.key});
final Color? foregroundColor;
final Color? backgroundColor;
final ShapeBorder? shape;
final Widget Function(BuildContext) builder;
final Widget Function(BuildContext) bottomSheetBuilder;
@override
BottomSheetActionButtonState createState() => BottomSheetActionButtonState();
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(ObjectFlagProperty<Widget Function(BuildContext p1)>.has(
'bottomSheetBuilder', bottomSheetBuilder))
..add(ColorProperty('foregroundColor', foregroundColor))
..add(ColorProperty('backgroundColor', backgroundColor))
..add(DiagnosticsProperty<ShapeBorder?>('shape', shape))
..add(ObjectFlagProperty<Widget? Function(BuildContext p1)>.has(
'builder', builder));
}
}
class BottomSheetActionButtonState
extends ConsumerState<BottomSheetActionButton> {
bool _showFab = true;
@override
void initState() {
super.initState();
}
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
//
return _showFab
? FloatingActionButton(
elevation: 0,
hoverElevation: 0,
shape: widget.shape,
foregroundColor: widget.foregroundColor,
backgroundColor: widget.backgroundColor,
child: widget.builder(context),
onPressed: () async {
await showModalBottomSheet<void>(
context: context, builder: widget.bottomSheetBuilder);
},
)
: Container();
}
void showFloatingActionButton(bool value) {
setState(() {
_showFab = value;
});
}
}

View file

@ -0,0 +1,205 @@
import 'dart:async';
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../proto/proto.dart' as proto;
import '../providers/account.dart';
import '../providers/chat.dart';
import '../providers/conversation.dart';
import '../tools/tools.dart';
import '../veilid_init.dart';
import '../veilid_support/veilid_support.dart';
class ChatComponent extends ConsumerStatefulWidget {
const ChatComponent(
{required this.activeAccountInfo,
required this.activeChat,
required this.activeChatContact,
super.key});
final ActiveAccountInfo activeAccountInfo;
final TypedKey activeChat;
final proto.Contact activeChatContact;
@override
ChatComponentState createState() => ChatComponentState();
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(DiagnosticsProperty<ActiveAccountInfo>(
'activeAccountInfo', activeAccountInfo))
..add(DiagnosticsProperty<TypedKey>('activeChat', activeChat))
..add(DiagnosticsProperty<proto.Contact>(
'activeChatContact', activeChatContact));
}
}
class ChatComponentState extends ConsumerState<ChatComponent> {
final _unfocusNode = FocusNode();
late final types.User _localUser;
late final types.User _remoteUser;
@override
void initState() {
super.initState();
_localUser = types.User(
id: widget.activeAccountInfo.localAccount.identityMaster
.identityPublicTypedKey()
.toString(),
firstName: widget.activeAccountInfo.account.profile.name,
);
_remoteUser = types.User(
id: proto.TypedKeyProto.fromProto(
widget.activeChatContact.identityPublicKey)
.toString(),
firstName: widget.activeChatContact.remoteProfile.name);
}
@override
void dispose() {
_unfocusNode.dispose();
super.dispose();
}
types.Message protoMessageToMessage(proto.Message message) {
final isLocal = message.author ==
widget.activeAccountInfo.localAccount.identityMaster
.identityPublicTypedKey()
.toProto();
final textMessage = types.TextMessage(
author: isLocal ? _localUser : _remoteUser,
createdAt: (message.timestamp ~/ 1000).toInt(),
id: message.timestamp.toString(),
text: message.text,
);
return textMessage;
}
Future<void> _addMessage(proto.Message protoMessage) async {
if (protoMessage.text.isEmpty) {
return;
}
final message = protoMessageToMessage(protoMessage);
// setState(() {
// _messages.insert(0, message);
// });
// Now add the message to the conversation messages
final localConversationRecordKey = proto.TypedKeyProto.fromProto(
widget.activeChatContact.localConversationRecordKey);
final remoteIdentityPublicKey = proto.TypedKeyProto.fromProto(
widget.activeChatContact.identityPublicKey);
await addLocalConversationMessage(
activeAccountInfo: widget.activeAccountInfo,
localConversationRecordKey: localConversationRecordKey,
remoteIdentityPublicKey: remoteIdentityPublicKey,
message: protoMessage);
ref.invalidate(activeConversationMessagesProvider);
}
Future<void> _handleSendPressed(types.PartialText message) async {
final protoMessage = proto.Message()
..author = widget.activeAccountInfo.localAccount.identityMaster
.identityPublicTypedKey()
.toProto()
..timestamp = (await eventualVeilid.future).now().toInt64()
..text = message.text;
//..signature = signature;
await _addMessage(protoMessage);
}
void _handleAttachmentPressed() {
//
}
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
final textTheme = Theme.of(context).textTheme;
final chatTheme = makeChatTheme(scale, textTheme);
final contactName = widget.activeChatContact.editedProfile.name;
final protoMessages =
ref.watch(activeConversationMessagesProvider).asData?.value;
if (protoMessages == null) {
return waitingPage(context);
}
final messages = <types.Message>[];
for (final protoMessage in protoMessages) {
final message = protoMessageToMessage(protoMessage);
messages.insert(0, message);
}
return DefaultTextStyle(
style: textTheme.bodySmall!,
child: Align(
alignment: AlignmentDirectional.centerEnd,
child: Stack(
children: [
Column(
children: [
Container(
height: 48,
decoration: BoxDecoration(
color: scale.primaryScale.subtleBorder,
),
child: Row(children: [
Align(
alignment: AlignmentDirectional.centerStart,
child: Padding(
padding: const EdgeInsetsDirectional.fromSTEB(
16, 0, 16, 0),
child: Text(contactName,
textAlign: TextAlign.start,
style: textTheme.titleMedium),
)),
const Spacer(),
IconButton(
icon: const Icon(Icons.close),
onPressed: () async {
ref.read(activeChatStateProvider.notifier).state =
null;
}).paddingLTRB(16, 0, 16, 0)
]),
),
Expanded(
child: DecoratedBox(
decoration: const BoxDecoration(),
child: Chat(
theme: chatTheme,
messages: messages,
//onAttachmentPressed: _handleAttachmentPressed,
//onMessageTap: _handleMessageTap,
//onPreviewDataFetched: _handlePreviewDataFetched,
onSendPressed: (message) {
unawaited(_handleSendPressed(message));
},
//showUserAvatars: false,
//showUserNames: true,
user: _localUser,
),
),
),
],
),
],
),
));
}
}

View file

@ -0,0 +1,94 @@
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 '../proto/proto.dart' as proto;
import '../providers/account.dart';
import '../providers/chat.dart';
import '../theme/theme.dart';
class ChatSingleContactItemWidget extends ConsumerWidget {
const ChatSingleContactItemWidget({required this.contact, super.key});
final proto.Contact contact;
@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>()!;
final activeChat = ref.watch(activeChatStateProvider);
final remoteConversationRecordKey =
proto.TypedKeyProto.fromProto(contact.remoteConversationRecordKey);
final selected = activeChat == remoteConversationRecordKey;
return Container(
margin: const EdgeInsets.fromLTRB(0, 4, 0, 0),
clipBehavior: Clip.antiAlias,
decoration: ShapeDecoration(
color: scale.tertiaryScale.subtleBorder,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
)),
child: Slidable(
key: ObjectKey(contact),
endActionPane: ActionPane(
motion: const DrawerMotion(),
children: [
SlidableAction(
onPressed: (context) async {
final activeAccountInfo =
await ref.read(fetchActiveAccountProvider.future);
if (activeAccountInfo != null) {
await deleteChat(
activeAccountInfo: activeAccountInfo,
remoteConversationRecordKey:
remoteConversationRecordKey);
ref.invalidate(fetchChatListProvider);
}
},
backgroundColor: scale.tertiaryScale.background,
foregroundColor: scale.tertiaryScale.text,
icon: Icons.delete,
label: translate('button.delete'),
padding: const EdgeInsets.all(2)),
// SlidableAction(
// onPressed: (context) => (),
// backgroundColor: scale.secondaryScale.background,
// foregroundColor: scale.secondaryScale.text,
// icon: Icons.edit,
// label: 'Edit',
// ),
],
),
// The child of the Slidable is what the user sees when the
// component is not dragged.
child: ListTile(
onTap: () async {
ref.read(activeChatStateProvider.notifier).state =
remoteConversationRecordKey;
ref.invalidate(fetchChatListProvider);
},
title: Text(contact.editedProfile.name),
/// xxx show last message here
subtitle: (contact.editedProfile.pronouns.isNotEmpty)
? Text(contact.editedProfile.pronouns)
: null,
iconColor: scale.tertiaryScale.background,
textColor: scale.tertiaryScale.text,
selected: selected,
//Text(Timestamp.fromInt64(contactInvitationRecord.expiration) / ),
leading: const Icon(Icons.chat))));
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<proto.Contact>('contact', contact));
}
}

View file

@ -0,0 +1,92 @@
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.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:searchable_listview/searchable_listview.dart';
import '../proto/proto.dart' as proto;
import '../tools/tools.dart';
import 'chat_single_contact_item_widget.dart';
import 'empty_chat_list_widget.dart';
class ChatSingleContactListWidget extends ConsumerWidget {
ChatSingleContactListWidget(
{required IList<proto.Contact> contactList,
required this.chatList,
super.key})
: contactMap = IMap.fromIterable(contactList,
keyMapper: (c) => c.remoteConversationRecordKey,
valueMapper: (c) => c);
final IMap<proto.TypedKey, proto.Contact> contactMap;
final IList<proto.Chat> chatList;
@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 SizedBox.expand(
child: styledTitleContainer(
context: context,
title: translate('chat_list.chats'),
child: SizedBox.expand(
child: (chatList.isEmpty)
? const EmptyChatListWidget()
: SearchableList<proto.Chat>(
autoFocusOnSearch: false,
initialList: chatList.toList(),
builder: (l, i, c) {
final contact =
contactMap[c.remoteConversationKey];
if (contact == null) {
return const Text('...');
}
return ChatSingleContactItemWidget(
contact: contact);
},
filter: (value) {
final lowerValue = value.toLowerCase();
return chatList.where((c) {
final contact =
contactMap[c.remoteConversationKey];
if (contact == null) {
return false;
}
return contact.editedProfile.name
.toLowerCase()
.contains(lowerValue) ||
contact.editedProfile.pronouns
.toLowerCase()
.contains(lowerValue);
}).toList();
},
spaceBetweenSearchAndList: 4,
inputDecoration: InputDecoration(
labelText: translate('chat_list.search'),
contentPadding: const EdgeInsets.all(2),
fillColor: scale.primaryScale.text,
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: scale.primaryScale.hoverBorder,
),
borderRadius: BorderRadius.circular(8),
),
),
).paddingAll(8))))
.paddingLTRB(8, 8, 8, 65);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(DiagnosticsProperty<IMap<proto.TypedKey, proto.Contact>>(
'contactMap', contactMap))
..add(IterableProperty<proto.Chat>('chatList', chatList));
}
}

View file

@ -0,0 +1,152 @@
import 'dart:async';
import 'dart:math';
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/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({
required this.name,
required this.message,
required this.generator,
super.key,
});
final String name;
final String message;
final FutureOr<Uint8List> generator;
@override
ContactInvitationDisplayDialogState createState() =>
ContactInvitationDisplayDialogState();
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(StringProperty('name', name))
..add(StringProperty('message', message))
..add(DiagnosticsProperty<FutureOr<Uint8List>?>('generator', generator));
}
}
class ContactInvitationDisplayDialogState
extends ConsumerState<ContactInvitationDisplayDialog> {
final focusNode = FocusNode();
final formKey = GlobalKey<FormState>();
late final AutoDisposeFutureProvider<Uint8List?> _generateFutureProvider;
@override
void initState() {
super.initState();
_generateFutureProvider =
AutoDisposeFutureProvider<Uint8List>((ref) async => widget.generator);
}
@override
void dispose() {
focusNode.dispose();
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 =
min<double>(MediaQuery.of(context).size.shortestSide - 48.0, 400);
return Dialog(
backgroundColor: Colors.white,
child: ConstrainedBox(
constraints: BoxConstraints(
minWidth: cardsize,
maxWidth: cardsize,
minHeight: cardsize,
maxHeight: cardsize),
child: signedContactInvitationBytesV.when(
loading: () => buildProgressIndicator(context),
data: (data) {
if (data == null) {
Navigator.of(context).pop();
return const Text('');
}
return Form(
key: formKey,
child: Column(children: [
FittedBox(
child: Text(
translate(
'send_invite_dialog.contact_invitation'),
style: textTheme.headlineSmall!
.copyWith(color: Colors.black)))
.paddingAll(8),
FittedBox(
child: QrImageView.withQr(
size: 300,
qr: QrCode.fromUint8List(
data: data,
errorCorrectLevel:
QrErrorCorrectLevel.L)))
.expanded(),
Text(widget.message,
softWrap: true,
style: textTheme.labelLarge!
.copyWith(color: Colors.black))
.paddingAll(8),
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, data)));
},
).paddingAll(16),
]));
},
error: (e, s) {
Navigator.of(context).pop();
showErrorToast(context,
translate('send_invite_dialog.failed_to_generate'));
return const Text('');
})));
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(DiagnosticsProperty<FocusNode>('focusNode', focusNode))
..add(DiagnosticsProperty<GlobalKey<FormState>>('formKey', formKey));
}
}

View file

@ -0,0 +1,127 @@
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 '../proto/proto.dart' as proto;
import '../providers/account.dart';
import '../providers/contact_invite.dart';
import '../tools/tools.dart';
import 'contact_invitation_display.dart';
class ContactInvitationItemWidget extends ConsumerWidget {
const ContactInvitationItemWidget(
{required this.contactInvitationRecord, super.key});
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) {
final theme = Theme.of(context);
//final textTheme = theme.textTheme;
final scale = theme.extension<ScaleScheme>()!;
return Container(
clipBehavior: Clip.antiAlias,
decoration: ShapeDecoration(
color: scale.tertiaryScale.subtleBorder,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
)),
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(),
// 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(
accepted: false,
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)),
],
),
// 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')),
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),
));
}
},
title: Text(
contactInvitationRecord.message.isEmpty
? translate('contact_list.invitation')
: contactInvitationRecord.message,
softWrap: true,
),
iconColor: scale.tertiaryScale.background,
textColor: scale.tertiaryScale.text,
//Text(Timestamp.fromInt64(contactInvitationRecord.expiration) / ),
leading: const Icon(Icons.person_add))));
}
}

View file

@ -0,0 +1,81 @@
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../proto/proto.dart' as proto;
import '../tools/tools.dart';
import 'contact_invitation_item_widget.dart';
class ContactInvitationListWidget extends ConsumerStatefulWidget {
const ContactInvitationListWidget({
required this.contactInvitationRecordList,
super.key,
});
final IList<proto.ContactInvitationRecord> contactInvitationRecordList;
@override
ContactInvitationListWidgetState createState() =>
ContactInvitationListWidgetState();
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(IterableProperty<proto.ContactInvitationRecord>(
'contactInvitationRecordList', contactInvitationRecordList));
}
}
class ContactInvitationListWidgetState
extends ConsumerState<ContactInvitationListWidget> {
final ScrollController _scrollController = ScrollController();
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
final theme = Theme.of(context);
//final textTheme = theme.textTheme;
final scale = theme.extension<ScaleScheme>()!;
return Container(
width: double.infinity,
margin: const EdgeInsets.fromLTRB(4, 0, 4, 4),
decoration: ShapeDecoration(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
)),
constraints: const BoxConstraints(maxHeight: 200),
child: Container(
width: double.infinity,
decoration: ShapeDecoration(
color: scale.primaryScale.subtleBackground,
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]))
.paddingLTRB(4, 2, 4, 2);
},
findChildIndexCallback: (key) {
final index = widget.contactInvitationRecordList.indexOf(
(key as ObjectKey).value! as proto.ContactInvitationRecord);
if (index == -1) {
return null;
}
return index;
},
).paddingLTRB(4, 6, 4, 6)),
);
}
}

View file

@ -0,0 +1,122 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:flutter_translate/flutter_translate.dart';
import '../proto/proto.dart' as proto;
import '../pages/main_pager/main_pager.dart';
import '../providers/account.dart';
import '../providers/chat.dart';
import '../providers/contact.dart';
import '../theme/theme.dart';
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) {
final theme = Theme.of(context);
//final textTheme = theme.textTheme;
final scale = theme.extension<ScaleScheme>()!;
final remoteConversationKey =
proto.TypedKeyProto.fromProto(contact.remoteConversationRecordKey);
return Container(
margin: const EdgeInsets.fromLTRB(0, 4, 0, 0),
clipBehavior: Clip.antiAlias,
decoration: ShapeDecoration(
color: scale.tertiaryScale.subtleBorder,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
)),
child: Slidable(
key: ObjectKey(contact),
endActionPane: ActionPane(
motion: const DrawerMotion(),
children: [
SlidableAction(
onPressed: (context) async {
final activeAccountInfo =
await ref.read(fetchActiveAccountProvider.future);
if (activeAccountInfo != null) {
await deleteContact(
activeAccountInfo: activeAccountInfo,
contact: contact);
ref
..invalidate(fetchContactListProvider)
..invalidate(fetchChatListProvider);
}
},
backgroundColor: scale.tertiaryScale.background,
foregroundColor: scale.tertiaryScale.text,
icon: Icons.delete,
label: translate('button.delete'),
padding: const EdgeInsets.all(2)),
// SlidableAction(
// onPressed: (context) => (),
// backgroundColor: scale.secondaryScale.background,
// foregroundColor: scale.secondaryScale.text,
// icon: Icons.edit,
// label: 'Edit',
// ),
],
),
// The child of the Slidable is what the user sees when the
// component is not dragged.
child: ListTile(
onTap: () async {
final activeAccountInfo =
await ref.read(fetchActiveAccountProvider.future);
if (activeAccountInfo != null) {
// Start a chat
await getOrCreateChatSingleContact(
activeAccountInfo: activeAccountInfo,
remoteConversationRecordKey: remoteConversationKey);
ref
..invalidate(fetchContactListProvider)
..invalidate(fetchChatListProvider);
// Click over to chats
if (context.mounted) {
await MainPager.of(context)?.pageController.animateToPage(
1,
duration: 250.ms,
curve: Curves.easeInOut);
}
}
// // 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),
// ));
// }
},
title: Text(contact.editedProfile.name),
subtitle: (contact.editedProfile.pronouns.isNotEmpty)
? Text(contact.editedProfile.pronouns)
: null,
iconColor: scale.tertiaryScale.background,
textColor: scale.tertiaryScale.text,
//Text(Timestamp.fromInt64(contactInvitationRecord.expiration) / ),
leading: const Icon(Icons.person))));
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<proto.Contact>('contact', contact));
}
}

View file

@ -0,0 +1,68 @@
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.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:searchable_listview/searchable_listview.dart';
import '../proto/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 IList<proto.Contact> contactList;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(IterableProperty<proto.Contact>('contactList', contactList));
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
//final textTheme = theme.textTheme;
final scale = theme.extension<ScaleScheme>()!;
return SizedBox.expand(
child: styledTitleContainer(
context: context,
title: translate('contact_list.title'),
child: SizedBox.expand(
child: (contactList.isEmpty)
? const EmptyContactListWidget()
: SearchableList<proto.Contact>(
autoFocusOnSearch: false,
initialList: contactList.toList(),
builder: (l, i, c) => ContactItemWidget(contact: c),
filter: (value) {
final lowerValue = value.toLowerCase();
return contactList
.where((element) =>
element.editedProfile.name
.toLowerCase()
.contains(lowerValue) ||
element.editedProfile.pronouns
.toLowerCase()
.contains(lowerValue))
.toList();
},
spaceBetweenSearchAndList: 4,
inputDecoration: InputDecoration(
labelText: translate('contact_list.search'),
contentPadding: const EdgeInsets.all(2),
fillColor: scale.primaryScale.text,
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: scale.primaryScale.hoverBorder,
),
borderRadius: BorderRadius.circular(8),
),
),
).paddingAll(8),
))).paddingLTRB(8, 0, 8, 8);
}
}

View file

@ -0,0 +1,18 @@
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
class DefaultAppBar extends AppBar {
DefaultAppBar(
{required super.title, super.key, Widget? leading, super.actions})
: super(
leading: leading ??
Container(
margin: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.black.withAlpha(32),
shape: BoxShape.circle),
child:
SvgPicture.asset('assets/images/vlogo.svg', height: 32)
.paddingAll(4)));
}

View file

@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_translate/flutter_translate.dart';
import '../tools/tools.dart';
class EmptyChatListWidget extends ConsumerWidget {
const EmptyChatListWidget({super.key});
@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 Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.chat,
color: scale.primaryScale.border,
size: 48,
),
Text(
translate('chat_list.start_a_conversation'),
style: textTheme.bodyMedium?.copyWith(
color: scale.primaryScale.border,
),
),
],
);
}
}

View file

@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class EmptyChatWidget extends ConsumerWidget {
const EmptyChatWidget({super.key});
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context, WidgetRef ref) {
//
return Container(
width: double.infinity,
height: double.infinity,
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.chat,
color: Theme.of(context).disabledColor,
size: 48,
),
Text(
'Say Something',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).disabledColor,
),
),
],
),
);
}
}

View file

@ -0,0 +1,35 @@
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) {
final theme = Theme.of(context);
final textTheme = theme.textTheme;
final scale = theme.extension<ScaleScheme>()!;
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.person_add_sharp,
color: scale.primaryScale.subtleBorder,
size: 48,
),
Text(
translate('contact_list.invite_people'),
style: textTheme.bodyMedium?.copyWith(
color: scale.primaryScale.subtleBorder,
),
),
],
);
}
}

View file

@ -0,0 +1,128 @@
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 '../tools/tools.dart';
class EnterPasswordDialog extends ConsumerStatefulWidget {
const EnterPasswordDialog({
this.matchPass,
this.description,
super.key,
});
final String? matchPass;
final String? description;
@override
EnterPasswordDialogState createState() => EnterPasswordDialogState();
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(StringProperty('reenter', matchPass))
..add(StringProperty('description', description));
}
}
class EnterPasswordDialogState extends ConsumerState<EnterPasswordDialog> {
final passwordController = TextEditingController();
final focusNode = FocusNode();
final formKey = GlobalKey<FormState>();
bool _passwordVisible = false;
@override
void initState() {
super.initState();
}
@override
void dispose() {
passwordController.dispose();
focusNode.dispose();
super.dispose();
}
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
return Dialog(
backgroundColor: scale.grayScale.subtleBackground,
child: Form(
key: formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
widget.matchPass == null
? translate('enter_password_dialog.enter_password')
: translate('enter_password_dialog.reenter_password'),
style: theme.textTheme.titleLarge,
).paddingAll(16),
TextField(
controller: passwordController,
focusNode: focusNode,
autofocus: true,
enableSuggestions: false,
obscureText:
!_passwordVisible, //This will obscure text dynamically
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.singleLineFormatter
],
onSubmitted: (password) {
Navigator.pop(context, password);
},
onChanged: (_) {
setState(() {});
},
decoration: InputDecoration(
prefixIcon: widget.matchPass == null
? null
: Icon(Icons.check_circle,
color: passwordController.text == widget.matchPass
? scale.primaryScale.background
: scale.grayScale.subtleBackground),
suffixIcon: IconButton(
icon: Icon(
_passwordVisible
? Icons.visibility
: Icons.visibility_off,
color: scale.primaryScale.text,
),
onPressed: () {
setState(() {
_passwordVisible = !_passwordVisible;
});
},
),
)).paddingAll(16),
if (widget.description != null)
SizedBox(
width: 400,
child: Text(
widget.description!,
textAlign: TextAlign.center,
).paddingAll(16))
],
),
));
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(DiagnosticsProperty<TextEditingController>(
'passwordController', passwordController))
..add(DiagnosticsProperty<FocusNode>('focusNode', focusNode))
..add(DiagnosticsProperty<GlobalKey<FormState>>('formKey', formKey));
}
}

View file

@ -0,0 +1,142 @@
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:pinput/pinput.dart';
import '../tools/tools.dart';
class EnterPinDialog extends ConsumerStatefulWidget {
const EnterPinDialog({
required this.reenter,
required this.description,
super.key,
});
final bool reenter;
final String? description;
@override
EnterPinDialogState createState() => EnterPinDialogState();
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(StringProperty('description', description))
..add(DiagnosticsProperty<bool>('reenter', reenter));
}
}
class EnterPinDialogState extends ConsumerState<EnterPinDialog> {
final pinController = TextEditingController();
final focusNode = FocusNode();
final formKey = GlobalKey<FormState>();
@override
void initState() {
super.initState();
}
@override
void dispose() {
pinController.dispose();
focusNode.dispose();
super.dispose();
}
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
final focusedBorderColor = scale.primaryScale.hoverBorder;
final fillColor = scale.primaryScale.elementBackground;
final borderColor = scale.primaryScale.border;
final defaultPinTheme = PinTheme(
width: 56,
height: 60,
textStyle: TextStyle(fontSize: 22, color: scale.primaryScale.text),
decoration: BoxDecoration(
color: fillColor,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: borderColor),
),
);
/// Optionally you can use form to validate the Pinput
return Dialog(
backgroundColor: scale.grayScale.subtleBackground,
child: Form(
key: formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
!widget.reenter
? translate('enter_pin_dialog.enter_pin')
: translate('enter_pin_dialog.reenter_pin'),
style: theme.textTheme.titleLarge,
).paddingAll(16),
Directionality(
// Specify direction if desired
textDirection: TextDirection.ltr,
child: Pinput(
controller: pinController,
focusNode: focusNode,
autofocus: true,
defaultPinTheme: defaultPinTheme,
enableSuggestions: false,
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.digitsOnly
],
hapticFeedbackType: HapticFeedbackType.lightImpact,
onCompleted: (pin) {
Navigator.pop(context, pin);
},
cursor: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Container(
margin: const EdgeInsets.only(bottom: 9),
width: 22,
height: 1,
color: focusedBorderColor,
),
],
),
focusedPinTheme: defaultPinTheme.copyWith(
height: 68,
width: 64,
decoration: defaultPinTheme.decoration!.copyWith(
border: Border.all(color: borderColor),
),
),
).paddingAll(16),
),
if (widget.description != null)
SizedBox(
width: 400,
child: Text(
widget.description!,
textAlign: TextAlign.center,
).paddingAll(16))
],
),
));
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(DiagnosticsProperty<TextEditingController>(
'pinController', pinController))
..add(DiagnosticsProperty<FocusNode>('focusNode', focusNode))
..add(DiagnosticsProperty<GlobalKey<FormState>>('formKey', formKey));
}
}

View file

@ -0,0 +1,343 @@
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 '../entities/local_account.dart';
import '../providers/account.dart';
import '../providers/contact.dart';
import '../providers/contact_invite.dart';
import '../tools/tools.dart';
import 'enter_password.dart';
import 'enter_pin.dart';
import 'profile_widget.dart';
class InviteDialog extends ConsumerStatefulWidget {
const InviteDialog(
{required this.onValidationCancelled,
required this.onValidationSuccess,
required this.onValidationFailed,
required this.inviteControlIsValid,
required this.buildInviteControl,
super.key});
final void Function() onValidationCancelled;
final void Function() onValidationSuccess;
final void Function() onValidationFailed;
final bool Function() inviteControlIsValid;
final Widget Function(
BuildContext context,
InviteDialogState dialogState,
Future<void> Function({required Uint8List inviteData})
validateInviteData) buildInviteControl;
@override
InviteDialogState createState() => InviteDialogState();
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(ObjectFlagProperty<void Function()>.has(
'onValidationCancelled', onValidationCancelled))
..add(ObjectFlagProperty<void Function()>.has(
'onValidationSuccess', onValidationSuccess))
..add(ObjectFlagProperty<void Function()>.has(
'onValidationFailed', onValidationFailed))
..add(ObjectFlagProperty<void Function()>.has(
'inviteControlIsValid', inviteControlIsValid))
..add(ObjectFlagProperty<
Widget Function(
BuildContext context,
InviteDialogState dialogState,
Future<void> Function({required Uint8List inviteData})
validateInviteData)>.has(
'buildInviteControl', buildInviteControl));
}
}
class InviteDialogState extends ConsumerState<InviteDialog> {
ValidContactInvitation? _validInvitation;
bool _isValidating = false;
bool _isAccepting = false;
@override
void initState() {
super.initState();
}
bool get isValidating => _isValidating;
bool get isAccepting => _isAccepting;
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) {
// initiator when accept is received will create
// contact in the case of a 'note to self'
final isSelf =
activeAccountInfo.localAccount.identityMaster.identityPublicKey ==
acceptedContact.remoteIdentity.identityPublicKey;
if (!isSelf) {
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, '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, 'invite_dialog.failed_to_reject');
}
}
}
setState(() {
_isAccepting = false;
});
navigator.pop();
}
Future<void> _validateInviteData({
required Uint8List inviteData,
}) async {
try {
final activeAccountInfo =
await ref.read(fetchActiveAccountProvider.future);
if (activeAccountInfo == null) {
setState(() {
_isValidating = false;
_validInvitation = null;
});
return;
}
final contactInvitationRecords =
await ref.read(fetchContactInvitationRecordsProvider.future);
setState(() {
_isValidating = true;
_validInvitation = null;
});
final validatedContactInvitation = await validateContactInvitation(
activeAccountInfo: activeAccountInfo,
contactInvitationRecords: contactInvitationRecords,
inviteData: inviteData,
getEncryptionKeyCallback:
(cs, encryptionKeyType, encryptedSecret) async {
String encryptionKey;
switch (encryptionKeyType) {
case EncryptionKeyType.none:
encryptionKey = '';
case EncryptionKeyType.pin:
final description =
translate('invite_dialog.protected_with_pin');
if (!context.mounted) {
return null;
}
final pin = await showDialog<String>(
context: context,
builder: (context) => EnterPinDialog(
reenter: false, description: description));
if (pin == null) {
return null;
}
encryptionKey = pin;
case EncryptionKeyType.password:
final description =
translate('invite_dialog.protected_with_password');
if (!context.mounted) {
return null;
}
final password = await showDialog<String>(
context: context,
builder: (context) =>
EnterPasswordDialog(description: description));
if (password == null) {
return null;
}
encryptionKey = password;
}
return decryptSecretFromBytes(
secretBytes: encryptedSecret,
cryptoKind: cs.kind(),
encryptionKeyType: encryptionKeyType,
encryptionKey: encryptionKey);
});
// Check if validation was cancelled
if (validatedContactInvitation == null) {
setState(() {
_isValidating = false;
_validInvitation = null;
widget.onValidationCancelled();
});
return;
}
// Verify expiration
// xxx
setState(() {
widget.onValidationSuccess();
_isValidating = false;
_validInvitation = validatedContactInvitation;
});
} on ContactInviteInvalidKeyException catch (e) {
String errorText;
switch (e.type) {
case EncryptionKeyType.none:
errorText = translate('invite_dialog.invalid_invitation');
case EncryptionKeyType.pin:
errorText = translate('invite_dialog.invalid_pin');
case EncryptionKeyType.password:
errorText = translate('invite_dialog.invalid_password');
}
if (context.mounted) {
showErrorToast(context, errorText);
}
setState(() {
_isValidating = false;
_validInvitation = null;
widget.onValidationFailed();
});
} on Exception catch (e) {
log.debug('exception: $e', e);
setState(() {
_isValidating = false;
_validInvitation = null;
widget.onValidationFailed();
});
rethrow;
}
}
@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: 300,
width: 300,
child: buildProgressIndicator(context).toCenter())
.paddingAll(16);
}
return ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 400, maxWidth: 400),
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
widget.buildInviteControl(context, this, _validateInviteData),
if (_isValidating)
Column(children: [
Text(translate('invite_dialog.validating'))
.paddingLTRB(0, 0, 0, 16),
buildProgressIndicator(context).paddingAll(16),
]).toCenter(),
if (_validInvitation == null &&
!_isValidating &&
widget.inviteControlIsValid())
Column(children: [
Text(translate('invite_dialog.invalid_invitation')),
const Icon(Icons.error)
]).paddingAll(16).toCenter(),
if (_validInvitation != null && !_isValidating)
Column(children: [
Container(
constraints: const BoxConstraints(maxHeight: 64),
width: double.infinity,
child: ProfileWidget(
name: _validInvitation!
.contactRequestPrivate.profile.name,
pronouns: _validInvitation!
.contactRequestPrivate.profile.pronouns,
)).paddingLTRB(0, 0, 0, 8),
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);
properties
..add(DiagnosticsProperty<bool>('isValidating', isValidating))
..add(DiagnosticsProperty<bool>('isAccepting', isAccepting));
}
}

View file

@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class NoContactWidget extends ConsumerWidget {
const NoContactWidget({super.key});
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context, WidgetRef ref) {
//
return Container(
width: double.infinity,
height: double.infinity,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.emoji_people_outlined,
color: Theme.of(context).disabledColor,
size: 48,
),
Text(
'Choose A Conversation To Chat',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).disabledColor,
),
),
],
),
);
}
}

View file

@ -0,0 +1,131 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_translate/flutter_translate.dart';
import '../tools/tools.dart';
import '../veilid_support/veilid_support.dart';
import 'invite_dialog.dart';
class PasteInviteDialog extends ConsumerStatefulWidget {
const PasteInviteDialog({super.key});
@override
PasteInviteDialogState createState() => PasteInviteDialogState();
static Future<void> show(BuildContext context) async {
await showStyledDialog<void>(
context: context,
title: translate('paste_invite_dialog.title'),
child: const PasteInviteDialog());
}
}
class PasteInviteDialogState extends ConsumerState<PasteInviteDialog> {
final _pasteTextController = TextEditingController();
@override
void initState() {
super.initState();
}
Future<void> _onPasteChanged(
String text,
Future<void> Function({
required Uint8List inviteData,
}) validateInviteData) async {
final lines = text.split('\n');
if (lines.isEmpty) {
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) {
return;
}
final inviteDataBase64 = lines
.sublist(firstline, lastline)
.join()
.replaceAll(RegExp(r'[^A-Za-z0-9\-_]'), '');
final inviteData = base64UrlNoPadDecode(inviteDataBase64);
await validateInviteData(inviteData: inviteData);
}
void onValidationCancelled() {
_pasteTextController.clear();
}
void onValidationSuccess() {
//_pasteTextController.clear();
}
void onValidationFailed() {
_pasteTextController.clear();
}
bool inviteControlIsValid() => _pasteTextController.text.isNotEmpty;
Widget buildInviteControl(
BuildContext context,
InviteDialogState dialogState,
Future<void> Function({required Uint8List inviteData})
validateInviteData) {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
//final textTheme = theme.textTheme;
//final height = MediaQuery.of(context).size.height;
final monoStyle = TextStyle(
fontFamily: 'Source Code Pro',
fontSize: 11,
color: scale.primaryScale.text,
);
return Column(mainAxisSize: MainAxisSize.min, children: [
Text(
translate('paste_invite_dialog.paste_invite_here'),
).paddingLTRB(0, 0, 0, 8),
Container(
constraints: const BoxConstraints(maxHeight: 200),
child: TextField(
enabled: !dialogState.isValidating,
onChanged: (text) async =>
_onPasteChanged(text, validateInviteData),
style: monoStyle,
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')
),
)).paddingLTRB(0, 0, 0, 8)
]);
}
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return InviteDialog(
onValidationCancelled: onValidationCancelled,
onValidationSuccess: onValidationSuccess,
onValidationFailed: onValidationFailed,
inviteControlIsValid: inviteControlIsValid,
buildInviteControl: buildInviteControl);
}
}

View file

@ -0,0 +1,49 @@
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,
this.pronouns,
super.key,
});
final String name;
final String? pronouns;
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
final textTheme = theme.textTheme;
return DecoratedBox(
decoration: ShapeDecoration(
color: scale.primaryScale.border,
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(16))),
child: Column(children: [
Text(
name,
style: textTheme.headlineSmall,
textAlign: TextAlign.left,
).paddingAll(4),
if (pronouns != null && pronouns!.isNotEmpty)
Text(pronouns!, style: textTheme.bodyMedium).paddingLTRB(4, 0, 4, 4),
]),
);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(StringProperty('name', name))
..add(StringProperty('pronouns', pronouns));
}
}

View file

@ -0,0 +1,399 @@
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:image/image.dart' as img;
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:pasteboard/pasteboard.dart';
import 'package:zxing2/qrcode.dart';
import '../tools/tools.dart';
import 'invite_dialog.dart';
class BarcodeOverlay extends CustomPainter {
BarcodeOverlay({
required this.barcode,
required this.arguments,
required this.boxFit,
required this.capture,
});
final BarcodeCapture capture;
final Barcode barcode;
final MobileScannerArguments arguments;
final BoxFit boxFit;
@override
void paint(Canvas canvas, Size size) {
if (barcode.corners == null) {
return;
}
final adjustedSize = applyBoxFit(boxFit, arguments.size, size);
var verticalPadding = size.height - adjustedSize.destination.height;
var horizontalPadding = size.width - adjustedSize.destination.width;
if (verticalPadding > 0) {
verticalPadding = verticalPadding / 2;
} else {
verticalPadding = 0;
}
if (horizontalPadding > 0) {
horizontalPadding = horizontalPadding / 2;
} else {
horizontalPadding = 0;
}
final ratioWidth =
(Platform.isIOS ? capture.width! : arguments.size.width) /
adjustedSize.destination.width;
final ratioHeight =
(Platform.isIOS ? capture.height! : arguments.size.height) /
adjustedSize.destination.height;
final adjustedOffset = <Offset>[];
for (final offset in barcode.corners!) {
adjustedOffset.add(
Offset(
offset.dx / ratioWidth + horizontalPadding,
offset.dy / ratioHeight + verticalPadding,
),
);
}
final cutoutPath = Path()..addPolygon(adjustedOffset, true);
final backgroundPaint = Paint()
..color = Colors.red.withOpacity(0.3)
..style = PaintingStyle.fill
..blendMode = BlendMode.dstOut;
canvas.drawPath(cutoutPath, backgroundPaint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
class ScannerOverlay extends CustomPainter {
ScannerOverlay(this.scanWindow);
final Rect scanWindow;
@override
void paint(Canvas canvas, Size size) {
final backgroundPath = Path()..addRect(Rect.largest);
final cutoutPath = Path()..addRect(scanWindow);
final backgroundPaint = Paint()
..color = Colors.black.withOpacity(0.5)
..style = PaintingStyle.fill
..blendMode = BlendMode.dstOut;
final backgroundWithCutout = Path.combine(
PathOperation.difference,
backgroundPath,
cutoutPath,
);
canvas.drawPath(backgroundWithCutout, backgroundPaint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
class ScanInviteDialog extends ConsumerStatefulWidget {
const ScanInviteDialog({super.key});
@override
ScanInviteDialogState createState() => ScanInviteDialogState();
static Future<void> show(BuildContext context) async {
await showStyledDialog<void>(
context: context,
title: translate('scan_invite_dialog.title'),
child: const ScanInviteDialog());
}
}
class ScanInviteDialogState extends ConsumerState<ScanInviteDialog> {
bool scanned = false;
@override
void initState() {
super.initState();
}
void onValidationCancelled() {
setState(() {
scanned = false;
});
}
void onValidationSuccess() {}
void onValidationFailed() {
setState(() {
scanned = false;
});
}
bool inviteControlIsValid() => false; // _pasteTextController.text.isNotEmpty;
Future<Uint8List?> scanQRImage(BuildContext context) async {
final theme = Theme.of(context);
//final textTheme = theme.textTheme;
final scale = theme.extension<ScaleScheme>()!;
final windowSize = MediaQuery.of(context).size;
//final maxDialogWidth = min(windowSize.width - 64.0, 800.0 - 64.0);
//final maxDialogHeight = windowSize.height - 64.0;
final scanWindow = Rect.fromCenter(
center: MediaQuery.of(context).size.center(Offset.zero),
width: 200,
height: 200,
);
final cameraController = MobileScannerController();
try {
return showDialog(
context: context,
builder: (context) => Stack(
fit: StackFit.expand,
children: [
MobileScanner(
fit: BoxFit.contain,
scanWindow: scanWindow,
controller: cameraController,
errorBuilder: (context, error, child) =>
ScannerErrorWidget(error: error),
onDetect: (c) {
final barcode = c.barcodes.firstOrNull;
final barcodeBytes = barcode?.rawBytes;
if (barcodeBytes != null) {
cameraController.dispose();
Navigator.pop(context, barcodeBytes);
}
}),
CustomPaint(
painter: ScannerOverlay(scanWindow),
),
Align(
alignment: Alignment.bottomCenter,
child: Container(
alignment: Alignment.bottomCenter,
height: 100,
color: Colors.black.withOpacity(0.4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
color: Colors.white,
icon: ValueListenableBuilder(
valueListenable: cameraController.torchState,
builder: (context, state, child) {
switch (state) {
case TorchState.off:
return Icon(Icons.flash_off,
color:
scale.grayScale.subtleBackground);
case TorchState.on:
return Icon(Icons.flash_on,
color: scale.primaryScale.background);
}
},
),
iconSize: 32,
onPressed: cameraController.toggleTorch,
),
SizedBox(
width: windowSize.width - 120,
height: 50,
child: FittedBox(
child: Text(
translate('scan_invite_dialog.instructions'),
overflow: TextOverflow.fade,
style: Theme.of(context)
.textTheme
.labelLarge!
.copyWith(color: Colors.white),
),
),
),
IconButton(
color: Colors.white,
icon: ValueListenableBuilder(
valueListenable:
cameraController.cameraFacingState,
builder: (context, state, child) {
switch (state) {
case CameraFacing.front:
return const Icon(Icons.camera_front);
case CameraFacing.back:
return const Icon(Icons.camera_rear);
}
},
),
iconSize: 32,
onPressed: cameraController.switchCamera,
),
],
),
),
),
Align(
alignment: Alignment.topRight,
child: IconButton(
color: Colors.white,
icon: Icon(Icons.close,
color: scale.grayScale.background),
iconSize: 32,
onPressed: () => {
SchedulerBinding.instance
.addPostFrameCallback((_) {
cameraController.dispose();
Navigator.pop(context, null);
})
})),
],
));
} on MobileScannerException catch (e) {
if (e.errorCode == MobileScannerErrorCode.permissionDenied) {
showErrorToast(
context, translate('scan_invite_dialog.permission_error'));
} else {
showErrorToast(context, translate('scan_invite_dialog.error'));
}
} on Exception catch (_) {
showErrorToast(context, translate('scan_invite_dialog.error'));
}
return null;
}
Future<Uint8List?> pasteQRImage(BuildContext context) async {
final imageBytes = await Pasteboard.image;
if (imageBytes == null) {
if (context.mounted) {
showErrorToast(context, translate('scan_invite_dialog.not_an_image'));
}
return null;
}
final image = img.decodeImage(imageBytes);
if (image == null) {
if (context.mounted) {
showErrorToast(
context, translate('scan_invite_dialog.could_not_decode_image'));
}
return null;
}
try {
final source = RGBLuminanceSource(
image.width,
image.height,
image
.convert(numChannels: 4)
.getBytes(order: img.ChannelOrder.abgr)
.buffer
.asInt32List());
final bitmap = BinaryBitmap(HybridBinarizer(source));
final reader = QRCodeReader();
final result = reader.decode(bitmap);
final segs = result.resultMetadata[ResultMetadataType.byteSegments]!
as List<Int8List>;
return Uint8List.fromList(segs[0].toList());
} on Exception catch (_) {
if (context.mounted) {
showErrorToast(
context, translate('scan_invite_dialog.not_a_valid_qr_code'));
}
return null;
}
}
Widget buildInviteControl(
BuildContext context,
InviteDialogState dialogState,
Future<void> Function({required Uint8List inviteData})
validateInviteData) {
//final theme = Theme.of(context);
//final scale = theme.extension<ScaleScheme>()!;
//final textTheme = theme.textTheme;
//final height = MediaQuery.of(context).size.height;
if (isiOS || isAndroid) {
return Column(mainAxisSize: MainAxisSize.min, children: [
if (!scanned)
Text(
translate('scan_invite_dialog.scan_qr_here'),
).paddingLTRB(0, 0, 0, 8),
if (!scanned)
Container(
constraints: const BoxConstraints(maxHeight: 200),
child: ElevatedButton(
onPressed: dialogState.isValidating
? null
: () async {
final inviteData = await scanQRImage(context);
if (inviteData != null) {
setState(() {
scanned = true;
});
await validateInviteData(inviteData: inviteData);
}
},
child: Text(translate('scan_invite_dialog.scan'))),
).paddingLTRB(0, 0, 0, 8)
]);
}
return Column(mainAxisSize: MainAxisSize.min, children: [
if (!scanned)
Text(
translate('scan_invite_dialog.paste_qr_here'),
).paddingLTRB(0, 0, 0, 8),
if (!scanned)
Container(
constraints: const BoxConstraints(maxHeight: 200),
child: ElevatedButton(
onPressed: dialogState.isValidating
? null
: () async {
final inviteData = await pasteQRImage(context);
if (inviteData != null) {
await validateInviteData(inviteData: inviteData);
setState(() {
scanned = true;
});
}
},
child: Text(translate('scan_invite_dialog.paste'))),
).paddingLTRB(0, 0, 0, 8)
]);
}
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return InviteDialog(
onValidationCancelled: onValidationCancelled,
onValidationSuccess: onValidationSuccess,
onValidationFailed: onValidationFailed,
inviteControlIsValid: inviteControlIsValid,
buildInviteControl: buildInviteControl);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<bool>('scanned', scanned));
}
}

View file

@ -0,0 +1,248 @@
import 'dart:async';
import 'dart:math';
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 '../entities/local_account.dart';
import '../providers/account.dart';
import '../providers/contact_invite.dart';
import '../tools/tools.dart';
import '../veilid_support/veilid_support.dart';
import 'contact_invitation_display.dart';
import 'enter_password.dart';
import 'enter_pin.dart';
class SendInviteDialog extends ConsumerStatefulWidget {
const SendInviteDialog({super.key});
@override
SendInviteDialogState createState() => SendInviteDialogState();
static Future<void> show(BuildContext context) async {
await showStyledDialog<void>(
context: context,
title: translate('send_invite_dialog.title'),
child: const SendInviteDialog());
}
}
class SendInviteDialogState extends ConsumerState<SendInviteDialog> {
final _messageTextController = TextEditingController(
text: translate('send_invite_dialog.connect_with_me'));
EncryptionKeyType _encryptionKeyType = EncryptionKeyType.none;
String _encryptionKey = '';
Timestamp? _expiration;
@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('send_invite_dialog.pin_description');
final pin = await showDialog<String>(
context: context,
builder: (context) =>
EnterPinDialog(reenter: false, 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(
reenter: true,
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('send_invite_dialog.pin_does_not_match'));
setState(() {
_encryptionKeyType = EncryptionKeyType.none;
_encryptionKey = '';
});
}
}
Future<void> _onPasswordEncryptionSelected(bool selected) async {
final description = translate('send_invite_dialog.password_description');
final password = await showDialog<String>(
context: context,
builder: (context) => EnterPasswordDialog(description: description));
if (password == null) {
return;
}
// ignore: use_build_context_synchronously
if (!context.mounted) {
return;
}
final matchpass = await showDialog<String>(
context: context,
builder: (context) => EnterPasswordDialog(
matchPass: password,
description: description,
));
if (matchpass == null) {
return;
} else if (password == matchpass) {
setState(() {
_encryptionKeyType = EncryptionKeyType.password;
_encryptionKey = password;
});
} else {
// ignore: use_build_context_synchronously
if (!context.mounted) {
return;
}
showErrorToast(
context, translate('send_invite_dialog.password_does_not_match'));
setState(() {
_encryptionKeyType = EncryptionKeyType.none;
_encryptionKey = '';
});
}
}
Future<void> _onGenerateButtonPressed() async {
final navigator = Navigator.of(context);
// Start generation
final activeAccountInfo = await ref.read(fetchActiveAccountProvider.future);
if (activeAccountInfo == null) {
navigator.pop();
return;
}
final generator = createContactInvitation(
activeAccountInfo: activeAccountInfo,
encryptionKeyType: _encryptionKeyType,
encryptionKey: _encryptionKey,
message: _messageTextController.text,
expiration: _expiration);
// ignore: use_build_context_synchronously
if (!context.mounted) {
return;
}
await showDialog<void>(
context: context,
builder: (context) => ContactInvitationDisplayDialog(
name: activeAccountInfo.localAccount.name,
message: _messageTextController.text,
generator: generator,
));
// if (ret == null) {
// return;
// }
ref.invalidate(fetchContactInvitationRecordsProvider);
navigator.pop();
}
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
final windowSize = MediaQuery.of(context).size;
final maxDialogWidth = min(windowSize.width - 64.0, 800.0 - 64.0);
final maxDialogHeight = windowSize.height - 64.0;
final theme = Theme.of(context);
//final scale = theme.extension<ScaleScheme>()!;
final textTheme = theme.textTheme;
return ConstrainedBox(
constraints:
BoxConstraints(maxHeight: maxDialogHeight, maxWidth: maxDialogWidth),
child: SingleChildScrollView(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
translate('send_invite_dialog.message_to_contact'),
).paddingAll(8),
TextField(
controller: _messageTextController,
inputFormatters: [
LengthLimitingTextInputFormatter(128),
],
decoration: InputDecoration(
border: const OutlineInputBorder(),
hintText: translate('send_invite_dialog.enter_message_hint'),
labelText: translate('send_invite_dialog.message')),
).paddingAll(8),
const SizedBox(height: 10),
Text(translate('send_invite_dialog.protect_this_invitation'),
style: textTheme.labelLarge)
.paddingAll(8),
Wrap(spacing: 5, children: [
ChoiceChip(
label: Text(translate('send_invite_dialog.unlocked')),
selected: _encryptionKeyType == EncryptionKeyType.none,
onSelected: _onNoneEncryptionSelected,
),
ChoiceChip(
label: Text(translate('send_invite_dialog.pin')),
selected: _encryptionKeyType == EncryptionKeyType.pin,
onSelected: _onPinEncryptionSelected,
),
ChoiceChip(
label: Text(translate('send_invite_dialog.password')),
selected: _encryptionKeyType == EncryptionKeyType.password,
onSelected: _onPasswordEncryptionSelected,
)
]).paddingAll(8),
Container(
width: double.infinity,
height: 60,
padding: const EdgeInsets.all(8),
child: ElevatedButton(
onPressed: _onGenerateButtonPressed,
child: Text(
translate('send_invite_dialog.generate'),
),
),
),
Text(translate('send_invite_dialog.note')).paddingAll(8),
Text(
translate('send_invite_dialog.note_text'),
style: Theme.of(context).textTheme.bodySmall,
).paddingAll(8),
],
),
),
);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<TextEditingController>(
'messageTextController', _messageTextController));
}
}

View file

@ -0,0 +1,66 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:signal_strength_indicator/signal_strength_indicator.dart';
import 'package:go_router/go_router.dart';
import '../providers/connection_state.dart';
import '../tools/tools.dart';
import '../veilid_support/veilid_support.dart';
class SignalStrengthMeterWidget extends ConsumerWidget {
const SignalStrengthMeterWidget({super.key});
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
const iconSize = 16.0;
final connState = ref.watch(connectionStateProvider);
late final double value;
late final Color color;
late final Color inactiveColor;
switch (connState.attachment.state) {
case AttachmentState.detached:
return Icon(Icons.signal_cellular_nodata,
size: iconSize, color: scale.grayScale.text);
case AttachmentState.detaching:
return Icon(Icons.signal_cellular_off,
size: iconSize, color: scale.grayScale.text);
case AttachmentState.attaching:
value = 0;
color = scale.primaryScale.text;
case AttachmentState.attachedWeak:
value = 1;
color = scale.primaryScale.text;
case AttachmentState.attachedStrong:
value = 2;
color = scale.primaryScale.text;
case AttachmentState.attachedGood:
value = 3;
color = scale.primaryScale.text;
case AttachmentState.fullyAttached:
value = 4;
color = scale.primaryScale.text;
case AttachmentState.overAttached:
value = 4;
color = scale.secondaryScale.subtleText;
}
inactiveColor = scale.grayScale.subtleText;
return GestureDetector(
onLongPress: () async {
await context.push('/developer');
},
child: SignalStrengthIndicator.bars(
value: value,
activeColor: color,
inactiveColor: inactiveColor,
size: iconSize,
barCount: 4,
spacing: 1,
));
}
}