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:star_menu/star_menu.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../chat_list/chat_list.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.onStartChat, this.selectedContactRecordKey, super.key}); @override State createState() => _ContactsBrowserState(); final Future Function(proto.Contact? contact) onContactSelected; 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)); } } 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; final menuParams = StarMenuParameters( shape: MenuShape.linear, centerOffset: const Offset(0, 64), // backgroundParams: // BackgroundParams(backgroundColor: theme.shadowColor.withAlpha(128)), boundaryBackground: BoundaryBackground( color: menuBackgroundColor, decoration: ShapeDecoration( color: menuBackgroundColor, shape: RoundedRectangleBorder( side: scaleConfig.useVisualIndicators ? BorderSide( width: 2, color: menuBorderColor, strokeAlign: 0) : BorderSide.none, borderRadius: BorderRadius.circular( 8 * scaleConfig.borderRadiusScale))))); ElevatedButton makeMenuButton( {required IconData iconData, required String text, required void Function()? onPressed}) => ElevatedButton.icon( onPressed: onPressed, icon: Icon( iconData, size: 32, ).paddingSTEB(0, 8, 0, 8), label: Text( text, maxLines: 2, textAlign: TextAlign.center, ).paddingSTEB(0, 8, 0, 8)); final inviteMenuItems = [ makeMenuButton( iconData: Icons.paste, text: translate('add_contact_sheet.paste_invite'), onPressed: () async { _invitationMenuController.closeMenu!(); await PasteInvitationDialog.show(context); }), makeMenuButton( iconData: Icons.qr_code_scanner, text: translate('add_contact_sheet.scan_invite'), onPressed: () async { _invitationMenuController.closeMenu!(); await ScanInvitationDialog.show(context); }).paddingLTRB(0, 0, 0, 8), makeMenuButton( iconData: Icons.contact_page, text: translate('add_contact_sheet.create_invite'), onPressed: () async { _invitationMenuController.closeMenu!(); await CreateInvitationDialog.show(context); }).paddingLTRB(0, 0, 0, 8), ]; return StarMenu( items: inviteMenuItems, onItemTapped: (_index, controller) { controller.closeMenu!(); }, controller: _invitationMenuController, params: menuParams, child: IconButton( onPressed: () {}, iconSize: 24, icon: Icon(Icons.person_add, color: menuIconColor), tooltip: translate('add_contact_sheet.add_contact')), ); } @override Widget build(BuildContext context) { final theme = Theme.of(context); final scale = theme.extension()!; //final scaleConfig = theme.extension()!; final cilState = context.watch().state; //final cilBusy = cilState.busy; final contactInvitationRecordList = cilState.state.asData?.value.map((x) => x.value).toIList() ?? const IListConst([]); final ciState = context.watch().state; //final ciBusy = ciState.busy; 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: _onSelectContact, onDelete: _onDeleteContact) .paddingLTRB(0, 4, 0, 0); case ContactsBrowserElementKind.invitation: final invitation = element.invitation!; return ContactInvitationItemWidget( contactInvitationRecord: invitation, disabled: false) .paddingLTRB(0, 4, 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, listViewPadding: const EdgeInsets.fromLTRB(4, 0, 4, 4), searchFieldPadding: const EdgeInsets.fromLTRB(4, 8, 4, 4), emptyWidget: contactList == null ? waitingPage( text: translate('contact_list.loading_contacts')) : const EmptyContactListWidget(), defaultSuffixIconColor: scale.primaryScale.border, closeKeyboardWhenScrolling: true, searchFieldEnabled: contactList != null, inputDecoration: InputDecoration(labelText: translate('contact_list.search')), secondaryWidget: buildInvitationButton(context).paddingLTRB(4, 0, 0, 0)) .expanded() ]); } Future _onSelectContact(proto.Contact contact) async { await widget.onContactSelected(contact); } Future _onStartChat(proto.Contact contact) async { await widget.onStartChat(contact); } Future _onDeleteContact(proto.Contact contact) async { final localConversationRecordKey = contact.localConversationRecordKey.toVeilid(); final contactListCubit = context.read(); final chatListCubit = context.read(); // Delete the contact itself await contactListCubit.deleteContact( localConversationRecordKey: localConversationRecordKey); // Remove any chats for this contact await chatListCubit.deleteChat( localConversationRecordKey: localConversationRecordKey); } //////////////////////////////////////////////////////////////////////////// final _invitationMenuController = StarMenuController(); }