Skip to content

Commit b02c8a3

Browse files
dhyash-simformaditya-css
authored andcommitted
feat: ✨ Add Animation in ChatViewList (#371)
1 parent 40e2bb8 commit b02c8a3

File tree

10 files changed

+945
-141
lines changed

10 files changed

+945
-141
lines changed

example/lib/chat_operations.dart

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import 'dart:math';
2+
3+
import 'package:chatview/chatview.dart';
4+
import 'package:flutter/material.dart';
5+
6+
class ChatOperations extends StatelessWidget {
7+
const ChatOperations({required this.controller, super.key});
8+
9+
final ChatViewListController controller;
10+
11+
@override
12+
Widget build(BuildContext context) {
13+
return Row(
14+
mainAxisSize: MainAxisSize.min,
15+
children: [
16+
FloatingActionButton.small(
17+
child: const Icon(Icons.add),
18+
onPressed: () {
19+
controller.addChat(
20+
ChatViewListItem(
21+
id: DateTime.now().millisecondsSinceEpoch.toString(),
22+
name: 'New Chat ${DateTime.now().millisecondsSinceEpoch}',
23+
lastMessage: Message(
24+
message: 'Hello, this is a new chat!',
25+
createdAt: DateTime.now(),
26+
sentBy: 'User',
27+
id: 'msg-${DateTime.now().millisecondsSinceEpoch}',
28+
),
29+
),
30+
);
31+
},
32+
),
33+
const SizedBox(width: 6),
34+
FloatingActionButton.small(
35+
child: const Icon(Icons.remove),
36+
onPressed: () {
37+
final key = controller.chatListMap.keys.firstOrNull;
38+
if (key == null) return;
39+
controller.removeChat(key);
40+
},
41+
),
42+
const SizedBox(width: 6),
43+
FloatingActionButton.small(
44+
child: const Icon(Icons.edit),
45+
onPressed: () {
46+
final randomChatIndex =
47+
Random().nextInt(controller.chatListMap.length);
48+
final chatId =
49+
controller.chatListMap.keys.elementAt(randomChatIndex);
50+
51+
controller.updateChat(
52+
chatId,
53+
(previousChat) {
54+
final newPinStatus = switch (previousChat.settings.pinStatus) {
55+
PinStatus.pinned => PinStatus.unpinned,
56+
PinStatus.unpinned => PinStatus.pinned,
57+
};
58+
return previousChat.copyWith(
59+
settings: previousChat.settings.copyWith(
60+
pinStatus: newPinStatus,
61+
pinTime: newPinStatus.isPinned ? DateTime.now() : null,
62+
),
63+
);
64+
},
65+
);
66+
},
67+
),
68+
const SizedBox(width: 6),
69+
FloatingActionButton.small(
70+
child: const Icon(Icons.person),
71+
onPressed: () {
72+
final randomChatIndex =
73+
Random().nextInt(controller.chatListMap.length);
74+
final chatId =
75+
controller.chatListMap.keys.elementAt(randomChatIndex);
76+
controller.updateChat(
77+
chatId,
78+
(previousChat) => previousChat.copyWith(
79+
name: 'Updated Chat ${Random().nextInt(100)}',
80+
),
81+
);
82+
},
83+
),
84+
const SizedBox(width: 6),
85+
FloatingActionButton.small(
86+
child: const Icon(Icons.more_horiz),
87+
onPressed: () {
88+
final chatId = controller.chatListMap.keys.elementAt(0);
89+
controller.updateChat(
90+
chatId,
91+
(previousChat) => previousChat.copyWith(
92+
typingUsers: previousChat.typingUsers.isEmpty
93+
? {const ChatUser(id: '1', name: 'John Doe')}
94+
: {},
95+
),
96+
);
97+
},
98+
),
99+
const SizedBox(width: 6),
100+
FloatingActionButton.small(
101+
child: const Icon(Icons.message),
102+
onPressed: () {
103+
final randomChatIndex =
104+
Random().nextInt(controller.chatListMap.length);
105+
final chatId =
106+
controller.chatListMap.keys.elementAt(randomChatIndex);
107+
final randomId = Random().nextInt(100);
108+
controller.updateChat(
109+
chatId,
110+
(previousChat) => previousChat.copyWith(
111+
lastMessage: Message(
112+
message: 'Random message $randomId',
113+
createdAt: DateTime.now(),
114+
sentBy: previousChat.lastMessage?.sentBy ?? '',
115+
id: '$randomId',
116+
),
117+
),
118+
);
119+
},
120+
),
121+
],
122+
);
123+
}
124+
}

lib/src/controller/chat_list_view_controller.dart

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import 'package:flutter/material.dart';
2626
import '../models/chat_view_list_item.dart';
2727
import '../values/enumeration.dart';
2828
import '../values/typedefs.dart';
29+
import '../widgets/chat_view_list/auto_animate_sliver_list.dart';
2930

3031
class ChatViewListController {
3132
ChatViewListController({
@@ -80,14 +81,24 @@ class ChatViewListController {
8081
final StreamController<Map<String, ChatViewListItem>>
8182
_chatListStreamController =
8283
StreamController<Map<String, ChatViewListItem>>.broadcast();
84+
final animatedList =
85+
GlobalKey<AutoAnimateSliverListState<ChatViewListItem>>();
86+
87+
Map<String, ChatViewListItem>? _searchResultMap;
8388

8489
late final Stream<List<ChatViewListItem>> chatListStream;
8590

8691
/// Adds a chat to the chat list.
8792
void addChat(ChatViewListItem chat) {
93+
if (_searchResultMap != null) {
94+
chatListMap[chat.id] = chat;
95+
return;
96+
}
97+
8898
chatListMap[chat.id] = chat;
8999
if (_chatListStreamController.isClosed) return;
90100
_chatListStreamController.add(chatListMap);
101+
animatedList.currentState?.addChatItem(chat);
91102
}
92103

93104
/// Function for loading data while pagination.
@@ -109,6 +120,23 @@ class ChatViewListController {
109120
/// If the chat with [chatId] does not exist, the method returns without
110121
/// making changes.
111122
void updateChat(String chatId, UpdateChatCallback newChat) {
123+
if (_searchResultMap != null) {
124+
final searchChat = _searchResultMap?[chatId];
125+
if (searchChat == null) {
126+
final chat = chatListMap[chatId];
127+
if (chat == null) return;
128+
chatListMap[chatId] = newChat(chat);
129+
return;
130+
}
131+
132+
final updatedChat = newChat(searchChat);
133+
_searchResultMap?[chatId] = updatedChat;
134+
chatListMap[chatId] = updatedChat;
135+
if (_chatListStreamController.isClosed) return;
136+
_chatListStreamController.add(_searchResultMap ?? chatListMap);
137+
return;
138+
}
139+
112140
final chat = chatListMap[chatId];
113141
if (chat == null) return;
114142

@@ -124,23 +152,31 @@ class ChatViewListController {
124152
void removeChat(String chatId) {
125153
if (!chatListMap.containsKey(chatId)) return;
126154
chatListMap.remove(chatId);
155+
156+
if (_searchResultMap?.containsKey(chatId) ?? false) {
157+
_searchResultMap?.remove(chatId);
158+
}
159+
127160
if (_chatListStreamController.isClosed) return;
161+
animatedList.currentState?.removeItemByKey(chatId);
128162
_chatListStreamController.add(chatListMap);
129163
}
130164

131165
/// Adds the given chat search results to the stream after the current frame.
132166
void setSearchChats(List<ChatViewListItem> searchResults) {
133167
final searchResultLength = searchResults.length;
134-
final searchResultMap = {
168+
_searchResultMap = {
135169
for (var i = 0; i < searchResultLength; i++)
136170
if (searchResults[i] case final chat) chat.id: chat,
137171
};
138172
if (_chatListStreamController.isClosed) return;
139-
_chatListStreamController.add(searchResultMap);
173+
_chatListStreamController.add(_searchResultMap ?? chatListMap);
140174
}
141175

142176
/// Function to clear the search results and show the original chat list.
143177
void clearSearch() {
178+
_searchResultMap?.clear();
179+
_searchResultMap = null;
144180
if (_chatListStreamController.isClosed) return;
145181
_chatListStreamController.add(chatListMap);
146182
}

lib/src/models/config_models/chat_view_list/chat_view_list_config.dart

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,19 +33,13 @@ class ChatViewListConfig {
3333
this.enablePagination = false,
3434
this.loadMoreConfig = const LoadMoreConfig(),
3535
this.tileConfig = const ListTileConfig(),
36-
this.separator = const Divider(height: 12),
3736
this.extraSpaceAtLast = 32,
3837
this.searchConfig,
3938
});
4039

4140
/// Configuration for the search text field in the chat list.
4241
final SearchConfig? searchConfig;
4342

44-
/// Widget to be used as a separator between chat items.
45-
///
46-
/// Defaults to a `Divider` with a height of `12`.
47-
final Widget separator;
48-
4943
/// Configuration for the chat tile widget in the chat list.
5044
final ListTileConfig tileConfig;
5145

lib/src/values/typedefs.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,11 @@ typedef MenuBuilderCallback = Widget Function(
148148
Widget child,
149149
);
150150
typedef MenuActionBuilder = List<Widget> Function(ChatViewListItem chat);
151+
typedef AutoAnimateItemBuilder<T> = Widget Function(
152+
BuildContext context,
153+
int index,
154+
bool isLastItem,
155+
T item,
156+
);
157+
typedef AutoAnimateItemExtractor<T> = String Function(T item);
158+
typedef ChatPinnedCallback<T> = bool Function(T chat);

lib/src/widgets/chat_groupedlist_widget.dart

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ class _ChatGroupedListWidgetState extends State<ChatGroupedListWidget>
9292
ChatBackgroundConfiguration get chatBackgroundConfig =>
9393
chatListConfig.chatBackgroundConfig;
9494

95+
final Map<String, GlobalKey> _messageKeys = {};
96+
9597
@override
9698
void initState() {
9799
super.initState();
@@ -189,17 +191,17 @@ class _ChatGroupedListWidgetState extends State<ChatGroupedListWidget>
189191
);
190192
}
191193

192-
Future<void> _onReplyTap(String id, List<Message>? messages) async {
194+
Future<void> _onReplyTap(String id) async {
193195
// Finds the replied message if exists
194-
final repliedMessages = messages?.firstWhere((message) => id == message.id);
196+
final replyMsgCurrentState = _messageKeys[id]?.currentState;
195197
final repliedMsgAutoScrollConfig =
196198
chatListConfig.repliedMessageConfig?.repliedMsgAutoScrollConfig;
197199
final highlightDuration = repliedMsgAutoScrollConfig?.highlightDuration ??
198200
const Duration(milliseconds: 300);
199201
// Scrolls to replied message and highlights
200-
if (repliedMessages != null && repliedMessages.key.currentState != null) {
202+
if (replyMsgCurrentState != null) {
201203
await Scrollable.ensureVisible(
202-
repliedMessages.key.currentState!.context,
204+
replyMsgCurrentState.context,
203205
// This value will make widget to be in center when auto scrolled.
204206
alignment: 0.5,
205207
curve:
@@ -266,6 +268,8 @@ class _ChatGroupedListWidgetState extends State<ChatGroupedListWidget>
266268
messages,
267269
lastMatchedDate,
268270
);
271+
} else {
272+
_initMessageKeys(messages);
269273
}
270274

271275
/// [count] that indicates how many separators
@@ -299,13 +303,14 @@ class _ChatGroupedListWidgetState extends State<ChatGroupedListWidget>
299303
valueListenable: _replyId,
300304
builder: (context, state, child) {
301305
final message = messages[newIndex];
306+
final messageKey = _messageKeys[message.id] ??= GlobalKey();
302307
final enableScrollToRepliedMsg = chatListConfig
303308
.repliedMessageConfig
304309
?.repliedMsgAutoScrollConfig
305310
.enableScrollToRepliedMsg ??
306311
false;
307312
return ChatBubbleWidget(
308-
key: message.key,
313+
key: messageKey,
309314
message: message,
310315
slideAnimation: _slideAnimation,
311316
onLongPress: (yCoordinate, xCoordinate) =>
@@ -316,9 +321,7 @@ class _ChatGroupedListWidgetState extends State<ChatGroupedListWidget>
316321
),
317322
onSwipe: widget.assignReplyMessage,
318323
shouldHighlight: state == message.id,
319-
onReplyTap: enableScrollToRepliedMsg
320-
? (replyId) => _onReplyTap(replyId, snapshot.data)
321-
: null,
324+
onReplyTap: enableScrollToRepliedMsg ? _onReplyTap : null,
322325
);
323326
},
324327
);
@@ -379,13 +382,15 @@ class _ChatGroupedListWidgetState extends State<ChatGroupedListWidget>
379382

380383
/// Holds index and separator mapping to display in chat
381384
for (var i = 0; i < messages.length; i++) {
385+
final message = messages[i];
386+
_messageKeys.putIfAbsent(message.id, () => GlobalKey());
382387
if (messageSeparator.isEmpty) {
383388
/// Separator for initial message
384-
messageSeparator[0] = messages[0].createdAt;
389+
messageSeparator[0] = message.createdAt;
385390
continue;
386391
}
387392
lastMatchedDate = _groupBy(
388-
messages[i],
393+
message,
389394
lastMatchedDate,
390395
);
391396
var previousDate = _groupBy(
@@ -404,6 +409,14 @@ class _ChatGroupedListWidgetState extends State<ChatGroupedListWidget>
404409

405410
return (messageSeparator, lastMatchedDate);
406411
}
412+
413+
void _initMessageKeys(List<Message> messages) {
414+
final messagesLength = messages.length;
415+
for (var i = 0; i < messagesLength; i++) {
416+
final message = messages[i];
417+
_messageKeys.putIfAbsent(message.id, () => GlobalKey());
418+
}
419+
}
407420
}
408421

409422
class _GroupSeparatorBuilder extends StatelessWidget {

0 commit comments

Comments
 (0)