diff --git a/README.md b/README.md index 9f4a3c40..e530817c 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,9 @@ ChatView is a Flutter package that allows you to integrate a highly customizable chat UI in your Flutter applications with [Flexible Backend Integration][chatViewConnect]. -[//]: # (TODO-YASH: USE MAIN BRANCH TO DISPLAY PREVIEW GIF) -![Preview](https://raw.githubusercontent.com/SimformSolutionsPvtLtd/chatview/feat/use_data_models_from_chatview_utils/preview/chatview.gif) +| ChatViewList | ChatView | +|--------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------| +| ![ChatViewList_Preview](https://raw.githubusercontent.com/SimformSolutionsPvtLtd/chatview/main/preview/chatviewlist.gif) | ![ChatView Preview](https://raw.githubusercontent.com/SimformSolutionsPvtLtd/chatview/main/preview/chatview.gif) | ## Features @@ -23,6 +24,7 @@ Flutter applications with [Flexible Backend Integration][chatViewConnect]. - User online status indicators - Typing indicators for active users - Unread message count badges +- Header and Footer support for additional widgets - Highly customizable UI components - Plug-and-play backend support using [chatview_connect][chatViewConnect] diff --git a/doc/documentation.md b/doc/documentation.md index 166ec91d..a4659bc9 100644 --- a/doc/documentation.md +++ b/doc/documentation.md @@ -5,7 +5,9 @@ Flutter applications with [Flexible Backend Integration](https://pub.dev/package ## Preview -![Preview](https://raw.githubusercontent.com/SimformSolutionsPvtLtd/chatview/main/preview/chatview.gif) +| ChatViewList | ChatView | +|--------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------| +| ![ChatViewList_Preview](https://raw.githubusercontent.com/SimformSolutionsPvtLtd/chatview/main/preview/chatviewlist.gif) | ![ChatView Preview](https://raw.githubusercontent.com/SimformSolutionsPvtLtd/chatview/main/preview/chatview.gif) | ## Features @@ -42,12 +44,12 @@ Flutter applications with [Flexible Backend Integration](https://pub.dev/package For a live web demo, visit [Chat View Example](https://simformsolutionspvtltd.github.io/chatview/). -## Compatibility with [`chatview_connect`](https://pub.dev/packages/chatview_connect) +## Compatibility with [chatview_connect](https://pub.dev/packages/chatview_connect) -| `chatview` version | [`chatview_connect`](https://pub.dev/packages/chatview_connect) version | -|--------------------|-------------------------------------------------------------------------| -| `>=2.4.1 <3.0.0` | `0.0.1` | -| `>= 3.0.0` | `0.0.2` | +| `chatview` version | [chatview_connect](https://pub.dev/packages/chatview_connect) version | +|--------------------|-----------------------------------------------------------------------| +| `>=2.4.1 <3.0.0` | `0.0.1` | +| `>= 3.0.0` | `0.0.2` | ## Compatible Message Types diff --git a/example/assets/vectors/ai_logo.svg b/example/assets/vectors/ai_logo.svg new file mode 100644 index 00000000..d92d5fef --- /dev/null +++ b/example/assets/vectors/ai_logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/example/assets/vectors/camera_2.svg b/example/assets/vectors/camera_2.svg new file mode 100644 index 00000000..963829a9 --- /dev/null +++ b/example/assets/vectors/camera_2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/example/assets/vectors/chat_discovery_ai.svg b/example/assets/vectors/chat_discovery_ai.svg new file mode 100644 index 00000000..37554f44 --- /dev/null +++ b/example/assets/vectors/chat_discovery_ai.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/example/assets/vectors/check_mark.svg b/example/assets/vectors/check_mark.svg new file mode 100644 index 00000000..0a7d0394 --- /dev/null +++ b/example/assets/vectors/check_mark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/example/assets/vectors/create_pen.svg b/example/assets/vectors/create_pen.svg new file mode 100644 index 00000000..826d852b --- /dev/null +++ b/example/assets/vectors/create_pen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/example/assets/vectors/phone.svg b/example/assets/vectors/phone.svg new file mode 100644 index 00000000..6a047c6c --- /dev/null +++ b/example/assets/vectors/phone.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/example/assets/vectors/pinned.svg b/example/assets/vectors/pinned.svg new file mode 100644 index 00000000..31306864 --- /dev/null +++ b/example/assets/vectors/pinned.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/example/assets/vectors/sticker.svg b/example/assets/vectors/sticker.svg new file mode 100644 index 00000000..3ece50e2 --- /dev/null +++ b/example/assets/vectors/sticker.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/example/assets/vectors/video.svg b/example/assets/vectors/video.svg new file mode 100644 index 00000000..e8f3ef66 --- /dev/null +++ b/example/assets/vectors/video.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/example/lib/chat_operations.dart b/example/lib/chat_operations.dart deleted file mode 100644 index f36dc588..00000000 --- a/example/lib/chat_operations.dart +++ /dev/null @@ -1,124 +0,0 @@ -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', - ), - ), - ); - }, - ), - ], - ); - } -} diff --git a/example/lib/chat_view_list_screen.dart b/example/lib/chat_view_list_screen.dart deleted file mode 100644 index c16f1055..00000000 --- a/example/lib/chat_view_list_screen.dart +++ /dev/null @@ -1,174 +0,0 @@ -import 'package:chatview/chatview.dart'; -import 'package:flutter/material.dart'; - -import 'data.dart'; -import 'main.dart'; - -class ChatViewListScreen extends StatefulWidget { - const ChatViewListScreen({super.key}); - - @override - State createState() => _ChatViewListScreenState(); -} - -class _ChatViewListScreenState extends State { - final _searchController = TextEditingController(); - - ChatViewListController? _chatListController; - - ScrollController? _scrollController; - - // Assign the controller in didChangeDependencies - // to ensure PrimaryScrollController is available. - @override - void didChangeDependencies() { - super.didChangeDependencies(); - _scrollController = PrimaryScrollController.of(context); - _chatListController ??= ChatViewListController( - initialChatList: Data.chatList, - scrollController: _scrollController!, - disposeOtherResources: false, - ); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - body: _chatListController == null - ? const Center(child: CircularProgressIndicator()) - : ChatViewList( - controller: _chatListController!, - appbar: ChatViewListAppBar( - titleText: 'ChatView', - centerTitle: false, - scrolledUnderElevation: 0, - actions: [ - IconButton( - icon: const Icon(Icons.search), - onPressed: () { - // Handle search action - }, - ), - ], - ), - header: SizedBox( - height: 60, - child: ListView( - padding: const EdgeInsetsGeometry.all(12), - scrollDirection: Axis.horizontal, - children: [ - FilterChip.elevated( - backgroundColor: Colors.grey.shade200, - label: Text( - 'All Chats (${_chatListController?.chatListMap.length ?? 0})'), - onSelected: (bool value) => - _chatListController?.clearSearch(), - ), - const SizedBox(width: 12), - FilterChip.elevated( - backgroundColor: Colors.grey.shade200, - label: const Text('Pinned Chats'), - onSelected: (bool value) { - _chatListController?.setSearchChats( - _chatListController?.chatListMap.values - .where((e) => e.settings.pinStatus.isPinned) - .toList() ?? - [], - ); - }, - ), - const SizedBox(width: 12), - FilterChip.elevated( - backgroundColor: Colors.grey.shade200, - label: const Text('Unread Chats'), - onSelected: (bool value) { - _chatListController?.setSearchChats( - _chatListController?.chatListMap.values - .where((e) => (e.unreadCount ?? 0) > 0) - .toList() ?? - [], - ); - }, - ), - ], - ), - ), - menuConfig: ChatMenuConfig( - deleteCallback: (chat) => - _chatListController?.removeChat(chat.id), - muteStatusCallback: (result) => _chatListController?.updateChat( - result.chat.id, - (previousChat) => previousChat.copyWith( - settings: previousChat.settings.copyWith( - muteStatus: result.status, - ), - ), - ), - pinStatusCallback: (result) => _chatListController?.updateChat( - result.chat.id, - (previousChat) => previousChat.copyWith( - settings: previousChat.settings.copyWith( - pinStatus: result.status, - ), - ), - ), - ), - separatorBuilder: (_, __) => Divider( - height: 0, - thickness: 2, - color: Colors.grey.shade200, - ), - tileConfig: ListTileConfig( - userActiveStatusConfig: const UserActiveStatusConfig( - alignment: UserActiveStatusAlignment.topRight, - ), - lastMessageStatusConfig: LastMessageStatusConfig( - showStatusFor: (message) => message.sentBy == '2', - ), - typingStatusConfig: const TypingStatusConfig( - showUserNames: true, - ), - unreadCountConfig: const UnreadCountConfig( - style: UnreadCountStyle.ninetyNinePlus, - ), - userAvatarConfig: UserAvatarConfig( - backgroundColor: Colors.red.shade100, - ), - onTap: (chat) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => ChatScreen( - chat: chat, - ), - ), - ); - }, - ), - searchConfig: SearchConfig( - textEditingController: _searchController, - debounceDuration: const Duration(milliseconds: 300), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(10)), - ), - onSearch: (value) async { - if (value.isEmpty) { - return null; - } - final list = _chatListController?.chatListMap.values - .where((chat) => - chat.name.toLowerCase().contains(value.toLowerCase())) - .toList(); - return list; - }, - ), - ), - ); - } - - @override - void dispose() { - _chatListController?.dispose(); - _searchController.dispose(); - super.dispose(); - } -} diff --git a/example/lib/data.dart b/example/lib/data.dart index d1e35f97..93dc04c6 100644 --- a/example/lib/data.dart +++ b/example/lib/data.dart @@ -4,202 +4,410 @@ class Data { static const profileImage = "https://github.com/SimformSolutionsPvtLtd/chatview/blob/main/example/assets/images/simform.png?raw=true"; - static final chatList = [ - ChatViewListItem( - id: '2', - name: 'Simform', - unreadCount: 2, - imageUrl: Data.profileImage, - lastMessage: Message( - id: '12', - sentBy: '2', - message: "🤩🤩", - createdAt: DateTime.now().toUtc(), - status: MessageStatus.delivered, + static List getChatList() { + final now = DateTime.now(); + return [ + ChatViewListItem( + id: '1', + name: 'Weekend', + imageUrl: + 'https://images.unsplash.com/photo-1506744038136-46273834b3fb?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2', + chatRoomType: ChatRoomType.group, + lastMessage: Message( + id: '1', + message: 'Who is up for a hike this weekend?', + createdAt: now.subtract(const Duration(days: 1)), + sentBy: '2', + status: MessageStatus.read, + ), + settings: const ChatSettings(pinStatus: PinStatus.pinned), ), - settings: ChatSettings( - pinStatus: PinStatus.pinned, - pinTime: DateTime.now().toUtc(), + ChatViewListItem( + id: '2', + name: 'Andrius Schneider', + imageUrl: + 'https://images.pexels.com/photos/1681010/pexels-photo-1681010.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2', + typingUsers: {const ChatUser(id: '3', name: 'Andrius')}, + unreadCount: 2, + lastMessage: Message( + id: '2', + message: 'typing...', + createdAt: DateTime(now.year, now.month, now.day, 9, 45), + sentBy: '3', + status: MessageStatus.delivered, + ), ), - ), - ChatViewListItem( - id: '1', - name: 'Flutter', - imageUrl: Data.profileImage, - userActiveStatus: UserActiveStatus.online, - lastMessage: Message( - id: '13', - sentBy: '1', - message: "https://example.com/image.png", - messageType: MessageType.image, - createdAt: DateTime.now().subtract(const Duration(days: 2)).toUtc(), - status: MessageStatus.delivered, + ChatViewListItem( + id: '3', + name: 'Anna Payet', + imageUrl: + 'https://images.pexels.com/photos/774909/pexels-photo-774909.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2', + lastMessage: Message( + id: '3', + message: 'Best breakfast ever', + createdAt: DateTime(now.year, now.month, now.day, 9, 37), + sentBy: '4', + messageType: MessageType.image, + status: MessageStatus.undelivered, + ), ), - ), - ChatViewListItem( - id: '3', - name: 'Flutter Dev Group', - imageUrl: Data.profileImage, - chatRoomType: ChatRoomType.group, - userActiveStatus: UserActiveStatus.online, - lastMessage: Message( - id: '13', - sentBy: '2', - message: "https://example.com/image.png", - messageType: MessageType.image, - createdAt: DateTime.now().subtract(const Duration(days: 7)).toUtc(), - status: MessageStatus.delivered, + ChatViewListItem( + id: '4', + name: 'Family', + unreadCount: 4, + chatRoomType: ChatRoomType.group, + imageUrl: + 'https://images.pexels.com/photos/1081685/pexels-photo-1081685.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2', + lastMessage: Message( + id: '4', + message: 'See you at the dinner table!', + createdAt: now.subtract(const Duration(days: 1)), + sentBy: '5', + status: MessageStatus.pending, + ), + ), + ChatViewListItem( + id: '5', + name: 'Maria', + imageUrl: + 'https://images.pexels.com/photos/1181690/pexels-photo-1181690.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2', + lastMessage: Message( + id: '5', + message: 'It was great to see you! Let\'s catch up again soon', + createdAt: now.subtract(const Duration(hours: 1)), + sentBy: 'me', + status: MessageStatus.read, + ), ), - ), - for (var i = 4; i < 10; i++) ChatViewListItem( - id: i.toString(), - name: 'Chat $i', - imageUrl: Data.profileImage, + id: '6', + name: 'Lunch club!', + chatRoomType: ChatRoomType.group, + imageUrl: + 'https://images.pexels.com/photos/415829/pexels-photo-415829.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2', lastMessage: Message( - id: i.toString(), - sentBy: i % 2 == 0 ? '1' : '2', - message: "This is message number $i", - createdAt: DateTime.now() - .subtract(Duration(days: i, hours: i % 2 == 0 ? i : i * 2)) - .toUtc(), + id: '6', + message: 'Can we reschedule our lunch to Thursday?', + createdAt: now.subtract(const Duration(days: 1)), + sentBy: '7', status: MessageStatus.delivered, ), + settings: const ChatSettings(muteStatus: MuteStatus.muted), ), - ]; + ChatViewListItem( + id: '7', + name: "Jasper's market", + imageUrl: + 'https://images.pexels.com/photos/102104/pexels-photo-102104.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2', + unreadCount: 1, + lastMessage: Message( + id: '7', + message: 'It will be ready on Thursday!', + createdAt: now.subtract(const Duration(hours: 12)), + sentBy: '8', + status: MessageStatus.read, + ), + ), + ChatViewListItem( + id: '8', + name: 'Work', + chatRoomType: ChatRoomType.group, + imageUrl: + 'https://images.pexels.com/photos/3184291/pexels-photo-3184291.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2', + lastMessage: Message( + id: '8', + message: 'Please review the latest project updates.', + createdAt: now.subtract(const Duration(days: 2)), + sentBy: '9', + status: MessageStatus.read, + ), + ), + ChatViewListItem( + id: '9', + name: 'Evelyn Harper', + imageUrl: + 'https://images.pexels.com/photos/1239291/pexels-photo-1239291.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2', + lastMessage: Message( + id: '9', + message: 'Let\'s plan our next trip!', + createdAt: now.subtract(const Duration(days: 3)), + sentBy: '10', + status: MessageStatus.read, + ), + ), + ChatViewListItem( + id: '10', + name: 'Book club', + chatRoomType: ChatRoomType.group, + imageUrl: + 'https://images.pexels.com/photos/159711/book-books-bookstore-book-reading-159711.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2', + lastMessage: Message( + id: '10', + message: 'Next meeting is on Friday at 6 PM.', + createdAt: now.subtract(const Duration(days: 4)), + sentBy: '11', + status: MessageStatus.read, + ), + ), + ]; + } - static final messageList = [ - Message( - id: '1', - message: "Hi!", - createdAt: DateTime.now(), - // userId of who sends the message - sentBy: '1', - status: MessageStatus.read, - ), - Message( + static const currentUser = ChatUser( + id: '1', + name: 'Flutter', + profilePhoto: Data.profileImage, + ); + + static const otherUsers = [ + ChatUser( id: '2', - message: "Hi!", - createdAt: DateTime.now(), - sentBy: '2', - status: MessageStatus.read, + name: 'Simform', + profilePhoto: Data.profileImage, ), - Message( + ChatUser( id: '3', - message: "We can meet?I am free", - createdAt: DateTime.now(), - sentBy: '1', - status: MessageStatus.read, + name: 'Jhon', + profilePhoto: Data.profileImage, ), - Message( + ChatUser( id: '4', - message: "Can you write the time and place of the meeting?", - createdAt: DateTime.now(), - sentBy: '1', - status: MessageStatus.read, + name: 'Mike', + profilePhoto: Data.profileImage, ), - Message( + ChatUser( id: '5', - message: "That's fine", - createdAt: DateTime.now(), - sentBy: '2', - reaction: Reaction(reactions: ['\u{2764}'], reactedUserIds: ['1']), - status: MessageStatus.read, + name: 'Rich', + profilePhoto: Data.profileImage, ), + ]; + + static getMessageList({bool isExampleOne = true}) => [ + Message( + id: '1', + message: "How's it going?", + createdAt: DateTime.now().subtract(const Duration(days: 30)), + sentBy: '1', + status: MessageStatus.read, + ), + Message( + id: '2', + message: "I'm doing great, thanks for asking! How about you?", + createdAt: + DateTime.now().subtract(const Duration(days: 30, minutes: 5)), + sentBy: '2', + status: MessageStatus.read, + reaction: Reaction(reactions: ['\u{1F44D}'], reactedUserIds: ['1']), + ), + Message( + id: '3', + message: "I'm good too. Just got back from a vacation.", + createdAt: DateTime.now().subtract(const Duration(days: 7)), + sentBy: '1', + status: MessageStatus.read, + ), + Message( + id: '4', + message: "That's awesome! Where did you go?", + createdAt: + DateTime.now().subtract(const Duration(days: 7, minutes: 5)), + sentBy: '2', + status: MessageStatus.read, + ), + Message( + id: '5', + message: "I went to the mountains. It was beautiful. Here's a pic:", + createdAt: + DateTime.now().subtract(const Duration(days: 7, minutes: 10)), + sentBy: '1', + status: MessageStatus.read, + ), + Message( + id: '6', + message: + "https://images.pexels.com/photos/167699/pexels-photo-167699.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2", + createdAt: + DateTime.now().subtract(const Duration(days: 7, minutes: 10)), + messageType: MessageType.image, + sentBy: '1', + status: MessageStatus.read, + reaction: Reaction(reactions: ['\u{2764}'], reactedUserIds: ['2']), + ), + Message( + id: '7', + message: "Wow, that's breathtaking!", + createdAt: + DateTime.now().subtract(const Duration(days: 7, minutes: 12)), + sentBy: '2', + status: MessageStatus.read, + replyMessage: const ReplyMessage( + message: + "https://images.pexels.com/photos/167699/pexels-photo-167699.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2", + replyTo: '1', + replyBy: '2', + messageId: '6', + messageType: MessageType.image, + ), + ), + Message( + id: '8', + message: "We should go together sometime.", + createdAt: DateTime.now().subtract(const Duration(days: 1)), + sentBy: '1', + status: MessageStatus.read, + ), + Message( + id: '9', + message: "I'd love that! Let's plan it.", + createdAt: + DateTime.now().subtract(const Duration(days: 1, minutes: 5)), + sentBy: '2', + status: MessageStatus.read, + ), + Message( + id: '10', + message: "Are you free this weekend?", + createdAt: DateTime.now(), + sentBy: '1', + status: MessageStatus.read, + ), + if (isExampleOne) + Message( + id: '11', + message: 'Message unavailable', + createdAt: DateTime.now().subtract(const Duration(minutes: 4)), + sentBy: '2', + status: MessageStatus.delivered, + messageType: MessageType.custom, + ) + else + Message( + id: '18', + message: 'Twin Pines Mall - Hill Valley', + createdAt: DateTime.now().subtract(const Duration(minutes: 4)), + sentBy: '2', + status: MessageStatus.delivered, + messageType: MessageType.custom, + ), + Message( + id: '12', + message: "I think so. Let me check my calendar.", + createdAt: DateTime.now().subtract(const Duration(minutes: 2)), + sentBy: '2', + status: MessageStatus.read, + ), + Message( + id: '13', + message: "Yep, I'm free on Saturday.", + createdAt: DateTime.now().subtract(const Duration(minutes: 1)), + sentBy: '2', + status: MessageStatus.read, + reaction: Reaction(reactions: ['\u{1F44D}'], reactedUserIds: ['1']), + replyMessage: const ReplyMessage( + message: "Are you free this weekend?", + replyTo: '1', + replyBy: '2', + messageId: '10', + ), + ), + Message( + id: '14', + message: "Great! Let's do something fun.", + createdAt: DateTime.now().subtract(const Duration(minutes: 2)), + sentBy: '1', + status: MessageStatus.read, + ), + Message( + id: '200', + message: + "/data/user/0/com.simform.example/cache/28-09-25-11-07-421922108777129930344.m4a", + createdAt: DateTime.now().subtract(const Duration(minutes: 2)), + sentBy: '1', + messageType: MessageType.voice, + status: MessageStatus.read, + ), + Message( + id: '201', + message: + "/data/user/0/com.simform.example/cache/28-09-25-11-07-421922108777129930344.m4a", + createdAt: DateTime.now().subtract(const Duration(minutes: 2)), + sentBy: '2', + messageType: MessageType.voice, + status: MessageStatus.read, + ), + ]; + + static final oldMessageList = [ Message( - id: '6', - message: "When to go ?", - createdAt: DateTime.now(), - sentBy: '3', + id: '101', + message: "Hey!", + createdAt: DateTime.now().subtract(const Duration(days: 1)), + sentBy: '2', status: MessageStatus.read, ), Message( - id: '7', - message: "I guess Simform will reply", - createdAt: DateTime.now(), - sentBy: '4', + id: '102', + message: "Hi there!", + createdAt: DateTime.now().subtract(const Duration(days: 1, minutes: 5)), + sentBy: '1', status: MessageStatus.read, ), Message( - id: '8', - message: "https://bit.ly/3JHS2Wl", - createdAt: DateTime.now(), + id: '103', + message: "How was your weekend?", + createdAt: DateTime.now().subtract(const Duration(days: 1, minutes: 10)), sentBy: '2', - reaction: Reaction( - reactions: ['\u{2764}', '\u{1F44D}', '\u{1F44D}'], - reactedUserIds: ['2', '3', '4'], - ), status: MessageStatus.read, - replyMessage: const ReplyMessage( - message: "Can you write the time and place of the meeting?", - replyTo: '1', - replyBy: '2', - messageId: '4', - ), ), Message( - id: '9', - message: "Done", - createdAt: DateTime.now(), + id: '104', + message: "It was great! I went hiking.", + createdAt: DateTime.now().subtract(const Duration(days: 1, minutes: 15)), sentBy: '1', status: MessageStatus.read, - reaction: Reaction( - reactions: [ - '\u{2764}', - '\u{2764}', - '\u{2764}', - ], - reactedUserIds: ['2', '3', '4'], - ), ), Message( - id: '10', - message: "Thank you!!", + id: '105', + message: "Sounds fun! Did you take any pictures?", + createdAt: DateTime.now().subtract(const Duration(minutes: 20)), + sentBy: '2', status: MessageStatus.read, - createdAt: DateTime.now(), + ), + Message( + id: '106', + message: "I'll send it in a moment.", + createdAt: DateTime.now().subtract(const Duration(minutes: 17)), + messageType: MessageType.text, sentBy: '1', - reaction: Reaction( - reactions: ['\u{2764}', '\u{2764}', '\u{2764}', '\u{2764}'], - reactedUserIds: ['2', '4', '3', '1'], - ), + status: MessageStatus.read, ), Message( - id: '11', - message: "https://miro.medium.com/max/1000/0*s7of7kWnf9fDg4XM.jpeg", - createdAt: DateTime.now(), + id: '107', + message: "https://www.fillmurray.com/640/360", + createdAt: DateTime.now().subtract(const Duration(minutes: 16)), messageType: MessageType.image, sentBy: '1', - reaction: Reaction(reactions: ['\u{2764}'], reactedUserIds: ['2']), status: MessageStatus.read, ), Message( - id: '12', - message: "🤩🤩", - createdAt: DateTime.now(), + id: '108', + message: "Haha, nice one!", + createdAt: DateTime.now().subtract(const Duration(minutes: 15)), sentBy: '2', status: MessageStatus.read, + reaction: Reaction(reactions: ['\u{1F602}'], reactedUserIds: ['2']), ), Message( - id: '13', - message: "Check this out", - createdAt: DateTime.now(), - replyMessage: const ReplyMessage( - messageId: '140', - replyTo: '2', - replyBy: '1', - message: "This", - ), + id: '109', + message: "What are you up to?", + createdAt: DateTime.now().subtract(const Duration(minutes: 10)), sentBy: '1', status: MessageStatus.read, - ), - for (int i = 14; i <= 19; i++) - Message( - id: i.toString(), - message: "This is message number $i", - createdAt: - DateTime.now().subtract(Duration(hours: i % 2 == 0 ? i : i * 2)), - sentBy: (i % 2 == 0) ? '1' : '2', - status: MessageStatus.read, + replyMessage: const ReplyMessage( + message: "Haha, nice one!", + replyTo: '1', + replyBy: '2', + messageId: '9', ), + ), ]; } diff --git a/example/lib/main.dart b/example/lib/main.dart index 446181b5..58fb0a8e 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,8 +1,13 @@ import 'package:chatview/chatview.dart'; -import 'package:example/chat_view_list_screen.dart'; -import 'package:example/data.dart'; -import 'package:example/models/theme.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:intl/intl.dart'; + +import 'data.dart'; +import 'models/chatview_list_theme.dart'; +import 'models/chatview_theme.dart'; +import 'values/colors.dart'; +import 'values/icons.dart'; void main() { runApp(const Example()); @@ -17,423 +22,877 @@ class Example extends StatelessWidget { title: 'Flutter Chat UI Demo', debugShowCheckedModeBanner: false, theme: ThemeData( - primaryColor: const Color(0xffEE5366), - colorScheme: - ColorScheme.fromSwatch(accentColor: const Color(0xffEE5366)), + primaryColor: AppColors.instaPurple, + colorScheme: ColorScheme.fromSwatch(accentColor: AppColors.instaPurple), + ), + darkTheme: ThemeData( + primaryColor: AppColors.instaPurple, + colorScheme: ColorScheme.fromSwatch(accentColor: AppColors.instaPurple), ), - home: const ChatViewListScreen(), + home: const ExampleOneListScreen(), ); } } -class ChatScreen extends StatefulWidget { - const ChatScreen({required this.chat, super.key}); - - final ChatViewListItem chat; +class ExampleOneListScreen extends StatefulWidget { + const ExampleOneListScreen({super.key}); @override - State createState() => _ChatScreenState(); + State createState() => _ExampleOneListScreenState(); } -class _ChatScreenState extends State { - AppTheme theme = LightTheme(); - bool isDarkTheme = false; - late final ChatController _chatController; +class _ExampleOneListScreenState extends State { + ChatViewListTheme _theme = ChatViewListTheme.uiOneLight; + bool _isDarkTheme = false; + + final _searchController = TextEditingController(); + + final _chatListController = ChatViewListController( + initialChatList: Data.getChatList(), + scrollController: ScrollController(), + ); @override - void initState() { - super.initState(); - _chatController = ChatController( - initialMessageList: Data.messageList, - scrollController: ScrollController(), - currentUser: const ChatUser( - id: '1', - name: 'Flutter', - profilePhoto: Data.profileImage, - ), - otherUsers: const [ - ChatUser( - id: '2', - name: 'Simform', - profilePhoto: Data.profileImage, - ), - ChatUser( - id: '3', - name: 'Jhon', - profilePhoto: Data.profileImage, - ), - ChatUser( - id: '4', - name: 'Mike', - profilePhoto: Data.profileImage, - ), - ChatUser( - id: '5', - name: 'Rich', - profilePhoto: Data.profileImage, + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: _theme.backgroundColor, + body: SafeArea( + child: ChatViewList( + controller: _chatListController, + backgroundColor: _theme.backgroundColor, + header: _headerWidget(), + appbar: ChatViewListAppBar( + backgroundColor: _theme.backgroundColor, + leading: Icon( + Icons.arrow_back_ios_rounded, + color: _theme.iconColor, + ), + centerTitle: false, + scrolledUnderElevation: 0, + titleText: 'ChatViewList', + titleTextStyle: TextStyle( + fontSize: 20, + color: _theme.textColor, + fontWeight: FontWeight.bold, + ), + actions: [ + SvgPicture.asset( + AppIcons.chatDiscoveryAi, + colorFilter: ColorFilter.mode( + _theme.iconColor, + BlendMode.srcIn, + ), + ), + const SizedBox(width: 16), + SvgPicture.asset( + AppIcons.createPen, + colorFilter: ColorFilter.mode( + _theme.iconColor, + BlendMode.srcIn, + ), + ), + IconButton( + onPressed: _onThemeIconTap, + icon: Icon( + _isDarkTheme ? Icons.light_mode : Icons.dark_mode, + color: _theme.iconColor, + ), + ), + ], + ), + searchConfig: SearchConfig( + textEditingController: _searchController, + hintText: 'Ask Meta AI or search', + hintStyle: TextStyle( + fontSize: 16.4, + color: _theme.searchText, + fontWeight: FontWeight.w400, + ), + textStyle: TextStyle( + fontSize: 16.4, + color: _theme.textColor, + fontWeight: FontWeight.w400, + ), + textFieldBackgroundColor: _theme.searchBg, + borderRadius: const BorderRadius.all(Radius.circular(30)), + prefixIcon: SizedBox.square( + dimension: 48, + child: Align(child: SvgPicture.asset(AppIcons.ai, width: 24)), + ), + clearIcon: Icon(Icons.clear, color: _theme.iconColor), + contentPadding: const EdgeInsets.symmetric( + vertical: 0, + horizontal: 16, + ), + padding: const EdgeInsets.symmetric(horizontal: 16), + onSearch: (value) { + if (value.isEmpty) { + _chatListController.clearSearch(); + return null; + } + + List chats = + _chatListController.chatListMap.values.toList(); + + final list = chats + .where((chat) => + chat.name.toLowerCase().contains(value.toLowerCase())) + .toList(); + return list; + }, + ), + menuConfig: ChatMenuConfig( + deleteCallback: (chat) => _chatListController.removeChat(chat.id), + muteStatusCallback: (result) => _chatListController.updateChat( + result.chat.id, + (previousChat) => previousChat.copyWith( + settings: previousChat.settings.copyWith( + muteStatus: result.status, + ), + ), + ), + pinStatusCallback: (result) => _chatListController.updateChat( + result.chat.id, + (previousChat) => previousChat.copyWith( + settings: previousChat.settings.copyWith( + pinStatus: result.status, + ), + ), + ), + ), + tileConfig: ListTileConfig( + showUserActiveStatusIndicator: false, + trailingBuilder: (chat) => _customTrailingWidget(chat), + userNameBuilder: (chat) => _customUserNameWidget(chat), + lastMessageTileBuilder: (chat) => _customLastMessageTile(chat), + onTap: (chat) => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ExampleOneChatScreen(chat: chat), + ), + ), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + userAvatarConfig: UserAvatarConfig( + radius: 26, + backgroundColor: _theme.secondaryBg, + ), + typingStatusConfig: TypingStatusConfig( + textBuilder: (chat) => 'Typing...', + textStyle: TextStyle( + fontSize: 13, + color: _theme.lastMessageText, + fontStyle: FontStyle.italic, + ), + ), + ), ), - ], + ), ); } @override void dispose() { - // ChatController should be disposed to avoid memory leaks - _chatController.dispose(); + _chatListController.dispose(); + _searchController.dispose(); super.dispose(); } - void _showHideTypingIndicator() { - _chatController.setTypingIndicator = !_chatController.showTypingIndicator; + void _onThemeIconTap() { + setState(() { + if (_isDarkTheme) { + _theme = ChatViewListTheme.uiOneLight; + _isDarkTheme = false; + } else { + _theme = ChatViewListTheme.uiOneDart; + _isDarkTheme = true; + } + }); } - void receiveMessage() async { - _chatController.addMessage( - Message( - id: DateTime.now().toString(), - message: 'I will schedule the meeting.', - createdAt: DateTime.now(), - sentBy: '2', - ), + Widget _customTrailingWidget(ChatViewListItem chat) { + final highlight = (chat.unreadCount ?? 0) > 0; + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (highlight) ...[ + const CircleAvatar( + radius: 4, + backgroundColor: AppColors.instaUnreadCountDot, + ), + const SizedBox(width: 12), + ], + SvgPicture.asset( + AppIcons.camera2, + colorFilter: ColorFilter.mode( + highlight ? _theme.iconColor : AppColors.instaDarkGrey, + BlendMode.srcIn, + ), + ), + ], ); - await Future.delayed(const Duration(milliseconds: 500)); - _chatController.addReplySuggestions([ - const SuggestionItemData(text: 'Thanks.'), - const SuggestionItemData(text: 'Thank you very much.'), - const SuggestionItemData(text: 'Great.') - ]); } - @override - Widget build(BuildContext context) { - return Scaffold( - body: ChatView( - chatController: _chatController, - onSendTap: _onSendTap, - loadMoreData: (direction, message) async { - await Future.delayed(const Duration(seconds: 1)); - _chatController.loadMoreData( - [ - Message( - id: (_chatController.initialMessageList.length + 1).toString(), - message: '${direction.name} meeting 1.', - createdAt: direction.isPrevious - ? DateTime(2015, 21, 00) - : DateTime.now(), - sentBy: '2', + Widget _headerWidget() { + return Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), + child: Row( + children: [ + Expanded( + child: Text( + 'Messages', + maxLines: 1, + style: TextStyle( + fontSize: 16, + color: _theme.textColor, + fontWeight: FontWeight.bold, ), - Message( - id: (_chatController.initialMessageList.length + 2).toString(), - message: '${direction.name} meeting 2.', - createdAt: direction.isPrevious - ? DateTime(2015, 21, 00) - : DateTime.now(), - sentBy: '2', - ), - if (direction.isNext && - _chatController.canRevertToInitialMessageList) - Data.messageList.first, - ], - direction: direction, - ); - }, - featureActiveConfig: const FeatureActiveConfig( - lastSeenAgoBuilderVisibility: true, - receiptsBuilderVisibility: true, - enableScrollToBottomButton: true, - enablePagination: true, - ), - scrollToBottomButtonConfig: ScrollToBottomButtonConfig( - backgroundColor: theme.textFieldBackgroundColor, - border: Border.all( - color: isDarkTheme ? Colors.transparent : Colors.grey, + overflow: TextOverflow.ellipsis, + ), ), - icon: Icon( - Icons.keyboard_arrow_down_rounded, - color: theme.themeIconColor, - weight: 10, - size: 30, + Text( + 'Requests', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle(color: _theme.searchText), ), - ), - chatViewState: ChatViewState.hasMessages, - chatViewStateConfig: ChatViewStateConfiguration( - loadingWidgetConfig: ChatViewStateWidgetConfiguration( - loadingIndicatorColor: theme.outgoingChatBubbleColor, + ], + ), + ); + } + + Widget _customUserNameWidget(ChatViewListItem chat) { + final highlightText = (chat.unreadCount ?? 0) > 0; + return Row( + children: [ + Flexible( + child: Text( + chat.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 14, + color: highlightText ? _theme.textColor : _theme.lastMessageText, + fontWeight: highlightText ? FontWeight.w500 : FontWeight.normal, + ), ), - onReloadButtonTap: () {}, ), - typeIndicatorConfig: TypeIndicatorConfiguration( - flashingCircleBrightColor: theme.flashingCircleBrightColor, - flashingCircleDarkColor: theme.flashingCircleDarkColor, - ), - appBar: ChatViewAppBar( - elevation: theme.elevation, - backGroundColor: theme.appBarColor, - profilePicture: widget.chat.imageUrl, - backArrowColor: theme.backArrowColor, - chatTitle: widget.chat.name, - chatTitleTextStyle: TextStyle( - color: theme.appBarTitleTextStyle, - fontWeight: FontWeight.bold, - fontSize: 18, - letterSpacing: 0.25, - ), - userStatus: - widget.chat.userActiveStatus.isOnline ? 'Online' : 'Offline', - userStatusTextStyle: const TextStyle(color: Colors.grey), - actions: [ - IconButton( - onPressed: _onThemeIconTap, - icon: Icon( - isDarkTheme - ? Icons.brightness_4_outlined - : Icons.dark_mode_outlined, - color: theme.themeIconColor, - ), - ), - IconButton( - tooltip: 'Toggle TypingIndicator', - onPressed: _showHideTypingIndicator, - icon: Icon( - Icons.keyboard, - color: theme.themeIconColor, - ), + if (chat.settings.pinStatus.isPinned) ...[ + const SizedBox(width: 6), + SvgPicture.asset( + AppIcons.pinned, + width: 16, + height: 16, + colorFilter: ColorFilter.mode( + _theme.lastMessageText, + BlendMode.srcIn, ), - IconButton( - tooltip: 'Simulate Message receive', - onPressed: receiveMessage, - icon: Icon( - Icons.supervised_user_circle, - color: theme.themeIconColor, - ), + ), + ], + ], + ); + } + + Widget _customLastMessageTile(ChatViewListItem chat) { + final message = chat.lastMessage; + final unreadCount = chat.unreadCount ?? 0; + final highlightText = unreadCount > 0; + if (message == null) { + return const SizedBox.shrink(); + } + + String prefix = switch (message.status) { + MessageStatus.read => 'Seen', + MessageStatus.delivered => 'Sent', + MessageStatus.undelivered => 'Failed to send.', + MessageStatus.pending => 'Sending...', + }; + + final showDisplayMessage = prefix == 'Seen' || prefix == 'Sent'; + + final String display; + if (highlightText && unreadCount != 1) { + display = '$unreadCount new messages'; + } else if (!showDisplayMessage) { + display = prefix; + } else if (message.sentBy == 'me') { + display = message.createdAt.getTimestamp(prefix: prefix); + } else { + display = switch (message.messageType) { + MessageType.image => '$prefix a Photo', + MessageType.text => message.message, + MessageType.voice => '$prefix a Audio', + MessageType.custom => '$prefix a Message', + }; + } + + return Row( + children: [ + if (showDisplayMessage && message.messageType != MessageType.text) ...[ + switch (message.messageType) { + MessageType.image => const Icon(Icons.photo, size: 14), + MessageType.voice => const Icon(Icons.mic, size: 14), + MessageType.text || MessageType.custom => const SizedBox.shrink(), + }, + const SizedBox(width: 5), + ], + Flexible( + child: Text( + display, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 14, + color: message.status.isUndelivered + ? Colors.red + : highlightText + ? _theme.textColor + : _theme.lastMessageText, + fontWeight: highlightText ? FontWeight.w500 : FontWeight.normal, ), - ], + ), ), - chatBackgroundConfig: ChatBackgroundConfiguration( - messageTimeIconColor: theme.messageTimeIconColor, - messageTimeTextStyle: TextStyle(color: theme.messageTimeTextColor), - defaultGroupSeparatorConfig: DefaultGroupSeparatorConfiguration( - textStyle: TextStyle( - color: theme.chatHeaderColor, - fontSize: 17, + if (showDisplayMessage) ...[ + Text( + ' · ${message.createdAt.getTimeAgo}', + style: TextStyle( + fontSize: 14, + color: _theme.lastMessageText, + fontWeight: FontWeight.normal, ), ), - backgroundColor: theme.backgroundColor, - ), - sendMessageConfig: SendMessageConfiguration( - imagePickerIconsConfig: ImagePickerIconsConfiguration( - cameraIconColor: theme.cameraIconColor, - galleryIconColor: theme.galleryIconColor, - ), - replyMessageColor: theme.replyMessageColor, - defaultSendButtonColor: theme.sendButtonColor, - replyDialogColor: theme.replyDialogColor, - replyTitleColor: theme.replyTitleColor, - textFieldBackgroundColor: theme.textFieldBackgroundColor, - closeIconColor: theme.closeIconColor, - textFieldConfig: TextFieldConfiguration( - onMessageTyping: (status) { - /// Do with status - debugPrint(status.toString()); - }, - compositionThresholdTime: const Duration(seconds: 1), - textStyle: TextStyle(color: theme.textFieldTextColor), + ], + if (chat.settings.muteStatus.isMuted) ...[ + const SizedBox(width: 6), + Icon( + Icons.notifications_off_outlined, + size: 16, + color: _theme.lastMessageText, ), - micIconColor: theme.replyMicIconColor, - voiceRecordingConfiguration: VoiceRecordingConfiguration( - backgroundColor: theme.waveformBackgroundColor, - recorderIconColor: theme.recordIconColor, - waveStyle: WaveStyle( - showMiddleLine: false, - waveColor: theme.waveColor ?? Colors.white, - extendWaveform: true, - ), + ], + ], + ); + } +} + +class ExampleOneChatScreen extends StatefulWidget { + const ExampleOneChatScreen({required this.chat, super.key}); + + final ChatViewListItem chat; + + @override + State createState() => _ExampleOneChatScreenState(); +} + +class _ExampleOneChatScreenState extends State { + ChatViewTheme _theme = ChatViewTheme.uiOneLight; + bool _isDarkTheme = false; + bool _isTopPaginationCalled = false; + bool _isBottomPaginationCalled = false; + + final ChatController _chatController = ChatController( + initialMessageList: Data.getMessageList(), + scrollController: ScrollController(), + currentUser: Data.currentUser, + otherUsers: Data.otherUsers, + ); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: ChatView( + chatController: _chatController, + onSendTap: _onSendTap, + isLastPage: () => _isTopPaginationCalled && _isBottomPaginationCalled, + loadMoreData: (direction, message) async { + if (direction.isNext) { + if (_isBottomPaginationCalled) { + return; + } + _isBottomPaginationCalled = true; + } else if (direction.isPrevious) { + if (_isTopPaginationCalled) { + return; + } + _isTopPaginationCalled = true; + } + await Future.delayed(const Duration(seconds: 1)); + _chatController.loadMoreData( + direction.isPrevious + ? [ + Message( + id: DateTime.timestamp() + .subtract(const Duration(days: 30, minutes: 10)) + .toIso8601String(), + message: "Long time no see!", + createdAt: DateTime.now() + .subtract(const Duration(days: 30, minutes: 10)), + sentBy: '2', + status: MessageStatus.read, + ), + Message( + id: DateTime.timestamp() + .subtract(const Duration(days: 30, minutes: 5)) + .toIso8601String(), + message: "Indeed! I was about to ping you.", + createdAt: DateTime.now() + .subtract(const Duration(days: 30, minutes: 5)), + sentBy: '1', + status: MessageStatus.read, + ), + ] + : [ + Message( + id: '14', + message: "How about a movie marathon?", + createdAt: + DateTime.now().subtract(const Duration(minutes: 1)), + sentBy: '2', + status: MessageStatus.read, + ), + Message( + id: '15', + message: "Sounds great! I'm in. 🎬", + createdAt: DateTime.now(), + sentBy: '1', + status: MessageStatus.read, + ), + ], + direction: direction, + ); + }, + featureActiveConfig: const FeatureActiveConfig( + lastSeenAgoBuilderVisibility: true, + receiptsBuilderVisibility: true, + enableOtherUserName: false, + enableScrollToBottomButton: true, + enableOtherUserProfileAvatar: true, + enablePagination: true, ), - ), - chatBubbleConfig: ChatBubbleConfiguration( - outgoingChatBubbleConfig: ChatBubble( - linkPreviewConfig: LinkPreviewConfiguration( - backgroundColor: theme.linkPreviewOutgoingChatColor, - bodyStyle: theme.outgoingChatLinkBodyStyle, - titleStyle: theme.outgoingChatLinkTitleStyle, - ), - receiptsWidgetConfig: - const ReceiptsWidgetConfig(showReceiptsIn: ShowReceiptsIn.all), - color: theme.outgoingChatBubbleColor, - ), - inComingChatBubbleConfig: ChatBubble( - linkPreviewConfig: LinkPreviewConfiguration( - linkStyle: TextStyle( - color: theme.inComingChatBubbleTextColor, - decoration: TextDecoration.underline, + scrollToBottomButtonConfig: const ScrollToBottomButtonConfig( + padding: EdgeInsets.only(bottom: 8, right: 12), + insidePadding: EdgeInsets.all(10), + alignment: ScrollButtonAlignment.right, + icon: Icon(Icons.arrow_downward_rounded), + border: Border.fromBorderSide(BorderSide.none), + boxShadow: [ + BoxShadow( + blurRadius: 8, + offset: Offset(0, 4), + color: Colors.black26, ), - backgroundColor: theme.linkPreviewIncomingChatColor, - bodyStyle: theme.incomingChatLinkBodyStyle, - titleStyle: theme.incomingChatLinkTitleStyle, - ), - textStyle: TextStyle(color: theme.inComingChatBubbleTextColor), - onMessageRead: (message) { - /// send your message reciepts to the other client - debugPrint('Message Read'); - }, - senderNameTextStyle: - TextStyle(color: theme.inComingChatBubbleTextColor), - color: theme.inComingChatBubbleColor, + ], ), - ), - replyPopupConfig: ReplyPopupConfiguration( - backgroundColor: theme.replyPopupColor, - buttonTextStyle: TextStyle(color: theme.replyPopupButtonColor), - topBorderColor: theme.replyPopupTopBorderColor, - ), - reactionPopupConfig: ReactionPopupConfiguration( - shadow: BoxShadow( - color: isDarkTheme ? Colors.black54 : Colors.grey.shade400, - blurRadius: 20, + chatViewState: ChatViewState.hasMessages, + typeIndicatorConfig: TypeIndicatorConfiguration( + customIndicator: _customTypingIndicator(), ), - backgroundColor: theme.reactionPopupColor, - ), - messageConfig: MessageConfiguration( - messageReactionConfig: MessageReactionConfiguration( - backgroundColor: theme.messageReactionBackGroundColor, - borderColor: theme.messageReactionBackGroundColor, - reactedUserCountTextStyle: - TextStyle(color: theme.inComingChatBubbleTextColor), - reactionCountTextStyle: - TextStyle(color: theme.inComingChatBubbleTextColor), - reactionsBottomSheetConfig: ReactionsBottomSheetConfiguration( - backgroundColor: theme.backgroundColor, - reactedUserTextStyle: TextStyle( - color: theme.inComingChatBubbleTextColor, - ), - reactionWidgetDecoration: BoxDecoration( - color: theme.inComingChatBubbleColor, - boxShadow: [ - BoxShadow( - color: isDarkTheme ? Colors.black12 : Colors.grey.shade200, - offset: const Offset(0, 20), - blurRadius: 40, - ) - ], - borderRadius: BorderRadius.circular(10), - ), + appBar: ChatViewAppBar( + elevation: 0, + chatTitle: widget.chat.name, + leading: IconButton( + onPressed: Navigator.of(context).maybePop, + icon: Icon(Icons.arrow_back_ios, color: _theme.iconColor), ), - ), - imageMessageConfig: ImageMessageConfiguration( - margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 15), - shareIconConfig: ShareIconConfiguration( - defaultIconBackgroundColor: theme.shareIconBackgroundColor, - defaultIconColor: theme.shareIconColor, + chatTitleTextStyle: TextStyle( + fontSize: 18, + color: _theme.titleColor, + fontWeight: FontWeight.w600, ), - ), - ), - profileCircleConfig: const ProfileCircleConfiguration( - profileImageUrl: Data.profileImage, - ), - repliedMessageConfig: RepliedMessageConfiguration( - loadOldReplyMessage: (id) async { - _chatController.replaceMessageList( - [ - Message( - id: '123', - message: 'What about the meeting.', - createdAt: DateTime(2015, 6, 21), - sentBy: '1', + profilePicture: widget.chat.imageUrl, + backGroundColor: _theme.backgroundColor, + userStatus: '@${widget.chat.name}', + userStatusTextStyle: TextStyle( + fontSize: 13, + color: _theme.titleColor, + ), + actions: [ + IconButton( + onPressed: () {}, + icon: SvgPicture.asset( + AppIcons.chatDiscoveryAi, + colorFilter: ColorFilter.mode( + _theme.iconColor, + BlendMode.srcIn, + ), ), - Message( - id: '124', - message: 'I will ignore the meeting.', - createdAt: DateTime(2015, 6, 21), - sentBy: '2', + ), + IconButton( + onPressed: () {}, + icon: SvgPicture.asset( + AppIcons.phone, + colorFilter: ColorFilter.mode( + _theme.iconColor, + BlendMode.srcIn, + ), ), - Message( - id: '125', - message: 'I will join the meeting.', - createdAt: DateTime(2015, 6, 20), - sentBy: '3', + ), + IconButton( + // Handle video call + onPressed: () {}, + icon: SvgPicture.asset( + AppIcons.video, + colorFilter: ColorFilter.mode( + _theme.iconColor, + BlendMode.srcIn, + ), ), - Message( - id: '126', - message: 'Such a boring meeting.', - createdAt: DateTime(2015, 6, 21), - sentBy: '1', + ), + PopupMenuButton( + icon: Icon( + Icons.more_vert_rounded, + color: _theme.iconColor, ), - Message( - id: '127', - message: 'Stop talking about the meeting.', - createdAt: DateTime(2015, 6, 21), - sentBy: '4', + itemBuilder: (context) => [ + const PopupMenuItem( + value: 'toggle_typing_indicator', + child: Text('Toggle TypingIndicator'), + ), + const PopupMenuItem( + value: 'simulate_message_receive', + child: Text('Simulate Message receive'), + ), + PopupMenuItem( + value: 'dark_theme', + child: Text(' ${_isDarkTheme ? 'Light' : 'Dark'} Mode'), + ), + ], + onSelected: (value) { + switch (value) { + case 'toggle_typing_indicator': + _showHideTypingIndicator(); + case 'simulate_message_receive': + receiveMessage(); + case 'dark_theme': + _onThemeIconTap(); + } + }, + ), + const SizedBox(width: 12), + ], + ), + chatBackgroundConfig: ChatBackgroundConfiguration( + backgroundColor: _theme.backgroundColor, + groupSeparatorBuilder: (separator) => + _customSeparatorWidget(separator), + ), + sendMessageConfig: SendMessageConfiguration( + closeIconColor: _theme.iconColor, + replyTitleColor: _theme.textColor, + replyMessageColor: _theme.textColor, + replyDialogColor: _theme.backgroundColor, + defaultSendButtonColor: Colors.white, + textFieldBackgroundColor: _theme.textField, + voiceRecordingConfiguration: VoiceRecordingConfiguration( + recorderIconColor: _theme.iconColor, + waveStyle: WaveStyle( + extendWaveform: true, + showMiddleLine: false, + waveColor: _theme.iconColor, + durationLinesColor: AppColors.black20, + backgroundColor: Colors.transparent, + scaleFactor: 60, + waveThickness: 3, + spacing: 4, + ), + ), + sendButtonStyle: IconButton.styleFrom( + backgroundColor: AppColors.instaPurple, + padding: const EdgeInsets.symmetric(horizontal: 16), + ), + textFieldConfig: TextFieldConfiguration( + hintText: 'Message...', + hideLeadingActionsOnType: false, + onMessageTyping: (status) { + /// Do with status + debugPrint(status.toString()); + }, + compositionThresholdTime: const Duration(seconds: 1), + textStyle: TextStyle(color: _theme.textColor), + contentPadding: const EdgeInsets.symmetric(horizontal: 12), + leadingActions: (context, controller) => + controller.text.trim().isEmpty + ? [ + CameraActionButton( + icon: const Icon( + Icons.camera_alt_rounded, + color: Colors.white, + ), + style: IconButton.styleFrom( + backgroundColor: AppColors.instaPurple, + ), + onPressed: (path, replyMessage) { + if (path?.isEmpty ?? true) return; + _chatController.addMessage( + Message( + id: DateTime.now() + .millisecondsSinceEpoch + .toString(), + message: path!, + createdAt: DateTime.now(), + messageType: MessageType.image, + sentBy: _chatController.currentUser.id, + replyMessage: + replyMessage ?? const ReplyMessage(), + ), + ); + _chatController.addMessage( + Message( + message: controller.text, + id: DateTime.now() + .millisecondsSinceEpoch + .toString(), + createdAt: DateTime.now(), + sentBy: _chatController.currentUser.id, + replyMessage: + replyMessage ?? const ReplyMessage(), + ), + ); + }, + ), + ] + : [ + IconButton( + icon: const Icon( + Icons.search_rounded, + color: Colors.white, + ), + style: IconButton.styleFrom( + backgroundColor: AppColors.instaPurple, + ), + onPressed: () => + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Search button pressed')), + ), + ), + ], + trailingActions: (context, controller) => [ + GalleryActionButton( + icon: Icon( + Icons.photo_rounded, + size: 30, + color: _theme.iconColor, + ), + onPressed: (path, replyMessage) { + if (path?.isEmpty ?? true) return; + _chatController.addMessage( + Message( + id: DateTime.now().millisecondsSinceEpoch.toString(), + message: path!, + createdAt: DateTime.now(), + messageType: MessageType.image, + sentBy: _chatController.currentUser.id, + replyMessage: replyMessage ?? const ReplyMessage(), + ), + ); + }, ), - Message( - id: '140', - message: 'Stop talking about the meeting.', - createdAt: DateTime(2015, 6, 21), - sentBy: '4', + EmojiPickerActionButton( + context: context, + onPressed: (emoji, replyMessage) { + if (emoji?.isEmpty ?? true) return; + controller.text = controller.text += emoji!; + _chatController.addMessage( + Message( + message: controller.text, + id: DateTime.now().millisecondsSinceEpoch.toString(), + createdAt: DateTime.now(), + sentBy: _chatController.currentUser.id, + replyMessage: replyMessage ?? const ReplyMessage(), + ), + ); + }, + icon: SvgPicture.asset( + AppIcons.sticker, + width: 30, + height: 30, + colorFilter: ColorFilter.mode( + _theme.iconColor, + BlendMode.srcIn, + ), + ), ), - Message( - id: '128', - message: 'Stop talking about the meeting.', - createdAt: DateTime(2015, 6, 21), - sentBy: '4', + IconButton( + onPressed: () => ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Add attachment')), + ), + icon: Icon( + Icons.add_circle_outline_rounded, + size: 30, + color: _theme.iconColor, + ), ), - Message( - id: '129', - message: 'Stop talking about the meeting.', - createdAt: DateTime(2015, 6, 21), - sentBy: '4', + ], + ), + ), + chatBubbleConfig: ChatBubbleConfiguration( + outgoingChatBubbleConfig: const ChatBubble( + linkPreviewConfig: LinkPreviewConfiguration( + backgroundColor: Colors.white12, + linkStyle: TextStyle( + fontSize: 16, + color: Colors.white, + ), + ), + color: AppColors.instaPurple, + textStyle: TextStyle(color: Colors.white, fontSize: 16), + receiptsWidgetConfig: ReceiptsWidgetConfig( + showReceiptsIn: ShowReceiptsIn.lastMessage, + ), + ), + inComingChatBubbleConfig: ChatBubble( + linkPreviewConfig: LinkPreviewConfiguration( + backgroundColor: Colors.white12, + linkStyle: TextStyle( + fontSize: 16, + color: _theme.textColor, ), - Message( - id: '130', - message: 'Stop talking about the meeting.', - createdAt: DateTime(2015, 6, 21), - sentBy: '4', + ), + color: _theme.incomingBubble, + textStyle: TextStyle(color: _theme.textColor, fontSize: 16), + onMessageRead: (message) { + /// send your message reciepts to the other client + debugPrint('Message Read'); + }, + ), + ), + reactionPopupConfig: const ReactionPopupConfiguration( + backgroundColor: Colors.white, + shadow: BoxShadow( + blurRadius: 8, + offset: Offset(0, 4), + color: Colors.black26, + ), + ), + messageConfig: MessageConfiguration( + voiceMessageConfig: VoiceMessageConfiguration( + margin: EdgeInsets.zero, + padding: EdgeInsets.zero, + playIcon: (isMessageBySender) => Icon( + Icons.play_arrow_rounded, + size: 24, + color: isMessageBySender ? Colors.white : _theme.iconColor, + ), + pauseIcon: (isMessageBySender) => Icon( + Icons.pause_rounded, + size: 24, + color: isMessageBySender ? Colors.white : _theme.iconColor, + ), + inComingPlayerWaveStyle: PlayerWaveStyle( + liveWaveColor: _theme.iconColor, + fixedWaveColor: AppColors.black20, + backgroundColor: Colors.transparent, + scaleFactor: 60, + waveThickness: 3, + spacing: 4, + ), + outgoingPlayerWaveStyle: PlayerWaveStyle( + liveWaveColor: _theme.iconColor, + fixedWaveColor: AppColors.white20, + backgroundColor: Colors.transparent, + scaleFactor: 60, + waveThickness: 3, + spacing: 4, + ), + ), + messageReactionConfig: MessageReactionConfiguration( + backgroundColor: _theme.incomingBubble, + borderColor: _theme.backgroundColor, + borderWidth: 2, + reactionsBottomSheetConfig: ReactionsBottomSheetConfiguration( + backgroundColor: _theme.backgroundColor, + reactedUserTextStyle: TextStyle( + color: _theme.textColor, ), - Message( - id: '131', - message: 'Stop talking about the meeting.', - createdAt: DateTime(2015, 6, 21), - sentBy: '4', + reactionWidgetDecoration: BoxDecoration( + color: _theme.incomingBubble, + borderRadius: const BorderRadius.all(Radius.circular(12)), ), - ], - ); - }, - backgroundColor: theme.repliedMessageColor, - verticalBarColor: theme.verticalBarColor, - repliedMsgAutoScrollConfig: RepliedMsgAutoScrollConfig( - enableHighlightRepliedMsg: true, - highlightColor: Colors.pinkAccent.shade100, - highlightScale: 1.1, - ), - textStyle: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - letterSpacing: 0.25, - ), - replyTitleTextStyle: TextStyle(color: theme.repliedTitleTextColor), - ), - swipeToReplyConfig: SwipeToReplyConfiguration( - replyIconColor: theme.swipeToReplyIconColor, - ), - replySuggestionsConfig: ReplySuggestionsConfig( - itemConfig: SuggestionItemConfig( - decoration: BoxDecoration( - color: theme.textFieldBackgroundColor, - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: theme.outgoingChatBubbleColor ?? Colors.white, ), ), - textStyle: TextStyle( - color: isDarkTheme ? Colors.white : Colors.black, + customMessageBuilder: (message) { + if (message.message == 'Message unavailable') { + return _buildUnavailableMessage(message); + } + return const SizedBox.shrink(); + }, + ), + profileCircleConfig: const ProfileCircleConfiguration( + padding: EdgeInsets.only(right: 4), + profileImageUrl: Data.profileImage, + ), + repliedMessageConfig: RepliedMessageConfiguration( + backgroundColor: _theme.replyBg, + verticalBarColor: _theme.verticalDivider, + textStyle: TextStyle(color: _theme.replyText), + replyTitleTextStyle: TextStyle( + fontSize: 12, + color: _theme.titleColor, + ), + loadOldReplyMessage: (id) async => { + // Implement logic to fetch old replied message with surrounding context using message ID + }, + repliedMsgAutoScrollConfig: RepliedMsgAutoScrollConfig( + enableHighlightRepliedMsg: true, + highlightScale: 1.1, + highlightColor: Colors.grey.shade300, + ), + ), + swipeToReplyConfig: SwipeToReplyConfiguration( + replyIconColor: _theme.iconColor, + replyIconBackgroundColor: _theme.backgroundColor, + replyIconProgressRingColor: _theme.incomingBubble, + ), + replySuggestionsConfig: ReplySuggestionsConfig( + itemConfig: SuggestionItemConfig( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: _theme.incomingBubble, + borderRadius: BorderRadius.circular(30), + border: Border.all(color: _theme.incomingBubble), + ), + textStyle: TextStyle(color: _theme.textColor), + ), + onTap: (item) => _onSendTap( + item.text, + const ReplyMessage(), + MessageType.text, ), ), - onTap: (item) => - _onSendTap(item.text, const ReplyMessage(), MessageType.text), ), ), ); } + @override + void dispose() { + // ChatController should be disposed to avoid memory leaks + _chatController.dispose(); + super.dispose(); + } + + void _showHideTypingIndicator() { + _chatController.setTypingIndicator = !_chatController.showTypingIndicator; + } + + void receiveMessage() async { + _chatController.addMessage( + Message( + id: DateTime.now().toString(), + message: 'I will schedule the meeting.', + createdAt: DateTime.now(), + sentBy: '2', + ), + ); + await Future.delayed(const Duration(milliseconds: 500)); + _chatController.addReplySuggestions([ + const SuggestionItemData(text: 'Thanks.'), + const SuggestionItemData(text: 'Thank you very much.'), + const SuggestionItemData(text: 'Great.') + ]); + } + void _onSendTap( String message, ReplyMessage replyMessage, @@ -464,13 +923,183 @@ class _ChatScreenState extends State { void _onThemeIconTap() { setState(() { - if (isDarkTheme) { - theme = LightTheme(); - isDarkTheme = false; + if (_isDarkTheme) { + _theme = ChatViewTheme.uiOneLight; + _isDarkTheme = false; } else { - theme = DarkTheme(); - isDarkTheme = true; + _theme = ChatViewTheme.uiOneDark; + _isDarkTheme = true; } }); } + + Widget _buildUnavailableMessage(Message message) { + return Container( + constraints: + BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.75), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + margin: EdgeInsets.fromLTRB( + 5, 0, 6, message.reaction.reactions.isNotEmpty ? 15 : 2), + decoration: BoxDecoration( + color: _theme.incomingBubble, + borderRadius: const BorderRadius.all(Radius.circular(18)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + message.message, + style: TextStyle( + fontSize: 16, + color: _theme.textColor, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 4), + Text( + 'This content may have been deleted by its owner or hidden by their privacy settings.', + style: TextStyle(color: _theme.textColor, fontSize: 15), + ), + ], + ), + ); + } + + Widget _customSeparatorWidget(String separator) { + final date = DateTime.tryParse(separator); + if (date == null) { + return const SizedBox.shrink(); + } + String separatorDate; + final now = DateTime.now(); + if (date.day == now.day && + date.month == now.month && + date.year == now.year) { + separatorDate = DateFormat('h:mm a').format(date); + } else if (date.day == now.day - 1 && + date.month == now.month && + date.year == now.year) { + separatorDate = 'Yesterday at ${DateFormat('h:mm a').format(date)}'; + } else if (date.isAfter(now.subtract(const Duration(days: 7)))) { + separatorDate = DateFormat('EEE h:mm a').format(date); + } else { + separatorDate = DateFormat('d MMM AT h:mm a').format(date); + } + return Align( + heightFactor: 2, + child: Text( + separatorDate.toUpperCase(), + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: _theme.titleColor, + ), + ), + ); + } + + Widget _customTypingIndicator() { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (_chatController.otherUsers.firstOrNull?.profilePhoto + case final image?) ...[ + CircleAvatar( + radius: 16, + backgroundImage: NetworkImage(image), + backgroundColor: _theme.incomingBubble, + ), + ], + Container( + margin: const EdgeInsets.only(left: 8), + decoration: BoxDecoration( + color: _theme.incomingBubble, + borderRadius: const BorderRadius.all(Radius.circular(30)), + ), + padding: const EdgeInsets.symmetric( + vertical: 18, + horizontal: 14, + ), + child: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + CircleAvatar( + radius: 2.75, + backgroundColor: AppColors.instaDarkGrey, + ), + SizedBox(width: 3), + CircleAvatar( + radius: 2.75, + backgroundColor: AppColors.instaDarkGrey, + ), + SizedBox(width: 3), + CircleAvatar( + radius: 2.75, + backgroundColor: AppColors.instaDarkGrey, + ), + ], + ), + ), + ], + ); + } +} + +extension on DateTime { + String get getTimeAgo { + final now = DateTime.now(); + final diff = now.difference(this); + + if (diff.inSeconds < 60) return 'now'; + if (diff.inMinutes < 60) return '${diff.inMinutes}m'; + if (diff.inHours < 24) return '${diff.inHours}h'; + if (diff.inDays < 7) return '${diff.inDays}d'; + if (diff.inDays < 30) { + final weeks = (diff.inDays / 7).floor(); + return '${weeks}w'; + } + return now.year == year + ? DateFormat('MMM d').format(this) // e.g. Aug 5 + : DateFormat('MMM d, y').format(this); // e.g. Aug 5, 2023; + } + + String getTimestamp({required String prefix}) { + String timeStamp; + if (isNow) { + timeStamp = 'just Now'; + } else { + final difference = DateTime.now().difference(this); + // if the difference is 1 day, show hours wise + if (difference.inDays == 0) { + if (difference.inHours > 0) { + timeStamp = '${difference.inHours}h ago'; + } else if (difference.inMinutes > 0) { + timeStamp = '${difference.inMinutes}m ago'; + } else { + timeStamp = 'now'; + } + // if less than 7 days, show days wise + } else if (isLast7Days) { + final day = DateFormat('EEEE').format(this); + timeStamp = 'on $day'; + } else { + timeStamp = ''; + } + } + return '$prefix $timeStamp'; + } + + bool get isNow { + final now = DateTime.now(); + return year == now.year && + month == now.month && + day == now.day && + hour == now.hour && + minute == now.minute; + } + + bool get isLast7Days { + final now = DateTime.now(); + return now.difference(DateTime(year, month, day)).inDays < 7; + } } diff --git a/example/lib/models/chatview_list_theme.dart b/example/lib/models/chatview_list_theme.dart new file mode 100644 index 00000000..29f9ac64 --- /dev/null +++ b/example/lib/models/chatview_list_theme.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; + +class ChatViewListTheme { + const ChatViewListTheme({ + required this.textColor, + required this.iconColor, + required this.searchBg, + required this.searchText, + required this.lastMessageText, + required this.backgroundColor, + required this.secondaryBg, + }); + + final Color textColor; + final Color iconColor; + final Color searchBg; + final Color searchText; + final Color lastMessageText; + final Color backgroundColor; + final Color secondaryBg; + + static const ChatViewListTheme uiOneDart = ChatViewListTheme( + iconColor: Colors.white, + textColor: Colors.white, + searchBg: Color(0xff26292E), + searchText: Color(0xffA7ADB6), + lastMessageText: Color(0xff74787F), + backgroundColor: Color(0xff0B1014), + secondaryBg: Color(0xff26292E), + ); + + static const ChatViewListTheme uiOneLight = ChatViewListTheme( + iconColor: Colors.black, + textColor: Colors.black, + searchBg: Color(0xffF3F4F7), + searchText: Color(0xff5D636E), + lastMessageText: Colors.black, + backgroundColor: Color(0xffFEFFFE), + secondaryBg: Color(0xffF3F5F7), + ); +} diff --git a/example/lib/models/chatview_theme.dart b/example/lib/models/chatview_theme.dart new file mode 100644 index 00000000..de7edaf4 --- /dev/null +++ b/example/lib/models/chatview_theme.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; + +class ChatViewTheme { + const ChatViewTheme({ + required this.textColor, + required this.titleColor, + required this.iconColor, + required this.backgroundColor, + required this.textField, + required this.incomingBubble, + required this.verticalDivider, + required this.replyBg, + required this.replyText, + }); + + final Color textColor; + final Color titleColor; + final Color iconColor; + final Color backgroundColor; + final Color textField; + final Color incomingBubble; + final Color verticalDivider; + final Color replyBg; + final Color replyText; + + static const ChatViewTheme uiOneDark = ChatViewTheme( + iconColor: Colors.white, + textColor: Colors.white, + titleColor: Color(0xff6B7079), + backgroundColor: Color(0xff0C1014), + textField: Color(0xff1C1E1F), + incomingBubble: Color(0xff26292E), + verticalDivider: Color(0xff191C21), + replyBg: Color(0xff191C21), + replyText: Color(0xff999B9D), + ); + + static const ChatViewTheme uiOneLight = ChatViewTheme( + iconColor: Colors.black, + textColor: Colors.black, + titleColor: Colors.black, + backgroundColor: Colors.white, + textField: Color(0xffF7F7F7), + incomingBubble: Color(0xffF3F5F7), + verticalDivider: Color(0xffF3F5F7), + replyBg: Color(0xffF9FAFB), + replyText: Color(0xff6D7073), + ); +} diff --git a/example/lib/models/theme.dart b/example/lib/models/theme.dart deleted file mode 100644 index 53d9932d..00000000 --- a/example/lib/models/theme.dart +++ /dev/null @@ -1,319 +0,0 @@ -import 'package:flutter/material.dart'; - -class AppTheme { - final Color? appBarColor; - final Color? backArrowColor; - final Color? backgroundColor; - final Color? replyDialogColor; - final Color? replyTitleColor; - final Color? textFieldBackgroundColor; - - final Color? outgoingChatBubbleColor; - - final Color? inComingChatBubbleColor; - - final Color? inComingChatBubbleTextColor; - final Color? repliedMessageColor; - final Color? repliedTitleTextColor; - final Color? textFieldTextColor; - - final Color? closeIconColor; - final Color? shareIconBackgroundColor; - - final Color? sendButtonColor; - final Color? cameraIconColor; - final Color? galleryIconColor; - final Color? recordIconColor; - final Color? stopIconColor; - final Color? swipeToReplyIconColor; - final Color? replyMessageColor; - final Color? appBarTitleTextStyle; - final Color? messageReactionBackGroundColor; - final Color? messageTimeIconColor; - final Color? messageTimeTextColor; - final Color? reactionPopupColor; - final Color? replyPopupColor; - final Color? replyPopupButtonColor; - final Color? replyPopupTopBorderColor; - final Color? reactionPopupTitleColor; - final Color? flashingCircleDarkColor; - final Color? flashingCircleBrightColor; - final Color? waveformBackgroundColor; - final Color? waveColor; - final Color? replyMicIconColor; - final Color? messageReactionBorderColor; - - final Color? verticalBarColor; - final Color? chatHeaderColor; - final Color? themeIconColor; - final Color? shareIconColor; - final double? elevation; - final Color? linkPreviewIncomingChatColor; - final Color? linkPreviewOutgoingChatColor; - final TextStyle? linkPreviewIncomingTitleStyle; - final TextStyle? linkPreviewOutgoingTitleStyle; - final TextStyle? incomingChatLinkTitleStyle; - final TextStyle? outgoingChatLinkTitleStyle; - final TextStyle? outgoingChatLinkBodyStyle; - final TextStyle? incomingChatLinkBodyStyle; - - AppTheme({ - this.cameraIconColor, - this.galleryIconColor, - this.flashingCircleDarkColor, - this.flashingCircleBrightColor, - this.outgoingChatLinkBodyStyle, - this.incomingChatLinkBodyStyle, - this.incomingChatLinkTitleStyle, - this.outgoingChatLinkTitleStyle, - this.linkPreviewOutgoingChatColor, - this.linkPreviewIncomingChatColor, - this.linkPreviewIncomingTitleStyle, - this.linkPreviewOutgoingTitleStyle, - this.repliedTitleTextColor, - this.swipeToReplyIconColor, - this.textFieldTextColor, - this.reactionPopupColor, - this.replyPopupButtonColor, - this.replyPopupTopBorderColor, - this.reactionPopupTitleColor, - this.appBarColor, - this.backArrowColor, - this.backgroundColor, - this.replyDialogColor, - this.replyTitleColor, - this.textFieldBackgroundColor, - this.outgoingChatBubbleColor, - this.inComingChatBubbleColor, - this.inComingChatBubbleTextColor, - this.repliedMessageColor, - this.closeIconColor, - this.shareIconBackgroundColor, - this.sendButtonColor, - this.replyMessageColor, - this.appBarTitleTextStyle, - this.messageReactionBackGroundColor, - this.messageReactionBorderColor, - this.verticalBarColor, - this.chatHeaderColor, - this.themeIconColor, - this.shareIconColor, - this.elevation, - this.messageTimeIconColor, - this.messageTimeTextColor, - this.replyPopupColor, - this.recordIconColor, - this.stopIconColor, - this.waveformBackgroundColor, - this.waveColor, - this.replyMicIconColor, - }); -} - -class DarkTheme extends AppTheme { - DarkTheme({ - Color flashingCircleDarkColor = Colors.grey, - Color flashingCircleBrightColor = const Color(0xffeeeeee), - TextStyle incomingChatLinkTitleStyle = const TextStyle(color: Colors.black), - TextStyle outgoingChatLinkTitleStyle = const TextStyle(color: Colors.white), - TextStyle outgoingChatLinkBodyStyle = const TextStyle(color: Colors.white), - TextStyle incomingChatLinkBodyStyle = const TextStyle(color: Colors.white), - double elevation = 1, - Color repliedTitleTextColor = Colors.white, - Color? swipeToReplyIconColor = Colors.white, - Color textFieldTextColor = Colors.white, - Color appBarColor = const Color(0xff1d1b25), - Color backArrowColor = Colors.white, - Color backgroundColor = const Color(0xff272336), - Color replyDialogColor = const Color(0xff272336), - Color linkPreviewOutgoingChatColor = const Color(0xff272336), - Color linkPreviewIncomingChatColor = const Color(0xff9f85ff), - TextStyle linkPreviewIncomingTitleStyle = const TextStyle(), - TextStyle linkPreviewOutgoingTitleStyle = const TextStyle(), - Color replyTitleColor = Colors.white, - Color textFieldBackgroundColor = const Color(0xff383152), - Color outgoingChatBubbleColor = const Color(0xff9f85ff), - Color inComingChatBubbleColor = const Color(0xff383152), - Color reactionPopupColor = const Color(0xff383152), - Color replyPopupColor = const Color(0xff383152), - Color replyPopupButtonColor = Colors.white, - Color replyPopupTopBorderColor = Colors.black54, - Color reactionPopupTitleColor = Colors.white, - Color inComingChatBubbleTextColor = Colors.white, - Color repliedMessageColor = const Color(0xff9f85ff), - Color closeIconColor = Colors.white, - Color shareIconBackgroundColor = const Color(0xff383152), - Color sendButtonColor = Colors.white, - Color cameraIconColor = const Color(0xff757575), - Color galleryIconColor = const Color(0xff757575), - Color recorderIconColor = const Color(0xff757575), - Color stopIconColor = const Color(0xff757575), - Color replyMessageColor = Colors.grey, - Color appBarTitleTextStyle = Colors.white, - Color messageReactionBackGroundColor = const Color(0xff383152), - Color messageReactionBorderColor = const Color(0xff272336), - Color verticalBarColor = const Color(0xff383152), - Color chatHeaderColor = Colors.white, - Color themeIconColor = Colors.white, - Color shareIconColor = Colors.white, - Color messageTimeIconColor = Colors.white, - Color messageTimeTextColor = Colors.white, - Color waveformBackgroundColor = const Color(0xff383152), - Color waveColor = Colors.white, - Color replyMicIconColor = Colors.white, - }) : super( - closeIconColor: closeIconColor, - verticalBarColor: verticalBarColor, - textFieldBackgroundColor: textFieldBackgroundColor, - replyTitleColor: replyTitleColor, - replyDialogColor: replyDialogColor, - backgroundColor: backgroundColor, - appBarColor: appBarColor, - appBarTitleTextStyle: appBarTitleTextStyle, - backArrowColor: backArrowColor, - chatHeaderColor: chatHeaderColor, - inComingChatBubbleColor: inComingChatBubbleColor, - inComingChatBubbleTextColor: inComingChatBubbleTextColor, - messageReactionBackGroundColor: messageReactionBackGroundColor, - messageReactionBorderColor: messageReactionBorderColor, - outgoingChatBubbleColor: outgoingChatBubbleColor, - repliedMessageColor: repliedMessageColor, - replyMessageColor: replyMessageColor, - sendButtonColor: sendButtonColor, - shareIconBackgroundColor: shareIconBackgroundColor, - themeIconColor: themeIconColor, - shareIconColor: shareIconColor, - elevation: elevation, - messageTimeIconColor: messageTimeIconColor, - messageTimeTextColor: messageTimeTextColor, - textFieldTextColor: textFieldTextColor, - repliedTitleTextColor: repliedTitleTextColor, - swipeToReplyIconColor: swipeToReplyIconColor, - reactionPopupColor: reactionPopupColor, - replyPopupColor: replyPopupColor, - replyPopupButtonColor: replyPopupButtonColor, - replyPopupTopBorderColor: replyPopupTopBorderColor, - reactionPopupTitleColor: reactionPopupTitleColor, - linkPreviewOutgoingChatColor: linkPreviewOutgoingChatColor, - linkPreviewIncomingChatColor: linkPreviewIncomingChatColor, - linkPreviewIncomingTitleStyle: linkPreviewIncomingTitleStyle, - linkPreviewOutgoingTitleStyle: linkPreviewOutgoingTitleStyle, - incomingChatLinkBodyStyle: incomingChatLinkBodyStyle, - incomingChatLinkTitleStyle: incomingChatLinkTitleStyle, - outgoingChatLinkBodyStyle: outgoingChatLinkBodyStyle, - outgoingChatLinkTitleStyle: outgoingChatLinkTitleStyle, - flashingCircleDarkColor: flashingCircleDarkColor, - flashingCircleBrightColor: flashingCircleBrightColor, - galleryIconColor: galleryIconColor, - cameraIconColor: cameraIconColor, - recordIconColor: recorderIconColor, - stopIconColor: stopIconColor, - waveformBackgroundColor: waveformBackgroundColor, - waveColor: waveColor, - replyMicIconColor: replyMicIconColor, - ); -} - -class LightTheme extends AppTheme { - LightTheme({ - Color flashingCircleDarkColor = const Color(0xffEE5366), - Color flashingCircleBrightColor = const Color(0xffFCD8DC), - TextStyle incomingChatLinkTitleStyle = const TextStyle(color: Colors.black), - TextStyle outgoingChatLinkTitleStyle = const TextStyle(color: Colors.black), - TextStyle outgoingChatLinkBodyStyle = const TextStyle(color: Colors.grey), - TextStyle incomingChatLinkBodyStyle = const TextStyle(color: Colors.grey), - Color textFieldTextColor = Colors.black, - Color repliedTitleTextColor = Colors.black, - Color swipeToReplyIconColor = Colors.black, - double elevation = 2, - Color appBarColor = Colors.white, - Color backArrowColor = const Color(0xffEE5366), - Color backgroundColor = const Color(0xffeeeeee), - Color replyDialogColor = const Color(0xffFCD8DC), - Color linkPreviewOutgoingChatColor = const Color(0xffFCD8DC), - Color linkPreviewIncomingChatColor = const Color(0xFFEEEEEE), - TextStyle linkPreviewIncomingTitleStyle = const TextStyle(), - TextStyle linkPreviewOutgoingTitleStyle = const TextStyle(), - Color replyTitleColor = const Color(0xffEE5366), - Color reactionPopupColor = Colors.white, - Color replyPopupColor = Colors.white, - Color replyPopupButtonColor = Colors.black, - Color replyPopupTopBorderColor = const Color(0xFFBDBDBD), - Color reactionPopupTitleColor = Colors.grey, - Color textFieldBackgroundColor = Colors.white, - Color outgoingChatBubbleColor = const Color(0xffEE5366), - Color inComingChatBubbleColor = Colors.white, - Color inComingChatBubbleTextColor = Colors.black, - Color repliedMessageColor = const Color(0xffff8aad), - Color closeIconColor = Colors.black, - Color shareIconBackgroundColor = const Color(0xFFE0E0E0), - Color sendButtonColor = const Color(0xffEE5366), - Color cameraIconColor = Colors.black, - Color galleryIconColor = Colors.black, - Color replyMessageColor = Colors.black, - Color appBarTitleTextStyle = Colors.black, - Color messageReactionBackGroundColor = const Color(0xFFEEEEEE), - Color messageReactionBorderColor = Colors.white, - Color verticalBarColor = const Color(0xffEE5366), - Color chatHeaderColor = Colors.black, - Color themeIconColor = Colors.black, - Color shareIconColor = Colors.black, - Color messageTimeIconColor = Colors.black, - Color messageTimeTextColor = Colors.black, - Color recorderIconColor = Colors.black, - Color stopIconColor = Colors.black, - Color waveformBackgroundColor = Colors.white, - Color waveColor = Colors.black, - Color replyMicIconColor = Colors.black, - }) : super( - reactionPopupColor: reactionPopupColor, - closeIconColor: closeIconColor, - verticalBarColor: verticalBarColor, - textFieldBackgroundColor: textFieldBackgroundColor, - replyTitleColor: replyTitleColor, - replyDialogColor: replyDialogColor, - backgroundColor: backgroundColor, - appBarColor: appBarColor, - appBarTitleTextStyle: appBarTitleTextStyle, - backArrowColor: backArrowColor, - chatHeaderColor: chatHeaderColor, - inComingChatBubbleColor: inComingChatBubbleColor, - inComingChatBubbleTextColor: inComingChatBubbleTextColor, - messageReactionBackGroundColor: messageReactionBackGroundColor, - messageReactionBorderColor: messageReactionBorderColor, - outgoingChatBubbleColor: outgoingChatBubbleColor, - repliedMessageColor: repliedMessageColor, - replyMessageColor: replyMessageColor, - sendButtonColor: sendButtonColor, - shareIconBackgroundColor: shareIconBackgroundColor, - themeIconColor: themeIconColor, - shareIconColor: shareIconColor, - elevation: elevation, - messageTimeIconColor: messageTimeIconColor, - messageTimeTextColor: messageTimeTextColor, - textFieldTextColor: textFieldTextColor, - repliedTitleTextColor: repliedTitleTextColor, - swipeToReplyIconColor: swipeToReplyIconColor, - replyPopupColor: replyPopupColor, - replyPopupButtonColor: replyPopupButtonColor, - replyPopupTopBorderColor: replyPopupTopBorderColor, - reactionPopupTitleColor: reactionPopupTitleColor, - linkPreviewOutgoingChatColor: linkPreviewOutgoingChatColor, - linkPreviewIncomingChatColor: linkPreviewIncomingChatColor, - linkPreviewIncomingTitleStyle: linkPreviewIncomingTitleStyle, - linkPreviewOutgoingTitleStyle: linkPreviewOutgoingTitleStyle, - incomingChatLinkBodyStyle: incomingChatLinkBodyStyle, - incomingChatLinkTitleStyle: incomingChatLinkTitleStyle, - outgoingChatLinkBodyStyle: outgoingChatLinkBodyStyle, - outgoingChatLinkTitleStyle: outgoingChatLinkTitleStyle, - flashingCircleDarkColor: flashingCircleDarkColor, - flashingCircleBrightColor: flashingCircleBrightColor, - galleryIconColor: galleryIconColor, - cameraIconColor: cameraIconColor, - stopIconColor: stopIconColor, - recordIconColor: recorderIconColor, - waveformBackgroundColor: waveformBackgroundColor, - waveColor: waveColor, - replyMicIconColor: replyMicIconColor, - ); -} diff --git a/example/lib/values/colors.dart b/example/lib/values/colors.dart new file mode 100644 index 00000000..fc88ea58 --- /dev/null +++ b/example/lib/values/colors.dart @@ -0,0 +1,16 @@ +import 'dart:ui'; + +class AppColors { + // ChatView Colors + static const Color grey200 = Color(0xffF6F5F4); + static const Color red200 = Color(0xFFFFCDD2); + static const Color black = Color(0xff000000); + static const Color black20 = Color(0x33000000); + static const Color white = Color(0xffffffff); + static const Color white20 = Color(0x33ffffff); + + // Instagram Colors + static const Color instaDarkGrey = Color(0xff767779); + static const Color instaPurple = Color(0xFF574FF0); + static const Color instaUnreadCountDot = Color(0xff5C6BF6); +} diff --git a/example/lib/values/icons.dart b/example/lib/values/icons.dart new file mode 100644 index 00000000..ccf0e5da --- /dev/null +++ b/example/lib/values/icons.dart @@ -0,0 +1,11 @@ +class AppIcons { + static const String camera2 = 'assets/vectors/camera_2.svg'; + static const String chatDiscoveryAi = 'assets/vectors/chat_discovery_ai.svg'; + static const String createPen = 'assets/vectors/create_pen.svg'; + static const String checkMark = "assets/vectors/check_mark.svg"; + static const String pinned = "assets/vectors/pinned.svg"; + static const String video = "assets/vectors/video.svg"; + static const String phone = "assets/vectors/phone.svg"; + static const String sticker = "assets/vectors/sticker.svg"; + static const String ai = "assets/vectors/ai_logo.svg"; +} diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 8a7f3b9b..4c15f737 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -12,8 +12,11 @@ dependencies: flutter: sdk: flutter cupertino_icons: ^1.0.5 + flutter_svg: ^2.2.1 + audio_waveforms: ^1.3.0 chatview: path: ../ + intl: dev_dependencies: flutter_test: @@ -24,3 +27,4 @@ flutter: uses-material-design: true assets: - assets/images/ + - assets/vectors/ diff --git a/lib/src/models/config_models/chat_view_list/list_tile_config.dart b/lib/src/models/config_models/chat_view_list/list_tile_config.dart index 691a7a83..f3f4564e 100644 --- a/lib/src/models/config_models/chat_view_list/list_tile_config.dart +++ b/lib/src/models/config_models/chat_view_list/list_tile_config.dart @@ -52,8 +52,12 @@ class ListTileConfig { this.middleWidgetPadding = const EdgeInsets.symmetric(horizontal: 12), this.padding = const EdgeInsets.symmetric(vertical: 6, horizontal: 8), this.lastMessageStatusConfig = const LastMessageStatusConfig(), + this.lastMessageTextStyle = const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + ), + this.highlightTextStyle = const TextStyle(fontWeight: FontWeight.bold), this.userNameTextStyle, - this.lastMessageTextStyle, this.onTap, this.lastMessageTileBuilder, this.userNameBuilder, @@ -69,6 +73,9 @@ class ListTileConfig { final UserNameBuilder? userNameBuilder; /// Custom widget builder for the trailing widget in the chat list. + /// + /// Defaults it will show last message createAtTime, unread count, + /// mute icon and pin icon. final TrailingBuilder? trailingBuilder; /// Text styles for various text elements in the user widget. @@ -87,6 +94,14 @@ class ListTileConfig { /// Text styles for the last message text in the user widget. final TextStyle? lastMessageTextStyle; + /// Text style for highlighting the last message text, + /// typically used for unread messages. + /// + /// Defaults to a bold font weight. + /// + /// If null, no special highlighting is applied. + final TextStyle? highlightTextStyle; + /// Text overflow behavior for the last message text. /// /// Defaults to `TextOverflow.ellipsis`. diff --git a/lib/src/models/config_models/chat_view_list/search_config.dart b/lib/src/models/config_models/chat_view_list/search_config.dart index cc70c03a..b141ba0c 100644 --- a/lib/src/models/config_models/chat_view_list/search_config.dart +++ b/lib/src/models/config_models/chat_view_list/search_config.dart @@ -40,6 +40,7 @@ class SearchConfig { this.borderRadius = const BorderRadius.all( Radius.circular(textFieldBorderRadius), ), + this.clearIcon = const Icon(Icons.clear), this.onTapOutside, this.textStyle, this.maxLines, @@ -132,6 +133,13 @@ class SearchConfig { /// Decoration for the search text field. final InputDecoration? decoration; + /// Icon to clear the search text field. + /// + /// Defaults to an clear icon `Icons.clear`. + /// + /// **Note**: if suffixIcon is provided, this will be ignored. + final Widget clearIcon; + /// Callback function that is called when the user taps outside the search /// text field. final TapRegionCallback? onTapOutside; diff --git a/lib/src/models/config_models/replied_message_configuration.dart b/lib/src/models/config_models/replied_message_configuration.dart index 8b660fbc..d824384e 100644 --- a/lib/src/models/config_models/replied_message_configuration.dart +++ b/lib/src/models/config_models/replied_message_configuration.dart @@ -44,6 +44,8 @@ class RepliedMessageConfiguration { this.micIconColor, }); + /// Used to fetch old replied message when original message is not present + /// in the current list of messages. final OldReplyMessageFetchCallback loadOldReplyMessage; /// Used to give color to vertical bar. diff --git a/lib/src/widgets/chat_view_list/chat_view_list_item_tile.dart b/lib/src/widgets/chat_view_list/chat_view_list_item_tile.dart index 214f8e52..38a9a748 100644 --- a/lib/src/widgets/chat_view_list/chat_view_list_item_tile.dart +++ b/lib/src/widgets/chat_view_list/chat_view_list_item_tile.dart @@ -146,6 +146,7 @@ class ChatViewListItemTile extends StatelessWidget { config.lastMessageTextOverflow, lastMessageTextStyle: config.lastMessageTextStyle, + highlightTextStyle: config.highlightTextStyle, lastMessageBuilder: config.lastMessageTileBuilder?.call(chat), statusConfig: config.lastMessageStatusConfig, diff --git a/lib/src/widgets/chat_view_list/last_message_view.dart b/lib/src/widgets/chat_view_list/last_message_view.dart index 0fc57c94..88ce648f 100644 --- a/lib/src/widgets/chat_view_list/last_message_view.dart +++ b/lib/src/widgets/chat_view_list/last_message_view.dart @@ -11,6 +11,7 @@ class LastMessageView extends StatelessWidget { required this.lastMessage, required this.showStatusIcon, required this.statusConfig, + this.highlightTextStyle, this.lastMessageType, this.lastMessageBuilder, this.lastMessageMaxLines, @@ -36,11 +37,12 @@ class LastMessageView extends StatelessWidget { final MessageType? lastMessageType; final Widget? lastMessageBuilder; final bool showStatusIcon; + final TextStyle? highlightTextStyle; final LastMessageStatusConfig statusConfig; @override Widget build(BuildContext context) { - final highlightText = unreadCount > 0; + final highlightText = highlightTextStyle != null && unreadCount > 0; return lastMessageBuilder ?? AnimatedSwitcher( switchOutCurve: Curves.easeOut, @@ -71,10 +73,8 @@ class LastMessageView extends StatelessWidget { PackageStrings.currentLocale.photo, maxLines: lastMessageMaxLines, overflow: lastMessageTextOverflow, - style: TextStyle( - fontWeight: highlightText - ? FontWeight.bold - : FontWeight.normal, + style: lastMessageTextStyle?.merge( + highlightText ? highlightTextStyle : null, ), ), ), @@ -85,13 +85,9 @@ class LastMessageView extends StatelessWidget { textAlign: TextAlign.left, maxLines: lastMessageMaxLines, overflow: lastMessageTextOverflow, - style: lastMessageTextStyle ?? - TextStyle( - fontSize: 14, - fontWeight: highlightText - ? FontWeight.bold - : FontWeight.normal, - ), + style: lastMessageTextStyle?.merge( + highlightText ? highlightTextStyle : null, + ), ), MessageType.voice => Row( mainAxisSize: MainAxisSize.min, @@ -103,6 +99,9 @@ class LastMessageView extends StatelessWidget { PackageStrings.currentLocale.voice, maxLines: lastMessageMaxLines, overflow: lastMessageTextOverflow, + style: lastMessageTextStyle?.merge( + highlightText ? highlightTextStyle : null, + ), ), ), ], diff --git a/lib/src/widgets/chat_view_list/search_text_field.dart b/lib/src/widgets/chat_view_list/search_text_field.dart index 719410b6..c9ddf60e 100644 --- a/lib/src/widgets/chat_view_list/search_text_field.dart +++ b/lib/src/widgets/chat_view_list/search_text_field.dart @@ -93,7 +93,7 @@ class _SearchTextFieldState extends State { builder: (context, value, _) => value.isEmpty ? const SizedBox.shrink() : IconButton( - icon: const Icon(Icons.clear), + icon: _config.clearIcon, onPressed: _onTapClear, ), ), diff --git a/lib/src/widgets/chatui_textfield.dart b/lib/src/widgets/chatui_textfield.dart index 8eeef49c..7778663c 100644 --- a/lib/src/widgets/chatui_textfield.dart +++ b/lib/src/widgets/chatui_textfield.dart @@ -108,6 +108,9 @@ class _ChatUITextFieldState extends State { @override void initState() { attachListeners(); + // Used to notify instead of onChanged of text field because + // onChanged is not called when text is set programmatically. + widget.textEditingController.addListener(_listenTextEditingController); debouncer = Debouncer( sendMessageConfig?.textFieldConfig?.compositionThresholdTime ?? const Duration(seconds: 1)); @@ -133,6 +136,7 @@ class _ChatUITextFieldState extends State { if (_keyboardHandler case final handler?) { HardwareKeyboard.instance.removeHandler(handler); } + widget.textEditingController.removeListener(_listenTextEditingController); super.dispose(); } @@ -259,7 +263,6 @@ class _ChatUITextFieldState extends State { minLines: textFieldConfig?.minLines ?? 1, keyboardType: textFieldConfig?.textInputType, inputFormatters: textFieldConfig?.inputFormatters, - onChanged: _onChanged, enabled: textFieldConfig?.enabled, textCapitalization: textFieldConfig?.textCapitalization ?? @@ -439,6 +442,12 @@ class _ChatUITextFieldState extends State { } } + void _listenTextEditingController() { + widget.textEditingController.addListener(() { + _onChanged(widget.textEditingController.text); + }); + } + void _onChanged(String inputText) { debouncer.run(onComplete: () { composingStatus.value = TypeWriterStatus.typed; diff --git a/lib/src/widgets/type_indicator/type_indicator_widget.dart b/lib/src/widgets/type_indicator/type_indicator_widget.dart index e3816d30..813c99e2 100644 --- a/lib/src/widgets/type_indicator/type_indicator_widget.dart +++ b/lib/src/widgets/type_indicator/type_indicator_widget.dart @@ -197,6 +197,7 @@ class _TypingIndicatorState extends State child: Padding( padding: typeIndicatorPadding, child: UserTypingBuilder( + showProfileCircle: false, animation: _largeBubbleAnimation, profileConfig: profileCircleConfiguration, bubble: widget.typeIndicatorConfig.customIndicator!, diff --git a/lib/src/widgets/type_indicator/user_typing_builder.dart b/lib/src/widgets/type_indicator/user_typing_builder.dart index 56d6ed28..7570dbe5 100644 --- a/lib/src/widgets/type_indicator/user_typing_builder.dart +++ b/lib/src/widgets/type_indicator/user_typing_builder.dart @@ -8,6 +8,7 @@ class UserTypingBuilder extends StatelessWidget { const UserTypingBuilder({ required this.animation, required this.bubble, + this.showProfileCircle = true, this.profileConfig, super.key, }); @@ -15,6 +16,7 @@ class UserTypingBuilder extends StatelessWidget { final Widget bubble; final Animation animation; final ProfileCircleConfiguration? profileConfig; + final bool showProfileCircle; @override Widget build(BuildContext context) { @@ -25,24 +27,27 @@ class UserTypingBuilder extends StatelessWidget { alignment: Alignment.centerLeft, child: child, ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - ProfileCircle( - bottomPadding: 0, - profileCirclePadding: EdgeInsets.zero, - imageUrl: profileConfig?.profileImageUrl, - imageType: profileConfig?.imageType, - assetImageErrorBuilder: profileConfig?.assetImageErrorBuilder, - networkImageErrorBuilder: profileConfig?.networkImageErrorBuilder, - defaultAvatarImage: - profileConfig?.defaultAvatarImage ?? Constants.profileImage, - networkImageProgressIndicatorBuilder: - profileConfig?.networkImageProgressIndicatorBuilder, - ), - bubble, - ], - ), + child: !showProfileCircle + ? bubble + : Row( + mainAxisSize: MainAxisSize.min, + children: [ + ProfileCircle( + bottomPadding: 0, + profileCirclePadding: EdgeInsets.zero, + imageUrl: profileConfig?.profileImageUrl, + imageType: profileConfig?.imageType, + assetImageErrorBuilder: profileConfig?.assetImageErrorBuilder, + networkImageErrorBuilder: + profileConfig?.networkImageErrorBuilder, + defaultAvatarImage: profileConfig?.defaultAvatarImage ?? + Constants.profileImage, + networkImageProgressIndicatorBuilder: + profileConfig?.networkImageProgressIndicatorBuilder, + ), + bubble, + ], + ), ); } } diff --git a/preview/chatviewlist.gif b/preview/chatviewlist.gif new file mode 100644 index 00000000..7d1b3fa5 Binary files /dev/null and b/preview/chatviewlist.gif differ diff --git a/pubspec.yaml b/pubspec.yaml index 523c36a2..631d2af7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,7 +23,7 @@ dependencies: chatview_utils: git: url: https://github.com/SimformSolutionsPvtLtd/chatview_utils - ref: feature/add_chatview_list_models_test + ref: feature/update_changelog emoji_picker_flutter: ^4.3.0 flutter: