Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions example/lib/chat_operations.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import 'dart:math';

import 'package:chatview/chatview.dart';
import 'package:flutter/material.dart';

class ChatOperations extends StatelessWidget {
const ChatOperations({required this.controller, super.key});

final ChatViewListController controller;

@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
FloatingActionButton.small(
child: const Icon(Icons.add),
onPressed: () {
controller.addChat(
ChatViewListItem(
id: DateTime.now().millisecondsSinceEpoch.toString(),
name: 'New Chat ${DateTime.now().millisecondsSinceEpoch}',
lastMessage: Message(
message: 'Hello, this is a new chat!',
createdAt: DateTime.now(),
sentBy: 'User',
id: 'msg-${DateTime.now().millisecondsSinceEpoch}',
),
),
);
},
),
const SizedBox(width: 6),
FloatingActionButton.small(
child: const Icon(Icons.remove),
onPressed: () {
final key = controller.chatListMap.keys.firstOrNull;
if (key == null) return;
controller.removeChat(key);
},
),
const SizedBox(width: 6),
FloatingActionButton.small(
child: const Icon(Icons.edit),
onPressed: () {
final randomChatIndex =
Random().nextInt(controller.chatListMap.length);
final chatId =
controller.chatListMap.keys.elementAt(randomChatIndex);

controller.updateChat(
chatId,
(previousChat) {
final newPinStatus = switch (previousChat.settings.pinStatus) {
PinStatus.pinned => PinStatus.unpinned,
PinStatus.unpinned => PinStatus.pinned,
};
return previousChat.copyWith(
settings: previousChat.settings.copyWith(
pinStatus: newPinStatus,
pinTime: newPinStatus.isPinned ? DateTime.now() : null,
),
);
},
);
},
),
const SizedBox(width: 6),
FloatingActionButton.small(
child: const Icon(Icons.person),
onPressed: () {
final randomChatIndex =
Random().nextInt(controller.chatListMap.length);
final chatId =
controller.chatListMap.keys.elementAt(randomChatIndex);
controller.updateChat(
chatId,
(previousChat) => previousChat.copyWith(
name: 'Updated Chat ${Random().nextInt(100)}',
),
);
},
),
const SizedBox(width: 6),
FloatingActionButton.small(
child: const Icon(Icons.more_horiz),
onPressed: () {
final chatId = controller.chatListMap.keys.elementAt(0);
controller.updateChat(
chatId,
(previousChat) => previousChat.copyWith(
typingUsers: previousChat.typingUsers.isEmpty
? {const ChatUser(id: '1', name: 'John Doe')}
: {},
),
);
},
),
const SizedBox(width: 6),
FloatingActionButton.small(
child: const Icon(Icons.message),
onPressed: () {
final randomChatIndex =
Random().nextInt(controller.chatListMap.length);
final chatId =
controller.chatListMap.keys.elementAt(randomChatIndex);
final randomId = Random().nextInt(100);
controller.updateChat(
chatId,
(previousChat) => previousChat.copyWith(
lastMessage: Message(
message: 'Random message $randomId',
createdAt: DateTime.now(),
sentBy: previousChat.lastMessage?.sentBy ?? '',
id: '$randomId',
),
),
);
},
),
],
);
}
}
40 changes: 38 additions & 2 deletions lib/src/controller/chat_list_view_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import 'package:flutter/material.dart';
import '../models/chat_view_list_item.dart';
import '../values/enumeration.dart';
import '../values/typedefs.dart';
import '../widgets/chat_view_list/auto_animate_sliver_list.dart';

