Skip to content

Commit 5b887a2

Browse files
authored
refactor(messaging): conversations screen improvements (#884)
1 parent 7ffbd56 commit 5b887a2

14 files changed

Lines changed: 562 additions & 365 deletions
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export 'cubit/chat_conversations_cubit.dart';
22
export 'cubit/sms_conversations_cubit.dart';
33
export 'view/conversations_screen.dart';
4+
export 'view/conversations_screen_unsupported.dart';
45
export 'view/conversations_screen_page.dart';
56
export 'widgets/widgets.dart';
Lines changed: 145 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import 'dart:async';
22

3+
import 'package:flutter/foundation.dart';
4+
35
import 'package:equatable/equatable.dart';
46
import 'package:flutter_bloc/flutter_bloc.dart';
57
import 'package:logging/logging.dart';
@@ -14,41 +16,84 @@ part 'chat_conversations_state.dart';
1416
final _logger = Logger('ChatConversationsCubit');
1517

1618
class 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
}

lib/features/messaging/features/conversations/cubit/chat_conversations_state.dart

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,26 @@ part of 'chat_conversations_cubit.dart';
22

33
class ChatConversationsState with EquatableMixin {
44
final List<ChatWithMessageAndMemebers> conversations;
5+
final List<ChatWithMessageAndMemebers> conversationsToShow;
56
final bool initialising;
67

7-
ChatConversationsState(this.conversations, this.initialising);
8+
ChatConversationsState(this.conversations, this.conversationsToShow, this.initialising);
89

9-
factory ChatConversationsState.initial() => ChatConversationsState([], true);
10+
factory ChatConversationsState.initial() => ChatConversationsState([], [], true);
1011

1112
@override
12-
List<Object?> get props => [conversations, initialising];
13+
List<Object?> get props => [conversations, conversationsToShow, initialising];
1314

14-
ChatConversationsState copyWith({List<ChatWithMessageAndMemebers>? conversations, bool? initialising}) {
15-
return ChatConversationsState(conversations ?? this.conversations, initialising ?? this.initialising);
15+
ChatConversationsState copyWith({
16+
List<ChatWithMessageAndMemebers>? conversations,
17+
List<ChatWithMessageAndMemebers>? conversationsToShow,
18+
bool? initialising,
19+
}) {
20+
return ChatConversationsState(
21+
conversations ?? this.conversations,
22+
conversationsToShow ?? this.conversationsToShow,
23+
initialising ?? this.initialising,
24+
);
1625
}
1726
}
1827

0 commit comments

Comments
 (0)