@@ -73,6 +73,9 @@ class ChatCreatorState extends OptimizedState<ChatCreator> {
7373 ConversationViewController ? oldController;
7474 Timer ? _debounce;
7575 Completer <void >? createCompleter;
76+ int selectedSuggestionIndex = - 1 ;
77+ int previousSuggestionIndex = - 1 ;
78+ final Map <int , GlobalKey > suggestionKeys = {};
7679
7780 bool canCreateGroupChats = backend.canCreateGroupChats ();
7881
@@ -134,6 +137,12 @@ class ChatCreatorState extends OptimizedState<ChatCreator> {
134137 if (addressController.text.isNotEmpty) {
135138 filteredChats.sort ((a, b) => a.participants.length.compareTo (b.participants.length));
136139 }
140+ final totalSuggestions = _totalSuggestions;
141+ if (addressController.text.isEmpty || totalSuggestions == 0 ) {
142+ selectedSuggestionIndex = - 1 ;
143+ } else if (selectedSuggestionIndex < 0 || selectedSuggestionIndex >= totalSuggestions) {
144+ selectedSuggestionIndex = 0 ;
145+ }
137146 });
138147 });
139148 });
@@ -216,6 +225,69 @@ class ChatCreatorState extends OptimizedState<ChatCreator> {
216225 findExistingChat ();
217226 }
218227
228+ GlobalKey _suggestionKey (int index) => suggestionKeys.putIfAbsent (index, () => GlobalKey ());
229+
230+ void _scrollSelectedSuggestionIntoView () {
231+ if (selectedSuggestionIndex < 0 ) return ;
232+ final movingUp = previousSuggestionIndex >= 0 && selectedSuggestionIndex < previousSuggestionIndex;
233+ WidgetsBinding .instance.addPostFrameCallback ((_) {
234+ if (! mounted) return ;
235+ final context = _suggestionKey (selectedSuggestionIndex).currentContext;
236+ if (context == null ) return ;
237+ Scrollable .ensureVisible (
238+ context,
239+ duration: const Duration (milliseconds: 150 ),
240+ alignment: movingUp ? 0 : 1 ,
241+ alignmentPolicy: ScrollPositionAlignmentPolicy .explicit,
242+ );
243+ });
244+ }
245+
246+ int get _totalSuggestions {
247+ int contactSuggestions = 0 ;
248+ for (final contact in filteredContacts) {
249+ contactSuggestions += getUniqueNumbers (contact.phones).length + getUniqueEmails (contact.emails).length;
250+ }
251+ return filteredChats.length + contactSuggestions;
252+ }
253+
254+ Future <bool > _selectSuggestionAt (int index) async {
255+ if (index < 0 ) return false ;
256+ if (index < filteredChats.length) {
257+ addSelectedList (filteredChats[index].participants
258+ .where ((e) => selectedContacts.firstWhereOrNull ((c) => c.address == e.address) == null )
259+ .map ((e) => SelectedContact (
260+ displayName: e.displayName,
261+ address: e.address,
262+ isIMessage: filteredChats[index].isIMessage,
263+ )));
264+ return true ;
265+ }
266+
267+ int contactIndex = index - filteredChats.length;
268+ for (final contact in filteredContacts) {
269+ final phones = getUniqueNumbers (contact.phones);
270+ if (contactIndex < phones.length) {
271+ final address = phones[contactIndex];
272+ if (selectedContacts.firstWhereOrNull ((c) => c.address == address) != null ) return true ;
273+ await addSelected (SelectedContact (displayName: contact.displayName, address: address));
274+ return true ;
275+ }
276+ contactIndex -= phones.length;
277+
278+ final emails = getUniqueEmails (contact.emails);
279+ if (contactIndex < emails.length) {
280+ final address = emails[contactIndex];
281+ if (selectedContacts.firstWhereOrNull ((c) => c.address == address) != null ) return true ;
282+ await addSelected (SelectedContact (displayName: contact.displayName, address: address));
283+ return true ;
284+ }
285+ contactIndex -= emails.length;
286+ }
287+
288+ return false ;
289+ }
290+
219291 Future <Chat ?> findExistingChat ({bool checkDeleted = false , bool update = true }) async {
220292 // no selected items, remove message view
221293 if (selectedContacts.isEmpty) {
@@ -318,6 +390,9 @@ class ChatCreatorState extends OptimizedState<ChatCreator> {
318390 }
319391
320392 Future <void > addressOnSubmitted () async {
393+ if (selectedSuggestionIndex >= 0 && await _selectSuggestionAt (selectedSuggestionIndex)) {
394+ return ;
395+ }
321396 final text = addressController.text;
322397 if (text.isEmail || text.isPhoneNumber) {
323398 await addSelected (SelectedContact (
@@ -500,6 +575,32 @@ class ChatCreatorState extends OptimizedState<ChatCreator> {
500575 event.logicalKey == LogicalKeyboardKey .tab) {
501576 messageNode.requestFocus ();
502577 return KeyEventResult .handled;
578+ } else if (event.logicalKey == LogicalKeyboardKey .arrowDown && _totalSuggestions > 0 ) {
579+ if (selectedSuggestionIndex >= _totalSuggestions - 1 ) {
580+ messageNode.requestFocus ();
581+ return KeyEventResult .handled;
582+ }
583+ setState (() {
584+ previousSuggestionIndex = selectedSuggestionIndex;
585+ selectedSuggestionIndex = min (
586+ selectedSuggestionIndex < 0 ? 0 : selectedSuggestionIndex + 1 ,
587+ _totalSuggestions - 1 ,
588+ );
589+ });
590+ _scrollSelectedSuggestionIntoView ();
591+ return KeyEventResult .handled;
592+ } else if (event.logicalKey == LogicalKeyboardKey .arrowUp && _totalSuggestions > 0 ) {
593+ setState (() {
594+ previousSuggestionIndex = selectedSuggestionIndex;
595+ selectedSuggestionIndex = max (selectedSuggestionIndex - 1 , 0 );
596+ });
597+ _scrollSelectedSuggestionIntoView ();
598+ return KeyEventResult .handled;
599+ } else if ((event.logicalKey == LogicalKeyboardKey .enter ||
600+ event.logicalKey == LogicalKeyboardKey .select) &&
601+ ! HardwareKeyboard .instance.isShiftPressed) {
602+ addressOnSubmitted ();
603+ return KeyEventResult .handled;
503604 }
504605 }
505606 return KeyEventResult .ignored;
@@ -643,8 +744,10 @@ class ChatCreatorState extends OptimizedState<ChatCreator> {
643744 _title =
644745 chat.participants.length > 1 ? "Group Chat" : chat.participants[0 ].fakeName;
645746 }
747+ final isSelectedSuggestion = selectedSuggestionIndex == index;
646748 return Material (
647- color: Colors .transparent,
749+ key: _suggestionKey (index),
750+ color: isSelectedSuggestion ? context.theme.colorScheme.outline.withOpacity (0.2 ) : Colors .transparent,
648751 child: InkWell (
649752 onTap: () {
650753 addSelectedList (chat.participants
@@ -678,44 +781,56 @@ class ChatCreatorState extends OptimizedState<ChatCreator> {
678781 SliverList (
679782 delegate: SliverChildBuilderDelegate (
680783 (context, index) {
681- final contact = filteredContacts[index];
682- contact.phones = getUniqueNumbers (contact.phones);
683- contact.emails = getUniqueEmails (contact.emails);
684- final hideInfo =
685- ss.settings.redactedMode.value && ss.settings.hideContactInfo.value;
686- return Column (
687- key: ValueKey (contact.id),
688- mainAxisSize: MainAxisSize .min,
689- children: [
690- ...contact.phones.map ((e) => Material (
691- color: Colors .transparent,
784+ final contact = filteredContacts[index];
785+ final phones = getUniqueNumbers (contact.phones);
786+ final emails = getUniqueEmails (contact.emails);
787+ contact.phones = phones;
788+ contact.emails = emails;
789+ final hideInfo =
790+ ss.settings.redactedMode.value && ss.settings.hideContactInfo.value;
791+ int suggestionOffset = filteredChats.length;
792+ for (int i = 0 ; i < index; i++ ) {
793+ suggestionOffset += getUniqueNumbers (filteredContacts[i].phones).length + getUniqueEmails (filteredContacts[i].emails).length;
794+ }
795+ return Column (
796+ key: ValueKey (contact.id),
797+ mainAxisSize: MainAxisSize .min,
798+ children: [
799+ ...phones.asMap ().entries.map ((entry) => Material (
800+ key: _suggestionKey (suggestionOffset + entry.key),
801+ color: selectedSuggestionIndex == suggestionOffset + entry.key
802+ ? context.theme.colorScheme.outline.withOpacity (0.2 )
803+ : Colors .transparent,
692804 child: InkWell (
693805 onTap: () {
694- if (selectedContacts.firstWhereOrNull ((c) => c.address == e ) !=
806+ if (selectedContacts.firstWhereOrNull ((c) => c.address == entry.value ) !=
695807 null ) return ;
696808 addSelected (
697- SelectedContact (displayName: contact.displayName, address: e ));
809+ SelectedContact (displayName: contact.displayName, address: entry.value ));
698810 },
699811 child: ChatCreatorTile (
700812 title: hideInfo ? "Contact" : contact.displayName,
701- subtitle: hideInfo ? "" : e ,
813+ subtitle: hideInfo ? "" : entry.value ,
702814 contact: contact,
703815 format: true ,
704816 ),
705817 ),
706818 )),
707- ...contact.emails.map ((e) => Material (
708- color: Colors .transparent,
819+ ...emails.asMap ().entries.map ((entry) => Material (
820+ key: _suggestionKey (suggestionOffset + phones.length + entry.key),
821+ color: selectedSuggestionIndex == suggestionOffset + phones.length + entry.key
822+ ? context.theme.colorScheme.outline.withOpacity (0.2 )
823+ : Colors .transparent,
709824 child: InkWell (
710825 onTap: () {
711- if (selectedContacts.firstWhereOrNull ((c) => c.address == e ) !=
826+ if (selectedContacts.firstWhereOrNull ((c) => c.address == entry.value ) !=
712827 null ) return ;
713828 addSelected (
714- SelectedContact (displayName: contact.displayName, address: e ));
829+ SelectedContact (displayName: contact.displayName, address: entry.value ));
715830 },
716831 child: ChatCreatorTile (
717832 title: hideInfo ? "Contact" : contact.displayName,
718- subtitle: hideInfo ? "" : e ,
833+ subtitle: hideInfo ? "" : entry.value ,
719834 contact: contact,
720835 ),
721836 ),
0 commit comments