class ChatViewListController {
ChatViewListController({
Expand Down Expand Up @@ -80,14 +81,24 @@ class ChatViewListController {
final StreamController<Map<String, ChatViewListItem>>
_chatListStreamController =
StreamController<Map<String, ChatViewListItem>>.broadcast();
final animatedList =
GlobalKey<AutoAnimateSliverListState<ChatViewListItem>>();

Map<String, ChatViewListItem>? _searchResultMap;

late final Stream<List<ChatViewListItem>> chatListStream;

/// Adds a chat to the chat list.
void addChat(ChatViewListItem chat) {
if (_searchResultMap != null) {
chatListMap[chat.id] = chat;
return;
}

chatListMap[chat.id] = chat;
if (_chatListStreamController.isClosed) return;
_chatListStreamController.add(chatListMap);
animatedList.currentState?.addChatItem(chat);
}

/// Function for loading data while pagination.
Expand All @@ -109,6 +120,23 @@ class ChatViewListController {
/// If the chat with [chatId] does not exist, the method returns without
/// making changes.
void updateChat(String chatId, UpdateChatCallback newChat) {
if (_searchResultMap != null) {
final searchChat = _searchResultMap?[chatId];
if (searchChat == null) {
final chat = chatListMap[chatId];
if (chat == null) return;
chatListMap[chatId] = newChat(chat);
return;
}

final updatedChat = newChat(searchChat);
_searchResultMap?[chatId] = updatedChat;
chatListMap[chatId] = updatedChat;
if (_chatListStreamController.isClosed) return;
_chatListStreamController.add(_searchResultMap ?? chatListMap);
return;
}

final chat = chatListMap[chatId];
if (chat == null) return;

Expand All @@ -124,23 +152,31 @@ class ChatViewListController {
void removeChat(String chatId) {
if (!chatListMap.containsKey(chatId)) return;
chatListMap.remove(chatId);

if (_searchResultMap?.containsKey(chatId) ?? false) {
_searchResultMap?.remove(chatId);
}

if (_chatListStreamController.isClosed) return;
animatedList.currentState?.removeItemByKey(chatId);
_chatListStreamController.add(chatListMap);
}

/// Adds the given chat search results to the stream after the current frame.
void setSearchChats(List<ChatViewListItem> searchResults) {
final searchResultLength = searchResults.length;
final searchResultMap = {
_searchResultMap = {
for (var i = 0; i < searchResultLength; i++)
if (searchResults[i] case final chat) chat.id: chat,
};
if (_chatListStreamController.isClosed) return;
_chatListStreamController.add(searchResultMap);
_chatListStreamController.add(_searchResultMap ?? chatListMap);
}

/// Function to clear the search results and show the original chat list.
void clearSearch() {
_searchResultMap?.clear();
_searchResultMap = null;
if (_chatListStreamController.isClosed) return;
_chatListStreamController.add(chatListMap);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,13 @@ class ChatViewListConfig {
this.enablePagination = false,
this.loadMoreConfig = const LoadMoreConfig(),
this.tileConfig = const ListTileConfig(),
this.separator = const Divider(height: 12),
this.extraSpaceAtLast = 32,
this.searchConfig,
});

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

/// Widget to be used as a separator between chat items.
///
/// Defaults to a `Divider` with a height of `12`.
final Widget separator;

/// Configuration for the chat tile widget in the chat list.
final ListTileConfig tileConfig;

Expand Down
8 changes: 8 additions & 0 deletions lib/src/values/typedefs.dart
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,11 @@ typedef MenuBuilderCallback = Widget Function(
Widget child,
);
typedef MenuActionBuilder = List<Widget> Function(ChatViewListItem chat);
typedef AutoAnimateItemBuilder<T> = Widget Function(
BuildContext context,
int index,
bool isLastItem,
T item,
);
typedef AutoAnimateItemExtractor<T> = String Function(T item);
typedef ChatPinnedCallback<T> = bool Function(T chat);
33 changes: 23 additions & 10 deletions lib/src/widgets/chat_groupedlist_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ class _ChatGroupedListWidgetState extends State<ChatGroupedListWidget>
ChatBackgroundConfiguration get chatBackgroundConfig =>
chatListConfig.chatBackgroundConfig;

final Map<String, GlobalKey> _messageKeys = {};

@override
void initState() {
super.initState();
Expand Down Expand Up @@ -189,17 +191,17 @@ class _ChatGroupedListWidgetState extends State<ChatGroupedListWidget>
);
}

Future<void> _onReplyTap(String id, List<Message>? messages) async {
Future<void> _onReplyTap(String id) async {
// Finds the replied message if exists
final repliedMessages = messages?.firstWhere((message) => id == message.id);
final replyMsgCurrentState = _messageKeys[id]?.currentState;
final repliedMsgAutoScrollConfig =
chatListConfig.repliedMessageConfig?.repliedMsgAutoScrollConfig;
final highlightDuration = repliedMsgAutoScrollConfig?.highlightDuration ??
const Duration(milliseconds: 300);
// Scrolls to replied message and highlights
if (repliedMessages != null && repliedMessages.key.currentState != null) {
if (replyMsgCurrentState != null) {
await Scrollable.ensureVisible(
repliedMessages.key.currentState!.context,
replyMsgCurrentState.context,
// This value will make widget to be in center when auto scrolled.
alignment: 0.5,
curve:
Expand Down Expand Up @@ -266,6 +268,8 @@ class _ChatGroupedListWidgetState extends State<ChatGroupedListWidget>
messages,
lastMatchedDate,
);
} else {
_initMessageKeys(messages);
}

/// [count] that indicates how many separators
Expand Down Expand Up @@ -299,13 +303,14 @@ class _ChatGroupedListWidgetState extends State<ChatGroupedListWidget>
valueListenable: _replyId,
builder: (context, state, child) {
final message = messages[newIndex];
final messageKey = _messageKeys[message.id] ??= GlobalKey();
final enableScrollToRepliedMsg = chatListConfig
.repliedMessageConfig
?.repliedMsgAutoScrollConfig
.enableScrollToRepliedMsg ??
false;
return ChatBubbleWidget(
key: message.key,
key: messageKey,
message: message,
slideAnimation: _slideAnimation,
onLongPress: (yCoordinate, xCoordinate) =>
Expand All @@ -316,9 +321,7 @@ class _ChatGroupedListWidgetState extends State<ChatGroupedListWidget>
),
onSwipe: widget.assignReplyMessage,
shouldHighlight: state == message.id,
onReplyTap: enableScrollToRepliedMsg
? (replyId) => _onReplyTap(replyId, snapshot.data)
: null,
onReplyTap: enableScrollToRepliedMsg ? _onReplyTap : null,
);
},
);
Expand Down Expand Up @@ -379,13 +382,15 @@ class _ChatGroupedListWidgetState extends State<ChatGroupedListWidget>

/// Holds index and separator mapping to display in chat
for (var i = 0; i < messages.length; i++) {
final message = messages[i];
_messageKeys.putIfAbsent(message.id, () => GlobalKey());
if (messageSeparator.isEmpty) {
/// Separator for initial message
messageSeparator[0] = messages[0].createdAt;
messageSeparator[0] = message.createdAt;
continue;
}
lastMatchedDate = _groupBy(
messages[i],
message,
lastMatchedDate,
);
var previousDate = _groupBy(
Expand All @@ -404,6 +409,14 @@ class _ChatGroupedListWidgetState extends State<ChatGroupedListWidget>

return (messageSeparator, lastMatchedDate);
}

void _initMessageKeys(List<Message> messages) {
final messagesLength = messages.length;
for (var i = 0; i < messagesLength; i++) {
final message = messages[i];
_messageKeys.putIfAbsent(message.id, () => GlobalKey());
}
}
}

class _GroupSeparatorBuilder extends StatelessWidget {
Expand Down
Loading