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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,6 @@ doc/api/
.idea/
.flutter-plugins
/.flutter-plugins-dependencies
chat_ui.iml
chat_ui.iml

devtools_options.yaml
1 change: 1 addition & 0 deletions example/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
.pub-cache/
.pub/
/build/
devtools_options.yaml

# Web related

Expand Down
5 changes: 4 additions & 1 deletion example/lib/chat_view_list_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ class _ChatViewListScreenState extends State<ChatViewListScreen> {
),
config: ChatViewListConfig(
tileConfig: ListTileConfig(
typingStatusConfig: const TypingStatusConfig(
showUserNames: true,
),
unreadCountConfig: const UnreadCountConfig(
style: UnreadCountStyle.ninetyNinePlus,
),
Expand Down Expand Up @@ -81,7 +84,7 @@ class _ChatViewListScreenState extends State<ChatViewListScreen> {
if (value.isEmpty) {
return null;
}
final list = _chatListController?.initialChatList
final list = _chatListController?.chatListMap.values
.where((chat) => chat.name
.toLowerCase()
.contains(value.toLowerCase()))
Expand Down
77 changes: 57 additions & 20 deletions lib/src/controller/chat_list_view_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,63 +24,100 @@ import 'dart:async';
import 'package:flutter/material.dart';

import '../models/chat_view_list_item.dart';
import '../values/typedefs.dart';

class ChatViewListController {
ChatViewListController({
required this.initialChatList,
required List<ChatViewListItem> initialChatList,
required this.scrollController,
this.disposeOtherResources = true,
}) {
// Add the initial chat list to the stream controller after the first frame.
final chatListLength = initialChatList.length;

final chatsMap = {
for (var i = 0; i < chatListLength; i++)
if (initialChatList[i] case final chat) chat.id: chat,
};

chatListMap = chatsMap;

// Adds the current chat map to the stream controller
// after the first frame render.
Future.delayed(
Duration.zero,
() => _chatListStreamController.add(initialChatList),
() => _chatListStreamController.add(chatListMap),
);
}

/// Represents initial chat list.
List<ChatViewListItem> initialChatList = [];
/// Stores and manages chat items by their unique IDs.
/// A map is used for efficient lookup, update, and removal of chats
/// by their unique id.
Map<String, ChatViewListItem> chatListMap = {};

/// Provides scroll controller for chat list.
ScrollController scrollController;

final bool disposeOtherResources;

/// Represents chat list user stream
final StreamController<List<ChatViewListItem>> _chatListStreamController =
StreamController.broadcast();
/// Stream controller to manage the chat list stream.
final StreamController<Map<String, ChatViewListItem>>
_chatListStreamController =
StreamController<Map<String, ChatViewListItem>>.broadcast();

late final Stream<List<ChatViewListItem>> chatListStream =
_chatListStreamController.stream;
_chatListStreamController.stream.map(
(chatMap) => chatMap.values.toList(),
);

/// Adds a chat to the chat list.
void addChat(ChatViewListItem chat) {
initialChatList.add(chat);
chatListMap[chat.id] = chat;
if (_chatListStreamController.isClosed) return;
_chatListStreamController.sink.add(initialChatList);
_chatListStreamController.add(chatListMap);
}

/// Function for loading data while pagination.
void loadMoreChats(List<ChatViewListItem> chatList) {
initialChatList.addAll(chatList);
final chatListLength = chatList.length;
chatListMap.addAll(
{
for (var i = 0; i < chatListLength; i++)
if (chatList[i] case final chat) chat.id: chat,
},
);
if (_chatListStreamController.isClosed) return;
_chatListStreamController.sink.add(initialChatList);
_chatListStreamController.add(chatListMap);
}

/// Updates the chat entry in [chatListMap] for the given [chatId] using
/// the provided [newChat] callback.
///
/// If the chat with [chatId] does not exist, the method returns without
/// making changes.
void updateChat(String chatId, UpdateChatCallback newChat) {
final chat = chatListMap[chatId];
if (chat == null) return;

chatListMap[chatId] = newChat(chat);
if (_chatListStreamController.isClosed) return;
_chatListStreamController.add(chatListMap);
}

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

/// Function to clear the search results and show the original chat list.
void clearSearch() {
if (_chatListStreamController.isClosed) return;
_chatListStreamController.sink.add(initialChatList);
_chatListStreamController.add(chatListMap);
}

/// Used to dispose ValueNotifiers and Streams.
Expand Down
32 changes: 32 additions & 0 deletions lib/src/extensions/extensions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import 'package:intl/intl.dart';

import '../inherited_widgets/configurations_inherited_widgets.dart';
import '../models/config_models/chat_bubble_configuration.dart';
import '../models/config_models/chat_view_list/list_type_indicator_config.dart';
import '../models/config_models/message_list_configuration.dart';
import '../models/config_models/reply_suggestions_config.dart';
import '../utils/constants/constants.dart';
Expand Down Expand Up @@ -151,6 +152,37 @@ extension ChatViewStateTitleExtension on String? {
}
}

extension type const TypingStatusConfigExtension(TypingStatusConfig config) {
String toTypingStatus(List<String> users) {
final prefix = config.prefix ?? '';
final suffix = config.suffix ?? '';
final showUserNames = config.showUserNames;
final locale = PackageStrings.currentLocale;
final text = '$prefix${locale.typing}$suffix';
if (users.isEmpty) return text;

final count = users.length;

final firstName = users[0];

if (count == 1) {
return '$firstName ${locale.isVerb} $text';
} else if (count == 2) {
final newText = showUserNames
? '$firstName & ${users[1]} ${locale.areVerb}'
: '$firstName & 1 ${locale.other} ${locale.isVerb}';
return '$newText $text';
} else if (showUserNames && count == 3) {
return '${users[0]}, ${users[1]} & ${users[2]} ${locale.areVerb} $text';
} else {
final newText = showUserNames
? '$firstName, ${users[1]} & ${count - 2}'
: '$firstName & ${count - 1}';
return '$newText ${locale.others} ${locale.areVerb} $text';
}
}
}

/// Extension on State for accessing inherited widget.
extension StatefulWidgetExtension on State {
ChatViewInheritedWidget? get chatViewIW =>
Expand Down
33 changes: 33 additions & 0 deletions lib/src/models/chat_view_list_item.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
import 'package:chatview_utils/chatview_utils.dart';

import '../values/enumeration.dart';
import '../values/typedefs.dart';
import 'omit.dart';

/// Model class representing a user or group in the chat list.
class ChatViewListItem {
Expand All @@ -30,6 +32,7 @@ class ChatViewListItem {
required this.id,
required this.name,
this.chatType = ChatType.user,
this.typingUsers = const <String>{},
this.userActiveStatus = UserActiveStatus.offline,
this.lastMessage,
this.imageUrl,
Expand Down Expand Up @@ -60,4 +63,34 @@ class ChatViewListItem {
///
/// Defaults to [UserActiveStatus.offline].
final UserActiveStatus userActiveStatus;

// TODO(YASH): Switch to User Object instead of string.
/// Set of users currently typing in the chat.
final Set<String> typingUsers;

ChatViewListItem copyWith({
Defaulted<String> id = const Omit(),
Defaulted<String> name = const Omit(),
Defaulted<ChatType> chatType = const Omit(),
Defaulted<Set<String>> typingUsers = const Omit(),
Defaulted<UserActiveStatus> userActiveStatus = const Omit(),
Defaulted<Message>? lastMessage = const Omit(),
Defaulted<String>? imageUrl = const Omit(),
Defaulted<int>? unreadCount = const Omit(),
}) {
return ChatViewListItem(
id: id is Omit ? this.id : id as String,
name: name is Omit ? this.name : name as String,
chatType: chatType is Omit ? this.chatType : chatType as ChatType,
typingUsers:
typingUsers is Omit ? this.typingUsers : typingUsers as Set<String>,
userActiveStatus: userActiveStatus is Omit
? this.userActiveStatus
: userActiveStatus as UserActiveStatus,
lastMessage:
lastMessage is Omit ? this.lastMessage : lastMessage as Message?,
imageUrl: imageUrl is Omit ? this.imageUrl : imageUrl as String?,
unreadCount: unreadCount is Omit ? this.unreadCount : unreadCount as int?,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import 'search_config.dart';
class ChatViewListConfig {
/// Creates a configuration object for the chat list UI.
const ChatViewListConfig({
this.backgroundColor = Colors.white,
this.enablePagination = false,
this.loadMoreConfig = const LoadMoreConfig(),
this.tileConfig = const ListTileConfig(),
Expand Down Expand Up @@ -60,4 +61,7 @@ class ChatViewListConfig {
///
/// Defaults to `false`.
final bool enablePagination;

/// Background color for the chat list.
final Color backgroundColor;
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import 'package:flutter/material.dart';
import '../../../values/typedefs.dart';
import '../../chat_view_list_item.dart';
import 'last_message_time_config.dart';
import 'list_type_indicator_config.dart';
import 'unread_count_config.dart';
import 'user_active_status_config.dart';
import 'user_avatar_config.dart';
Expand All @@ -37,6 +38,7 @@ class ListTileConfig {
this.userAvatarConfig = const UserAvatarConfig(),
this.unreadCountConfig = const UnreadCountConfig(),
this.userActiveStatusConfig = const UserActiveStatusConfig(),
this.typingStatusConfig = const TypingStatusConfig(),
this.userNameMaxLines = 1,
this.userNameTextOverflow = TextOverflow.ellipsis,
this.lastMessageMaxLines = 1,
Expand All @@ -47,7 +49,7 @@ class ListTileConfig {
this.lastMessageTextStyle,
this.onTap,
this.onLongPress,
this.customLastMessageListViewBuilder,
this.lastMessageTileBuilder,
});

/// Padding around the widget in the chat list.
Expand Down Expand Up @@ -113,5 +115,8 @@ class ListTileConfig {
final bool showOnlineStatus;

/// Custom builder for the last message view in the chat list.
final CustomLastMessageListViewBuilder? customLastMessageListViewBuilder;
final ChatViewListLastMessageTileBuilder? lastMessageTileBuilder;

/// Configuration for the typing status indicator in the chat list.
final TypingStatusConfig typingStatusConfig;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import 'package:flutter/material.dart';

import '../../../values/typedefs.dart';

/// Configuration for the chat list type indicator.
/// Controls appearance and behavior of the indicator shown in chat list tiles.
class TypingStatusConfig {
/// Creates a [TypingStatusConfig].
///
/// Configuration options for the chat view list type indicator:
///
/// - [suffix]: Appends a string to the indicator text.
/// Default is `null`.
/// - [maxLines]: Limits the number of lines for the indicator text.
/// Default is `1`.
/// - [showUserNames]: Toggles the display of user names in the indicator.
/// Default is `false`.
/// - [textStyle]: Customizes the style of the indicator text.
/// - [overflow]: Sets the text overflow behavior.
/// Default is `TextOverflow.ellipsis`.
/// - [prefix]: Prepends a string to the indicator text.
/// Default is `null`.
/// - [textBuilder]: Allows custom building of the indicator text.
/// Default is `null`.
/// - [widgetBuilder]: Allows custom building of the indicator widget.
/// Default is `null`.
const TypingStatusConfig({
this.suffix = '...',
this.maxLines = 1,
this.showUserNames = false,
this.textStyle = const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
fontStyle: FontStyle.italic,
),
this.overflow = TextOverflow.ellipsis,
this.prefix,
this.textBuilder,
this.widgetBuilder,
});

/// Optional text appended after the typing indicator.
///
/// The main indicator text comes from locale `typing`.
///
/// Example:
/// - If `suffix = '...'`,
/// ➜ Display: `'typing...'`
/// - If `suffix = null`,
/// ➜ Display: `'typing'`
final String? suffix;

/// Whether to show user names in the indicator.
///
/// If `true`, the indicator will include user names.
/// Example:
/// - If `showUserNames = true` and there are two users typing,
/// ➜ Display: `'ChatView & Simform are typing...'`
/// - If `showUserNames = false`,
/// ➜ Display: `'ChatView & 1 other is typing...'`
final bool showUserNames;

/// Optional text prepended before the typing indicator.
///
/// The main indicator text comes from locale `typing`.
///
/// Example:
/// - If `prefix = 'User'`,
/// ➜ Display: `'User typing...'`
/// - If `prefix = null`,
/// ➜ Display: `'typing...'`
final String? prefix;

/// Style for the indicator text.
final TextStyle? textStyle;

/// Maximum number of lines for the indicator text.
final int? maxLines;

/// Overflow behavior for the indicator text.
final TextOverflow? overflow;

/// Custom builder for indicator text.
final ChatViewListTextBuilder? textBuilder;

/// Custom builder for indicator widget.
final ChatViewListWidgetBuilder? widgetBuilder;
}
Loading