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_bloc/flutter_bloc.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:searchable_listview/searchable_listview.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../contact_invitation/contact_invitation.dart'; import '../../proto/proto.dart' as proto; import '../../theme/theme.dart'; import '../cubits/cubits.dart'; import 'contact_item_widget.dart'; import 'empty_contact_list_widget.dart'; enum ContactsBrowserElementKind { contact, invitation, } class ContactsBrowserElement { ContactsBrowserElement.contact(proto.Contact c) : kind = ContactsBrowserElementKind.contact, invitation = null, contact = c; ContactsBrowserElement.invitation(proto.ContactInvitationRecord i) : kind = ContactsBrowserElementKind.invitation, contact = null, invitation = i; String get sortKey => switch (kind) { ContactsBrowserElementKind.contact => contact!.displayName, ContactsBrowserElementKind.invitation => invitation!.recipient + invitation!.message }; final ContactsBrowserElementKind kind; final proto.ContactInvitationRecord? invitation; final proto.Contact? contact; } class ContactsBrowser extends StatefulWidget { const ContactsBrowser( {required this.onContactSelected, required this.onContactDeleted, required this.onStartChat, this.selectedContactRecordKey, super.key}); @override State createState() => _ContactsBrowserState(); final Future Function(proto.Contact? contact) onContactSelected; final Future Function(proto.Contact contact) onContactDeleted; final Future Function(proto.Contact contact) onStartChat; final TypedKey? selectedContactRecordKey; @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties ..add(DiagnosticsProperty( 'selectedContactRecordKey', selectedContactRecordKey)) ..add( ObjectFlagProperty Function(proto.Contact? contact)>.has( 'onContactSelected', onContactSelected)) ..add( ObjectFlagProperty Function(proto.Contact contact)>.has( 'onStartChat', onStartChat)) ..add( ObjectFlagProperty Function(proto.Contact contact)>.has( 'onContactDeleted', onContactDeleted)); } } class _ContactsBrowserState extends State with SingleTickerProviderStateMixin { Widget buildInvitationButton(BuildContext context) { final theme = Theme.of(context); final scaleScheme = theme.extension()!; final scaleConfig = theme.extension()!; final menuIconColor = scaleConfig.preferBorders ? scaleScheme.primaryScale.hoverBorder : scaleScheme.primaryScale.hoverBorder; // final menuBackgroundColor = scaleConfig.preferBorders // ? scaleScheme.primaryScale.activeElementBackground // : scaleScheme.primaryScale.activeElementBackground; // final menuBorderColor = scaleScheme.primaryScale.hoverBorder; PopupMenuEntry makeMenuButton( {required IconData iconData, required String text, void Function()? onTap}) => PopupMenuItem( onTap: onTap, child: Row( spacing: 8.scaled(context), mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Icon(iconData, size: 32.scaled(context)), Text( text, textScaler: MediaQuery.of(context).textScaler, maxLines: 2, textAlign: TextAlign.center, ) ])); final inviteMenuItems = [ makeMenuButton( iconData: Icons.contact_page, text: translate('add_contact_sheet.create_invite'), onTap: () async { await CreateInvitationDialog.show(context); }), makeMenuButton( iconData: Icons.qr_code_scanner, text: translate('add_contact_sheet.scan_invite'), onTap: () async { await ScanInvitationDialog.show(context); }), makeMenuButton( iconData: Icons.paste, text: translate('add_contact_sheet.paste_invite'), onTap: () async { await PasteInvitationDialog.show(context); }), ]; return PopupMenuButton( itemBuilder: (_) => inviteMenuItems, menuPadding: const EdgeInsets.symmetric(vertical: 8).scaled(context), tooltip: translate('add_contact_sheet.add_contact'), child: Icon( size: 32.scaled(context), Icons.person_add, color: menuIconColor)); } @override Widget build(BuildContext context) { final theme = Theme.of(context); final scale = theme.extension()!; final cilState = context.watch().state; final contactInvitationRecordList = cilState.state.asData?.value.map((x) => x.value).toIList() ?? const IListConst([]); final ciState = context.watch().state; final contactList = ciState.state.asData?.value.map((x) => x.value).toIList(); final initialList = []; if (contactList != null) { initialList .addAll(contactList.toList().map(ContactsBrowserElement.contact)); } if (contactInvitationRecordList.isNotEmpty) { initialList.addAll(contactInvitationRecordList .toList() .map(ContactsBrowserElement.invitation)); } initialList.sort((a, b) => a.sortKey.compareTo(b.sortKey)); return Column(children: [ SearchableList( initialList: initialList, itemBuilder: (element) { switch (element.kind) { case ContactsBrowserElementKind.contact: final contact = element.contact!; return ContactItemWidget( contact: contact, selected: widget.selectedContactRecordKey == contact.localConversationRecordKey.toVeilid(), disabled: false, onDoubleTap: _onStartChat, onTap: onContactSelected, onDelete: _onContactDeleted) .paddingLTRB(0, 4.scaled(context), 0, 0); case ContactsBrowserElementKind.invitation: final invitation = element.invitation!; return ContactInvitationItemWidget( contactInvitationRecord: invitation, disabled: false) .paddingLTRB(0, 4.scaled(context), 0, 0); } }, filter: (value) { final lowerValue = value.toLowerCase(); final filtered = []; for (final element in initialList) { switch (element.kind) { case ContactsBrowserElementKind.contact: final contact = element.contact!; if (contact.nickname.toLowerCase().contains(lowerValue) || contact.profile.name .toLowerCase() .contains(lowerValue) || contact.profile.pronouns .toLowerCase() .contains(lowerValue)) { filtered.add(element); } case ContactsBrowserElementKind.invitation: final invitation = element.invitation!; if (invitation.message .toLowerCase() .contains(lowerValue) || invitation.recipient .toLowerCase() .contains(lowerValue)) { filtered.add(element); } } } return filtered; }, searchFieldHeight: 40.scaled(context), listViewPadding: const EdgeInsets.fromLTRB(4, 0, 4, 4).scaled(context), searchFieldPadding: const EdgeInsets.fromLTRB(4, 8, 4, 4).scaled(context), emptyWidget: contactList == null ? waitingPage( text: translate('contact_list.loading_contacts')) : const EmptyContactListWidget(), defaultSuffixIconColor: scale.primaryScale.border, searchFieldEnabled: contactList != null, inputDecoration: InputDecoration(labelText: translate('contact_list.search')), secondaryWidget: buildInvitationButton(context) .paddingLTRB(8.scaled(context), 0, 0, 0)) .expanded() ]); } Future onContactSelected(proto.Contact contact) async { await widget.onContactSelected(contact); } Future _onStartChat(proto.Contact contact) async { await widget.onStartChat(contact); } Future _onContactDeleted(proto.Contact contact) async { await widget.onContactDeleted(contact); } //////////////////////////////////////////////////////////////////////////// }