11import 'dart:async' ;
22
3+ import 'package:flutter/foundation.dart' ;
4+
35import 'package:equatable/equatable.dart' ;
46import 'package:flutter_bloc/flutter_bloc.dart' ;
57import 'package:logging/logging.dart' ;
@@ -14,41 +16,84 @@ part 'chat_conversations_state.dart';
1416final _logger = Logger ('ChatConversationsCubit' );
1517
1618class ChatConversationsCubit extends Cubit <ChatConversationsState > {
17- ChatConversationsCubit (this ._client, this ._repository, this ._contactsRepo) : super (ChatConversationsState .initial ()) {
18- init ();
19- }
19+ ChatConversationsCubit (
20+ this ._client,
21+ this ._chatsRepository,
22+ this ._contactsRepository, {
23+ Duration searchDebounceDuration = const Duration (milliseconds: 100 ),
24+ }) : _searchDebounceDuration = searchDebounceDuration,
25+ super (ChatConversationsState .initial ());
2026
2127 final PhoenixSocket _client;
22- final ChatsRepository _repository;
23- final ContactsRepository _contactsRepo;
28+ final ChatsRepository _chatsRepository;
29+ final ContactsRepository _contactsRepository;
30+ final Duration _searchDebounceDuration;
2431
25- late final StreamSubscription _conversationsSub;
32+ StreamSubscription ? _conversationsSub;
33+ String _searchString = '' ;
34+ Timer ? _searchDebounceTimer;
2635
2736 void init () async {
28- _logger.info ('Initialising' );
29-
30- final conversations = await _repository.getChatsWithLastMessages ();
31- final contacts = await _evaluateContacts (conversations.map ((e) => e.$1).toList ());
32- final conversationsWithContacts = _mergeChatsWithContacts (conversations, contacts);
33- conversationsWithContacts.sort (_comparator);
34-
35- emit (ChatConversationsState (conversationsWithContacts, false ));
36- _logger.info ('Initialised: ${conversations .length } chats' );
37-
38- _conversationsSub = _repository.eventBus.listen ((event) async {
39- if (event is ChatUpdate ) {
40- final newList = _mergeWithChatUpdate (event.chat, await _evaluateContacts ([event.chat]));
41- emit (state.copyWith (conversations: newList));
42- }
43- if (event is ChatRemove ) {
44- final newList = _removeChat (event.chatId);
45- emit (state.copyWith (conversations: newList));
46- }
47- if (event is ChatMessageUpdate ) {
48- final newList = _mergeWithMessageUpdate (event.message);
49- emit (state.copyWith (conversations: newList));
50- }
51- });
37+ _conversationsSub? .cancel ();
38+ _conversationsSub = _actionsStream
39+ .asyncMap ((event) async {
40+ if (event is List <(Chat , ChatMessage ?)>) {
41+ final contacts = await _evaluateContacts (event.map ((e) => e.$1).toList ());
42+
43+ final (raw, toShow) = await compute ((data) {
44+ final (conversations, contacts, searchString) = data;
45+ final merged = _mergeChatsWithContacts (conversations, contacts);
46+ merged.sort (_comparator);
47+ final filtered = filterBySearch (searchString, merged);
48+ return (merged, filtered);
49+ }, (event, contacts, _searchString));
50+
51+ return ChatConversationsState (raw, toShow, false );
52+ }
53+ if (event is ChatUpdate ) {
54+ final contacts = await _evaluateContacts ([event.chat]);
55+
56+ final (raw, toShow) = await compute ((data) {
57+ final (chat, contacts, conversations, searchString) = data;
58+ final newList = _mergeWithChatUpdate (chat, contacts, conversations);
59+ final filtered = filterBySearch (searchString, newList);
60+ return (newList, filtered);
61+ }, (event.chat, contacts, state.conversations, _searchString));
62+
63+ return ChatConversationsState (raw, toShow, false );
64+ }
65+ if (event is ChatRemove ) {
66+ final (raw, toShow) = await compute ((data) {
67+ final (chatId, conversations, searchString) = data;
68+ final newList = _removeChat (chatId, conversations);
69+ final filtered = filterBySearch (searchString, newList);
70+ return (newList, filtered);
71+ }, (event.chatId, state.conversations, _searchString));
72+
73+ return ChatConversationsState (raw, toShow, false );
74+ }
75+ if (event is ChatMessageUpdate ) {
76+ final (raw, toShow) = await compute ((data) {
77+ final (msg, chList, searchString) = data;
78+ final newList = _mergeWithMessageUpdate (msg, chList);
79+ final filtered = filterBySearch (searchString, newList);
80+ return (newList, filtered);
81+ }, (event.message, state.conversations, _searchString));
82+
83+ return ChatConversationsState (raw, toShow, false );
84+ }
85+
86+ _logger.info ('list recomputed ${state .conversationsToShow .length } / ${state .conversations .length }' );
87+ })
88+ .listen ((newState) {
89+ if (! isClosed && newState != null ) emit (newState);
90+ });
91+ }
92+
93+ Future <void > updateSearch (String value) async {
94+ _searchString = value;
95+ _searchDebounceTimer? .cancel ();
96+ _searchDebounceTimer = Timer (_searchDebounceDuration, () => _doUpdateSearch ());
5297 }
5398
5499 Future <bool > deleteConversation (int id) async {
@@ -63,54 +108,80 @@ class ChatConversationsCubit extends Cubit<ChatConversationsState> {
63108 return channel.leaveGroup ();
64109 }
65110
111+ Stream get _actionsStream async * {
112+ yield await _chatsRepository.getChatsWithLastMessages ();
113+ yield * _chatsRepository.eventBus;
114+ }
115+
116+ Future <List <Contact >> _evaluateContacts (List <Chat > chats) async {
117+ final userIds = chats.expand ((e) => e.members).map ((e) => e.userId).toSet ();
118+ final q = await Future .wait (
119+ userIds.map ((e) => _contactsRepository.getContactBySource (ContactSourceType .external , e)),
120+ );
121+ return q.nonNulls.toList ();
122+ }
123+
124+ Future <void > _doUpdateSearch () async {
125+ final conversationsToShow = await compute ((data) {
126+ final (searchString, conversations) = data;
127+ return filterBySearch (searchString, conversations);
128+ }, (_searchString, state.conversations));
129+ if (isClosed) return ;
130+ emit (state.copyWith (conversationsToShow: conversationsToShow));
131+ }
132+
66133 /// Sort chat list by last message if available, otherwise by chat update time
67- int _comparator (ChatWithMessageAndMemebers a, ChatWithMessageAndMemebers b) {
134+ static int _comparator (ChatWithMessageAndMemebers a, ChatWithMessageAndMemebers b) {
68135 final aLastActivity = a.message? .createdAt ?? a.chat.updatedAt;
69136 final bLastActivity = b.message? .createdAt ?? b.chat.updatedAt;
70137 return bLastActivity.compareTo (aLastActivity);
71138 }
72139
73- List <ChatWithMessageAndMemebers > _mergeWithChatUpdate (Chat chat, List <Contact > contacts) {
140+ static List <ChatWithMessageAndMemebers > _mergeWithChatUpdate (
141+ Chat chat,
142+ List <Contact > contacts,
143+ List <ChatWithMessageAndMemebers > conversations,
144+ ) {
74145 List <ChatWithMessageAndMemebers > newList;
75- final index = state. conversations.indexWhere ((e) => e.chat.id == chat.id);
146+ final index = conversations.indexWhere ((e) => e.chat.id == chat.id);
76147 if (index == - 1 ) {
77- newList = [(chat: chat, message: null , contacts: contacts), ...state. conversations];
148+ newList = [(chat: chat, message: null , contacts: contacts), ...conversations];
78149 } else {
79- newList = List .of (state. conversations);
150+ newList = List .of (conversations);
80151 final item = newList[index];
81152 newList[index] = (chat: chat, message: item.message, contacts: contacts);
82153 newList.sort (_comparator);
83154 }
84155 return newList;
85156 }
86157
87- List <ChatWithMessageAndMemebers > _mergeWithMessageUpdate (ChatMessage message) {
88- final index = state.conversations.indexWhere ((e) => e.chat.id == message.chatId);
89- final oldMessage = state.conversations[index].message;
158+ static List <ChatWithMessageAndMemebers > _mergeWithMessageUpdate (
159+ ChatMessage message,
160+ List <ChatWithMessageAndMemebers > conversations,
161+ ) {
162+ final index = conversations.indexWhere ((e) => e.chat.id == message.chatId);
163+ final oldMessage = conversations[index].message;
90164 final isOldMessageNewer = oldMessage != null && oldMessage.createdAt.isAfter (message.createdAt);
91165
92166 if (index != - 1 && ! isOldMessageNewer) {
93- final newList = List .of (state. conversations);
167+ final newList = List .of (conversations);
94168 final oldItem = newList[index];
95169 newList[index] = (chat: oldItem.chat, message: message, contacts: oldItem.contacts);
96170 newList.sort (_comparator);
97171 return newList;
98172 }
99173
100- return state. conversations;
174+ return conversations;
101175 }
102176
103- List <ChatWithMessageAndMemebers > _removeChat (int chatId) {
104- return state.conversations.where ((e) => e.chat.id != chatId).toList ();
105- }
106-
107- Future <List <Contact >> _evaluateContacts (List <Chat > chats) async {
108- final userIds = chats.expand ((e) => e.members).map ((e) => e.userId).toSet ();
109- final q = await Future .wait (userIds.map ((e) => _contactsRepo.getContactBySource (ContactSourceType .external , e)));
110- return q.nonNulls.toList ();
177+ static List <ChatWithMessageAndMemebers > _removeChat (int chatId, List <ChatWithMessageAndMemebers > conversations) {
178+ return conversations.where ((e) => e.chat.id != chatId).toList ();
111179 }
112180
113- List <ChatWithMessageAndMemebers > _mergeChatsWithContacts (List <(Chat , ChatMessage ?)> chats, List <Contact > contacts) {
181+ static List <ChatWithMessageAndMemebers > _mergeChatsWithContacts (
182+ List <(Chat , ChatMessage ?)> chats,
183+ List <Contact > contacts,
184+ ) {
114185 final contactMap = {for (final contact in contacts) contact.sourceId: contact};
115186 return chats.map ((e) {
116187 final (chat, lastMessage) = e;
@@ -119,9 +190,33 @@ class ChatConversationsCubit extends Cubit<ChatConversationsState> {
119190 }).toList ();
120191 }
121192
193+ static List <ChatWithMessageAndMemebers > filterBySearch (
194+ String search,
195+ List <ChatWithMessageAndMemebers > conversations,
196+ ) {
197+ if (search.isEmpty) return conversations;
198+
199+ return conversations.where ((e) {
200+ var (: chat, : message, : contacts) = e;
201+
202+ final groupName = chat.name? .toLowerCase ();
203+ final contactNames = contacts
204+ .where ((e) => e.isCurrentUser == false )
205+ .map ((e) => '${e .aliasName } + ${e .firstName } + ${e .lastName }' .toLowerCase ())
206+ .join (' ' );
207+ final contactPhones = contacts.expand ((e) => e.phones).map ((e) => e.number).join (' ' );
208+ final lastMessageText = message? .content ?? '' ;
209+
210+ return groupName? .contains (search) == true ||
211+ contactNames.contains (search) ||
212+ contactPhones.contains (search) ||
213+ lastMessageText.contains (search);
214+ }).toList ();
215+ }
216+
122217 @override
123218 Future <void > close () {
124- _conversationsSub.cancel ();
219+ _conversationsSub? .cancel ();
125220 return super .close ();
126221 }
127222}
0 commit comments