Skip to content

Commit 523a764

Browse files
feat: ✨ Add Typing Indicator in ChatViewList
1 parent c9da40e commit 523a764

24 files changed

+623
-288
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,6 @@ doc/api/
2222
.idea/
2323
.flutter-plugins
2424
/.flutter-plugins-dependencies
25-
chat_ui.iml
25+
chat_ui.iml
26+
27+
devtools_options.yaml

example/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
.pub-cache/
3131
.pub/
3232
/build/
33+
devtools_options.yaml
3334

3435
# Web related
3536

example/lib/chat_view_list_screen.dart

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ class _ChatViewListScreenState extends State<ChatViewListScreen> {
2020
final today = DateTime(now.year, now.month, now.day);
2121
final yesterday = DateTime(now.year, now.month, now.day - 1);
2222
final lastMessageTime = DateTime.parse('2025-06-06T13:58:00.000Z');
23-
var initialUsersList = [
23+
final initialChatList = [
2424
ChatViewListModel(
2525
id: '1',
26-
name: 'Breaking Bad',
26+
name: 'Breaking Bad Group',
2727
lastMessage: Message(
2828
message:
2929
'I am not in danger, Skyler. I am the danger. A guy opens his door and gets shot and you think that of me? No. I am the one who knocks!',
@@ -93,7 +93,7 @@ class _ChatViewListScreenState extends State<ChatViewListScreen> {
9393
];
9494

9595
controller = ChatViewListController(
96-
initialUsersList: initialUsersList,
96+
initialChatList: initialChatList,
9797
scrollController: ScrollController(),
9898
);
9999
super.initState();
@@ -113,14 +113,20 @@ class _ChatViewListScreenState extends State<ChatViewListScreen> {
113113
appbar: const ChatViewListAppBar(
114114
title: 'Breaking Bad',
115115
),
116+
listTypeIndicatorConfig: const ListTypeIndicatorConfig(
117+
showUserNames: true,
118+
),
116119
config: ChatViewListConfig(
120+
unreadCountConfig: const UnreadCountConfig(
121+
style: UnreadCountStyle.count,
122+
),
117123
chatViewListTileConfig: ChatViewListTileConfig(
118124
backgroundColor: Colors.blue,
119-
onTap: (user) {
125+
onTap: (chat) {
120126
Navigator.of(context).push(
121127
MaterialPageRoute(
122128
builder: (context) => ChatScreen(
123-
user: user,
129+
chat: chat,
124130
),
125131
),
126132
);
@@ -135,9 +141,9 @@ class _ChatViewListScreenState extends State<ChatViewListScreen> {
135141
if (value.isEmpty) {
136142
return null;
137143
}
138-
final list = controller.initialUsersList
139-
.where((user) =>
140-
user.name.toLowerCase().contains(value.toLowerCase()))
144+
final list = controller.chatListMap.values
145+
.where((chat) =>
146+
chat.name.toLowerCase().contains(value.toLowerCase()))
141147
.toList();
142148
return list;
143149
},

example/lib/main.dart

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,9 @@ class Example extends StatelessWidget {
2727
}
2828

2929
class ChatScreen extends StatefulWidget {
30-
const ChatScreen({
31-
super.key,
32-
required this.user,
33-
});
30+
const ChatScreen({required this.chat, super.key});
3431

35-
final ChatViewListModel user;
32+
final ChatViewListModel chat;
3633

3734
@override
3835
State<ChatScreen> createState() => _ChatScreenState();
@@ -144,17 +141,17 @@ class _ChatScreenState extends State<ChatScreen> {
144141
appBar: ChatViewAppBar(
145142
elevation: theme.elevation,
146143
backGroundColor: theme.appBarColor,
147-
profilePicture: widget.user.imageUrl,
144+
profilePicture: widget.chat.imageUrl,
148145
backArrowColor: theme.backArrowColor,
149-
chatTitle: widget.user.name,
146+
chatTitle: widget.chat.name,
150147
chatTitleTextStyle: TextStyle(
151148
color: theme.appBarTitleTextStyle,
152149
fontWeight: FontWeight.bold,
153150
fontSize: 18,
154151
letterSpacing: 0.25,
155152
),
156153
userStatus:
157-
widget.user.userActiveStatus.isOnline ? 'Online' : 'Offline',
154+
widget.chat.userActiveStatus.isOnline ? 'Online' : 'Offline',
158155
userStatusTextStyle: const TextStyle(color: Colors.grey),
159156
actions: [
160157
IconButton(

lib/chatview.dart

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ library chatview;
2424

2525
export 'package:audio_waveforms/audio_waveforms.dart'
2626
show
27-
WaveStyle,
28-
PlayerWaveStyle,
2927
AndroidEncoder,
28+
AndroidOutputFormat,
3029
IosEncoder,
31-
AndroidOutputFormat;
30+
PlayerWaveStyle,
31+
WaveStyle;
3232
export 'package:chatview_utils/chatview_utils.dart';
3333
export 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
3434

@@ -40,7 +40,6 @@ export 'src/models/config_models/chat_view_list_user_config.dart';
4040
export 'src/models/config_models/load_more_widget_config.dart';
4141
export 'src/models/config_models/receipts_widget_config.dart';
4242
export 'src/models/config_models/search_config.dart';
43-
export 'src/models/config_models/unread_widget_config.dart';
4443
export 'src/models/models.dart';
4544
export 'src/utils/chat_view_locale.dart';
4645
export 'src/utils/package_strings.dart';

lib/src/chat_list_view_controller.dart

Lines changed: 73 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -24,56 +24,102 @@ import 'dart:async';
2424
import 'package:flutter/material.dart';
2525

2626
import 'models/chat_view_list_tile.dart';
27+
import 'values/typedefs.dart';
2728

2829
class ChatViewListController {
2930
ChatViewListController({
30-
required this.initialUsersList,
31+
required List<ChatViewListModel> initialChatList,
3132
required this.scrollController,
32-
});
33+
}) {
34+
final chatListLength = initialChatList.length;
3335

34-
/// Represents initial chat list users.
35-
List<ChatViewListModel> initialUsersList = [];
36+
final chatsMap = {
37+
for (var i = 0; i < chatListLength; i++)
38+
if (initialChatList[i] case final chat) chat.id: chat,
39+
};
40+
41+
chatListMap = chatsMap;
42+
43+
// Adds the current chat map to the stream controller
44+
// after the first frame render.
45+
Future.delayed(
46+
Duration.zero,
47+
() => _chatListStreamController.add(chatListMap),
48+
);
49+
}
50+
51+
/// Stores and manages chat items by their unique IDs.
52+
/// A map is used for efficient lookup, update, and removal of chats
53+
/// by their unique id.
54+
Map<String, ChatViewListModel> chatListMap = {};
3655

3756
/// Provides scroll controller for chat list.
3857
ScrollController scrollController;
3958

40-
/// Represents chat list user stream
41-
StreamController<List<ChatViewListModel>> chatListStreamController =
42-
StreamController.broadcast();
59+
/// Stream controller to manage the chat list stream.
60+
final StreamController<Map<String, ChatViewListModel>>
61+
_chatListStreamController =
62+
StreamController<Map<String, ChatViewListModel>>.broadcast();
4363

44-
/// Used to add user in the chat list.
45-
void addUser(ChatViewListModel user) {
46-
initialUsersList.add(user);
47-
if (chatListStreamController.isClosed) return;
48-
chatListStreamController.sink.add(initialUsersList);
49-
}
64+
late final Stream<List<ChatViewListModel>> chatListStream =
65+
_chatListStreamController.stream.map(
66+
(chatMap) => chatMap.values.toList(),
67+
);
5068

51-
/// Function for loading data while pagination.
52-
void loadMoreUsers(List<ChatViewListModel> userList) {
53-
initialUsersList.addAll(userList);
54-
if (chatListStreamController.isClosed) return;
55-
chatListStreamController.sink.add(initialUsersList);
69+
/// Adds a chat to the chat list.
70+
void addChat(ChatViewListModel chat) {
71+
chatListMap[chat.id] = chat;
72+
if (_chatListStreamController.isClosed) return;
73+
_chatListStreamController.add(chatListMap);
5674
}
5775

58-
/// Function to add search results of the chat list in the stream.
59-
void updateChatList(List<ChatViewListModel> searchResults) {
60-
WidgetsBinding.instance.addPostFrameCallback(
61-
(_) {
62-
if (chatListStreamController.isClosed) return;
63-
chatListStreamController.sink.add(searchResults);
76+
/// Function for loading data while pagination.
77+
void loadMoreChats(List<ChatViewListModel> chatList) {
78+
final chatListLength = chatList.length;
79+
chatListMap.addAll(
80+
{
81+
for (var i = 0; i < chatListLength; i++)
82+
if (chatList[i] case final chat) chat.id: chat,
6483
},
6584
);
85+
if (_chatListStreamController.isClosed) return;
86+
_chatListStreamController.add(chatListMap);
87+
}
88+
89+
/// Updates the chat entry in [chatListMap] for the given [chatId] using
90+
/// the provided [newChat] callback.
91+
///
92+
/// If the chat with [chatId] does not exist, the method returns without
93+
/// making changes.
94+
void updateChat(String chatId, UpdateChatCallback newChat) {
95+
final chat = chatListMap[chatId];
96+
if (chat == null) return;
97+
98+
chatListMap[chatId] = newChat(chat);
99+
if (_chatListStreamController.isClosed) return;
100+
_chatListStreamController.add(chatListMap);
101+
}
102+
103+
/// Adds the given chat search results to the stream after the current frame.
104+
void setSearchChats(List<ChatViewListModel> searchResults) {
105+
final searchResultLength = searchResults.length;
106+
final searchResultMap = {
107+
for (var i = 0; i < searchResultLength; i++)
108+
if (searchResults[i] case final chat) chat.id: chat,
109+
};
110+
if (_chatListStreamController.isClosed) return;
111+
_chatListStreamController.add(searchResultMap);
66112
}
67113

68114
/// Function to clear the search results and show the original chat list.
69115
void clearSearch() {
70-
if (chatListStreamController.isClosed) return;
71-
chatListStreamController.sink.add(initialUsersList);
116+
if (_chatListStreamController.isClosed) return;
117+
_chatListStreamController.add(chatListMap);
72118
}
73119

74120
/// Used to dispose ValueNotifiers and Streams.
75121
void dispose() {
76122
scrollController.dispose();
77-
chatListStreamController.close();
123+
_chatListStreamController.close();
78124
}
79125
}

lib/src/extensions/extensions.dart

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import 'package:intl/intl.dart';
2626

2727
import '../inherited_widgets/configurations_inherited_widgets.dart';
2828
import '../models/config_models/chat_bubble_configuration.dart';
29+
import '../models/config_models/chat_list/list_type_indicator_config.dart';
2930
import '../models/config_models/message_list_configuration.dart';
3031
import '../models/config_models/reply_suggestions_config.dart';
3132
import '../utils/constants/constants.dart';
@@ -143,6 +144,37 @@ extension ChatViewStateTitleExtension on String? {
143144
}
144145
}
145146

147+
extension TypingIndicatorExtension on List<String> {
148+
String toTypingStatus(ListTypeIndicatorConfig listTypeIndicatorConfig) {
149+
final prefix = listTypeIndicatorConfig.prefix ?? '';
150+
final suffix = listTypeIndicatorConfig.suffix ?? '';
151+
final showUserNames = listTypeIndicatorConfig.showUserNames;
152+
final locale = PackageStrings.currentLocale;
153+
final text = '$prefix${locale.typing}$suffix';
154+
if (isEmpty) return text;
155+
156+
final count = length;
157+
158+
final firstName = this[0];
159+
160+
if (count == 1) {
161+
return '$firstName ${locale.isVerb} $text';
162+
} else if (count == 2) {
163+
final newText = showUserNames
164+
? '$firstName & ${this[1]} ${locale.areVerb}'
165+
: '$firstName & 1 ${locale.other} ${locale.isVerb}';
166+
return '$newText $text';
167+
} else if (showUserNames && count == 3) {
168+
return '${this[0]}, ${this[1]} & ${this[2]} ${locale.areVerb} $text';
169+
} else {
170+
final newText = showUserNames
171+
? '$firstName, ${this[1]} & ${length - 2}'
172+
: '$firstName & ${count - 1}';
173+
return '$newText ${locale.others} ${locale.areVerb} $text';
174+
}
175+
}
176+
}
177+
146178
/// Extension on State for accessing inherited widget.
147179
extension StatefulWidgetExtension on State {
148180
ChatViewInheritedWidget? get chatViewIW =>

lib/src/models/chat_view_list_tile.dart

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,23 @@
2222
import 'package:chatview_utils/chatview_utils.dart';
2323

2424
import '../values/enumeration.dart';
25+
import '../values/typedefs.dart';
26+
import 'omit.dart';
2527

2628
/// Model class representing a user or group in the chat list.
2729
class ChatViewListModel {
30+
/// Creates a user or group object for the chat list.
31+
const ChatViewListModel({
32+
required this.id,
33+
required this.name,
34+
this.chatType = ChatType.user,
35+
this.typingUsers = const <String>{},
36+
this.userActiveStatus = UserActiveStatus.offline,
37+
this.lastMessage,
38+
this.imageUrl,
39+
this.unreadCount,
40+
});
41+
2842
/// Unique identifier for the user or group.
2943
final String id;
3044

@@ -47,14 +61,33 @@ class ChatViewListModel {
4761
/// Defaults to [UserActiveStatus.offline].
4862
final UserActiveStatus userActiveStatus;
4963

50-
/// Creates a user or group object for the chat list.
51-
const ChatViewListModel({
52-
required this.id,
53-
required this.name,
54-
this.lastMessage,
55-
this.imageUrl,
56-
this.unreadCount,
57-
this.chatType = ChatType.user,
58-
this.userActiveStatus = UserActiveStatus.offline,
59-
});
64+
// TODO(YASH): Switch to User Object instead of string.
65+
/// Set of users currently typing in the chat.
66+
final Set<String> typingUsers;
67+
68+
ChatViewListModel copyWith({
69+
Defaulted<String> id = const Omit(),
70+
Defaulted<String> name = const Omit(),
71+
Defaulted<ChatType> chatType = const Omit(),
72+
Defaulted<Set<String>> typingUsers = const Omit(),
73+
Defaulted<UserActiveStatus> userActiveStatus = const Omit(),
74+
Defaulted<Message>? lastMessage = const Omit(),
75+
Defaulted<String>? imageUrl = const Omit(),
76+
Defaulted<int>? unreadCount = const Omit(),
77+
}) {
78+
return ChatViewListModel(
79+
id: id is Omit ? this.id : id as String,
80+
name: name is Omit ? this.name : name as String,
81+
chatType: chatType is Omit ? this.chatType : chatType as ChatType,
82+
typingUsers:
83+
typingUsers is Omit ? this.typingUsers : typingUsers as Set<String>,
84+
userActiveStatus: userActiveStatus is Omit
85+
? this.userActiveStatus
86+
: userActiveStatus as UserActiveStatus,
87+
lastMessage:
88+
lastMessage is Omit ? this.lastMessage : lastMessage as Message?,
89+
imageUrl: imageUrl is Omit ? this.imageUrl : imageUrl as String?,
90+
unreadCount: unreadCount is Omit ? this.unreadCount : unreadCount as int?,
91+
);
92+
}
6093
}

0 commit comments

Comments
 (0)