diff --git a/example/assets/images/chat_background.png b/example/assets/images/chat_background.png new file mode 100644 index 00000000..cb17a0ff Binary files /dev/null and b/example/assets/images/chat_background.png differ diff --git a/example/assets/vectors/add.svg b/example/assets/vectors/add.svg new file mode 100644 index 00000000..960a62d3 --- /dev/null +++ b/example/assets/vectors/add.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/example/assets/vectors/archived.svg b/example/assets/vectors/archived.svg new file mode 100644 index 00000000..a1b177c0 --- /dev/null +++ b/example/assets/vectors/archived.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/example/assets/vectors/calls.svg b/example/assets/vectors/calls.svg new file mode 100644 index 00000000..2b9048ea --- /dev/null +++ b/example/assets/vectors/calls.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/example/assets/vectors/camera.svg b/example/assets/vectors/camera.svg new file mode 100644 index 00000000..2e881fa0 --- /dev/null +++ b/example/assets/vectors/camera.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/example/assets/vectors/camera_outline.svg b/example/assets/vectors/camera_outline.svg new file mode 100644 index 00000000..93c343e3 --- /dev/null +++ b/example/assets/vectors/camera_outline.svg @@ -0,0 +1,3 @@ + + + diff --git a/example/assets/vectors/close_circular.svg b/example/assets/vectors/close_circular.svg new file mode 100644 index 00000000..9702189d --- /dev/null +++ b/example/assets/vectors/close_circular.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/example/assets/vectors/community.svg b/example/assets/vectors/community.svg new file mode 100644 index 00000000..e3c26dad --- /dev/null +++ b/example/assets/vectors/community.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/example/assets/vectors/lock.svg b/example/assets/vectors/lock.svg new file mode 100644 index 00000000..0b98e414 --- /dev/null +++ b/example/assets/vectors/lock.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/example/assets/vectors/messages.svg b/example/assets/vectors/messages.svg new file mode 100644 index 00000000..b45781c6 --- /dev/null +++ b/example/assets/vectors/messages.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/example/assets/vectors/mic.svg b/example/assets/vectors/mic.svg new file mode 100644 index 00000000..7eb2ba36 --- /dev/null +++ b/example/assets/vectors/mic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/example/assets/vectors/more_horizontal.svg b/example/assets/vectors/more_horizontal.svg new file mode 100644 index 00000000..a9b0dc9b --- /dev/null +++ b/example/assets/vectors/more_horizontal.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/example/assets/vectors/plus.svg b/example/assets/vectors/plus.svg new file mode 100644 index 00000000..92fb8c39 --- /dev/null +++ b/example/assets/vectors/plus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/example/assets/vectors/send.svg b/example/assets/vectors/send.svg new file mode 100644 index 00000000..36adbf96 --- /dev/null +++ b/example/assets/vectors/send.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/example/assets/vectors/settings.svg b/example/assets/vectors/settings.svg new file mode 100644 index 00000000..b7b1a5e6 --- /dev/null +++ b/example/assets/vectors/settings.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/example/assets/vectors/status.svg b/example/assets/vectors/status.svg new file mode 100644 index 00000000..88916074 --- /dev/null +++ b/example/assets/vectors/status.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/example/lib/data.dart b/example/lib/data.dart index 93dc04c6..9d3697a1 100644 --- a/example/lib/data.dart +++ b/example/lib/data.dart @@ -317,21 +317,18 @@ class Data { 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, + id: '19', + message: 'https://bit.ly/3JHS2Wl', + createdAt: DateTime.now().subtract(const Duration(minutes: 3)), + sentBy: '2', status: MessageStatus.read, + messageType: MessageType.text, ), 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)), + id: '20', + message: "Check this out! We can meet here.", + createdAt: DateTime.now().subtract(const Duration(minutes: 1)), sentBy: '2', - messageType: MessageType.voice, status: MessageStatus.read, ), ]; diff --git a/example/lib/example_two/example_two_chat_screen.dart b/example/lib/example_two/example_two_chat_screen.dart new file mode 100644 index 00000000..621add52 --- /dev/null +++ b/example/lib/example_two/example_two_chat_screen.dart @@ -0,0 +1,485 @@ +import 'package:chatview/chatview.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:intl/intl.dart'; + +import '../data.dart'; +import '../values/colors.dart'; +import '../values/borders.dart'; +import '../values/icons.dart'; +import '../values/images.dart'; +import '../widgets/reply_message_tile.dart'; +import '../widgets/wp_custom_chat_bar.dart'; + +class ExampleTwoChatScreen extends StatefulWidget { + const ExampleTwoChatScreen({required this.chat, super.key}); + + final ChatViewListItem chat; + + @override + State createState() => _ExampleTwoChatScreenState(); +} + +class _ExampleTwoChatScreenState extends State { + bool _isTopPaginationCalled = false; + bool _isBottomPaginationCalled = false; + + final ChatController _chatController = ChatController( + initialMessageList: Data.getMessageList(isExampleOne: false), + scrollController: ScrollController(), + currentUser: Data.currentUser, + otherUsers: Data.otherUsers, + ); + + @override + void initState() { + super.initState(); + + // Set system UI overlay style + WidgetsBinding.instance.addPostFrameCallback((_) { + SystemChrome.setSystemUIOverlayStyle( + const SystemUiOverlayStyle( + statusBarColor: Color(0xFFE5DDD5), + statusBarBrightness: Brightness.dark, + ), + ); + }); + } + + @override + void dispose() { + _chatController.dispose(); + super.dispose(); + } + + void _showHideTypingIndicator() { + _chatController.setTypingIndicator = !_chatController.showTypingIndicator; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.uiTwoBackground, + body: ChatView( + chatController: _chatController, + chatViewState: ChatViewState.hasMessages, + + 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, + ); + }, + appBar: ChatViewAppBar( + elevation: 0, + chatTitle: widget.chat.name, + chatTitleTextStyle: const TextStyle( + fontSize: 18, + color: Colors.black, + fontWeight: FontWeight.w600, + ), + profilePicture: widget.chat.imageUrl, + backGroundColor: AppColors.uiTwoBackground, + userStatus: 'tap here for contact info', + userStatusTextStyle: const TextStyle( + fontSize: 13, + color: Colors.black87, + ), + actions: [ + IconButton( + // Handle video call + onPressed: () => showSnackBar('Video call pressed'), + icon: SvgPicture.asset(AppIcons.video), + ), + IconButton( + // Handle voice call + onPressed: () => showSnackBar('Voice call pressed'), + icon: SvgPicture.asset(AppIcons.phone), + ), + PopupMenuButton( + icon: const Icon(Icons.more_vert_rounded), + itemBuilder: (context) => [ + const PopupMenuItem( + value: 'toggle_typing_indicator', + child: Text('Toggle TypingIndicator'), + ), + ], + onSelected: (value) { + switch (value) { + case 'toggle_typing_indicator': + _showHideTypingIndicator(); + } + }, + ), + const SizedBox(width: 12), + ], + ), + typeIndicatorConfig: TypeIndicatorConfiguration( + customIndicator: _customTypingIndicator(), + ), + profileCircleConfig: const ProfileCircleConfiguration( + padding: EdgeInsets.only(right: 4), + ), + chatBackgroundConfig: ChatBackgroundConfiguration( + backgroundColor: AppColors.uiTwoBackground, + backgroundImage: AppImages.wpChatBackground, + groupSeparatorBuilder: (separator) => _customSeparator(separator), + ), + chatBubbleConfig: ChatBubbleConfiguration( + outgoingChatBubbleConfig: const ChatBubble( + linkPreviewConfig: LinkPreviewConfiguration( + linkStyle: TextStyle(color: Colors.black87, fontSize: 16), + ), + border: AppBorders.exampleTwoMessageBorder, + color: Color(0xFFD0FECF), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + bottomLeft: Radius.circular(12), + bottomRight: Radius.circular(4), + ), + textStyle: TextStyle(color: Colors.black87, fontSize: 16), + padding: EdgeInsets.all(5.5), + receiptsWidgetConfig: ReceiptsWidgetConfig( + showReceiptsIn: ShowReceiptsIn.lastMessage, + ), + ), + inComingChatBubbleConfig: ChatBubble( + linkPreviewConfig: const LinkPreviewConfiguration( + linkStyle: TextStyle(color: Colors.black87, fontSize: 16), + ), + border: AppBorders.exampleTwoMessageBorder, + color: Colors.white, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + bottomLeft: Radius.circular(4), + bottomRight: Radius.circular(12), + ), + textStyle: const TextStyle(color: Colors.black87, fontSize: 16), + padding: const EdgeInsets.all(5.5), + onMessageRead: (message) { + /// send your message reciepts to the other client + debugPrint('Message Read'); + }, + ), + ), + messageConfig: MessageConfiguration( + voiceMessageConfig: VoiceMessageConfiguration( + margin: EdgeInsets.zero, + padding: EdgeInsets.zero, + playIcon: (_) => const Icon( + Icons.play_arrow_rounded, + size: 38, + color: Color(0xff767779), + ), + pauseIcon: (_) => const Icon( + Icons.pause_rounded, + size: 38, + color: Color(0xff767779), + ), + inComingPlayerWaveStyle: const PlayerWaveStyle( + liveWaveColor: Color(0xff000000), + fixedWaveColor: Color(0x33000000), + backgroundColor: Colors.transparent, + scaleFactor: 60, + waveThickness: 3, + spacing: 4, + ), + outgoingPlayerWaveStyle: const PlayerWaveStyle( + liveWaveColor: Color(0xff000000), + fixedWaveColor: Color(0x33000000), + backgroundColor: Colors.transparent, + scaleFactor: 60, + waveThickness: 3, + spacing: 4, + ), + ), + messageReactionConfig: MessageReactionConfiguration( + backgroundColor: Colors.white, + borderColor: Colors.grey.shade300, + reactionsBottomSheetConfig: const ReactionsBottomSheetConfiguration( + backgroundColor: Colors.white, + ), + ), + // Custom message builder for location messages + customMessageBuilder: (message) { + // For demo, we check if the message contains 'Twin Pines Mall' + if (message.message.contains('Twin Pines Mall')) { + return _buildLocationMessage(message); + } + return const SizedBox.shrink(); + }, + ), + + // Reply message configuration + repliedMessageConfig: RepliedMessageConfiguration( + margin: const EdgeInsets.symmetric(horizontal: 12), + backgroundColor: Colors.grey.shade100, + verticalBarColor: const Color(0xFF128C7E), + loadOldReplyMessage: (messageId) async {}, + repliedMessageWidgetBuilder: (replyMessage) => ReplyMessageTile( + replyMessage: replyMessage, + chatController: _chatController, + ), + ), + + // Swipe to reply configuration + swipeToReplyConfig: const SwipeToReplyConfiguration( + onLeftSwipe: null, + onRightSwipe: null, + ), + sendMessageBuilder: (replyMessage) => WpCustomChatBar( + chatController: _chatController, + replyMessage: replyMessage ?? const ReplyMessage(), + onAttachPressed: () => ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Attach button pressed')), + ), + ), + featureActiveConfig: const FeatureActiveConfig( + lastSeenAgoBuilderVisibility: false, + enableOtherUserProfileAvatar: false, + enableOtherUserName: false, + enablePagination: true, + ), + reactionPopupConfig: const ReactionPopupConfiguration( + backgroundColor: Colors.white, + shadow: BoxShadow( + blurRadius: 8, + color: Colors.black26, + offset: Offset(0, 4), + ), + ), + ), + ); + } + + Widget _customTypingIndicator() { + return Container( + margin: const EdgeInsets.only(left: 6), + decoration: const BoxDecoration( + color: Colors.white, + border: AppBorders.exampleTwoMessageBorder, + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + padding: const EdgeInsets.symmetric( + vertical: 12.75, + horizontal: 8.75, + ), + child: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + CircleAvatar(radius: 2.75, backgroundColor: AppColors.uiTwoGrey), + SizedBox(width: 3), + CircleAvatar(radius: 2.75, backgroundColor: AppColors.uiTwoGrey), + SizedBox(width: 3), + CircleAvatar(radius: 2.75, backgroundColor: AppColors.uiTwoGrey), + ], + ), + ); + } + + Widget _customSeparator(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 = 'Today'; + } else if (date.day == now.day - 1 && + date.month == now.month && + date.year == now.year) { + separatorDate = 'Yesterday'; + } else { + separatorDate = DateFormat('d MMMM y').format(date); + } + return Align( + child: Container( + margin: const EdgeInsets.symmetric(vertical: 12), + padding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 3, + ), + decoration: const BoxDecoration( + color: Colors.white, + border: AppBorders.exampleTwoMessageBorder, + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + child: Text( + separatorDate, + style: const TextStyle( + fontSize: 12, + color: Color(0xff0A0A0A), + fontWeight: FontWeight.w500, + ), + ), + ), + ); + } + + Widget _buildLocationMessage(Message message) { + return Container( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * 0.75, + ), + padding: const EdgeInsets.all(12), + margin: EdgeInsets.fromLTRB( + message.sentBy == _chatController.currentUser.id ? 64 : 8, + 4, + message.sentBy == _chatController.currentUser.id ? 8 : 64, + 4, + ), + decoration: BoxDecoration( + color: message.sentBy == _chatController.currentUser.id + ? const Color(0xFFD0FECF) + : Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.location_on, color: Colors.red.shade600, size: 20), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Location', + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14, + color: Colors.grey.shade700, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + Container( + height: 120, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: BorderRadius.circular(8), + ), + child: const Center( + child: Icon(Icons.map, size: 40, color: Colors.grey), + ), + ), + const SizedBox(height: 8), + Text( + message.message, + style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 16), + ), + Text( + 'Hill Valley, CA', + style: TextStyle(color: Colors.grey.shade600, fontSize: 14), + ), + const SizedBox(height: 4), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + _formatTime(message.createdAt), + style: TextStyle(color: Colors.grey.shade500, fontSize: 12), + ), + if (message.sentBy == _chatController.currentUser.id) ...[ + const SizedBox(width: 4), + Icon( + _getMessageStatusIcon(message.status), + size: 16, + color: _getMessageStatusColor(message.status), + ), + ], + ], + ), + ], + ), + ); + } + + void showSnackBar(String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(message)), + ); + } + + String _formatTime(DateTime dateTime) { + return '${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}'; + } + + IconData _getMessageStatusIcon(MessageStatus status) { + return switch (status) { + MessageStatus.delivered => Icons.done_all, + MessageStatus.read => Icons.done_all, + MessageStatus.pending => Icons.access_time, + MessageStatus.undelivered => Icons.error_outline, + }; + } + + Color _getMessageStatusColor(MessageStatus status) { + return switch (status) { + MessageStatus.read => const Color(0xFF4FC3F7), + MessageStatus.delivered => Colors.grey.shade600, + MessageStatus.pending => Colors.grey.shade500, + MessageStatus.undelivered => Colors.red.shade600, + }; + } +} diff --git a/example/lib/example_two/example_two_list_screen.dart b/example/lib/example_two/example_two_list_screen.dart new file mode 100644 index 00000000..56be51ae --- /dev/null +++ b/example/lib/example_two/example_two_list_screen.dart @@ -0,0 +1,616 @@ +import 'package:chatview/chatview.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +import '../data.dart'; +import '../main.dart'; +import '../models/chatview_list_theme.dart'; +import '../values/colors.dart'; +import '../values/icons.dart'; +import 'example_two_chat_screen.dart'; + +class ExampleTwoListScreen extends StatefulWidget { + const ExampleTwoListScreen({super.key}); + + @override + State createState() => _ExampleTwoListScreenState(); +} + +class _ExampleTwoListScreenState extends State { + ChatViewListTheme _theme = ChatViewListTheme.uiTwoLight; + bool _isDarkTheme = false; + + final _searchController = TextEditingController(); + + ChatViewListController? _chatListController; + + ScrollController? _scrollController; + + String _selectedFilter = 'All'; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + SystemChrome.setSystemUIOverlayStyle( + const SystemUiOverlayStyle(statusBarColor: AppColors.uiTwoBackground), + ); + }); + } + + // Assign the controller in didChangeDependencies + // to ensure PrimaryScrollController is available. + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _scrollController = PrimaryScrollController.of(context); + _chatListController ??= ChatViewListController( + initialChatList: Data.getChatList(), + scrollController: _scrollController!, + disposeOtherResources: false, + ); + } + + @override + Widget build(BuildContext context) { + return Theme( + data: Theme.of(context).copyWith( + primaryColor: AppColors.uiTwoGreen, + colorScheme: ColorScheme.fromSwatch(accentColor: AppColors.uiTwoGreen), + ), + child: Builder( + builder: (context) => Scaffold( + backgroundColor: _theme.backgroundColor, + body: _chatListController == null + ? const Center(child: CircularProgressIndicator()) + : ChatViewList( + backgroundColor: _theme.backgroundColor, + controller: _chatListController!, + header: _buildHeader(), + footer: _buildFooter(), + appbar: CupertinoSliverNavigationBar( + largeTitle: Text( + 'Chats', + style: TextStyle(color: _theme.textColor), + ), + border: const Border.fromBorderSide(BorderSide.none), + backgroundColor: _theme.backgroundColor, + leading: PopupMenuButton( + style: const ButtonStyle( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + icon: CircleAvatar( + radius: 14, + backgroundColor: _theme.iconButton, + child: SvgPicture.asset( + AppIcons.moreHoriz, + width: 24, + height: 24, + colorFilter: ColorFilter.mode( + _theme.iconColor, + BlendMode.srcIn, + ), + ), + ), + itemBuilder: (context) => [ + PopupMenuItem( + value: 'dark_theme', + child: + Text(' ${_isDarkTheme ? 'Light' : 'Dark'} Mode'), + ), + ], + onSelected: (value) { + switch (value) { + case 'dark_theme': + _onThemeIconTap(); + } + }, + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + GestureDetector( + onTap: () => showSnackBar('Camera Button Tapped'), + child: CircleAvatar( + radius: 14, + backgroundColor: _theme.iconButton, + child: SvgPicture.asset( + AppIcons.camera, + width: 24, + height: 24, + colorFilter: ColorFilter.mode( + _theme.iconColor, + BlendMode.srcIn, + ), + ), + ), + ), + const SizedBox(width: 16), + GestureDetector( + onTap: () => showSnackBar('Add Button Tapped'), + child: CircleAvatar( + radius: 14, + backgroundColor: AppColors.uiTwoGreen, + child: SvgPicture.asset( + AppIcons.add, + width: 24, + height: 24, + colorFilter: ColorFilter.mode( + _theme.backgroundColor, + BlendMode.srcIn, + ), + ), + ), + ), + ], + ), + ), + separatorBuilder: (_, __) => Divider( + height: 1, + indent: 80, + endIndent: 0, + color: _theme.divider, + ), + 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( + lastMessageIconColor: _theme.iconColor, + onTap: (value) => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => ExampleTwoChatScreen(chat: value), + ), + ), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 10, + ), + showUserActiveStatusIndicator: false, + userAvatarConfig: const UserAvatarConfig( + radius: 28, + backgroundColor: Color(0xFFD0FECF), + ), + lastMessageStatusConfig: LastMessageStatusConfig( + statusBuilder: (status) => switch (status) { + MessageStatus.read => SvgPicture.asset( + AppIcons.checkMark, + width: 19, + height: 19, + ), + MessageStatus.delivered => SvgPicture.asset( + AppIcons.checkMark, + width: 19, + height: 19, + colorFilter: const ColorFilter.mode( + AppColors.uiTwoGrey, + BlendMode.srcIn, + ), + ), + MessageStatus.pending => const Icon( + Icons.schedule, + size: 19, + color: AppColors.uiTwoGrey, + ), + MessageStatus.undelivered => const Icon( + Icons.error_rounded, + size: 19, + color: Colors.red, + ), + }, + ), + pinIconConfig: PinIconConfig( + widget: SvgPicture.asset(AppIcons.pinned), + ), + typingStatusConfig: const TypingStatusConfig( + textStyle: TextStyle( + fontStyle: FontStyle.italic, + color: Color(0xff767779), + fontSize: 14, + ), + ), + timeConfig: const LastMessageTimeConfig( + textStyle: TextStyle( + color: Color(0xff767779), + fontSize: 14, + ), + ), + unreadCountConfig: UnreadCountConfig( + backgroundColor: AppColors.uiTwoGreen, + style: UnreadCountStyle.ninetyNinePlus, + textStyle: TextStyle(color: _theme.backgroundColor), + ), + userNameTextStyle: TextStyle( + fontSize: 16, + color: _theme.textColor, + fontWeight: FontWeight.w600, + ), + lastMessageTextStyle: const TextStyle( + color: Color(0xff767779), + fontSize: 14, + ), + ), + searchConfig: SearchConfig( + textEditingController: _searchController, + hintText: 'Ask Meta AI or Search', + hintStyle: TextStyle( + fontSize: 16.4, + color: _theme.searchText, + fontWeight: FontWeight.w400, + ), + textFieldBackgroundColor: _theme.searchBg, + prefixIcon: Icon( + Icons.search, + color: _theme.searchText, + size: 24, + ), + borderRadius: const BorderRadius.all(Radius.circular(10)), + contentPadding: const EdgeInsets.symmetric( + vertical: 0, + horizontal: 16, + ), + padding: const EdgeInsets.symmetric(horizontal: 16), + textStyle: TextStyle(color: _theme.textColor), + clearIcon: Icon( + Icons.clear, + color: _theme.iconColor, + size: 24, + ), + onSearch: (value) async { + if (value.isEmpty) { + return null; + } + + List chats = + _chatListController?.chatListMap.values.toList() ?? + []; + + final list = chats + .where((chat) => chat.name + .toLowerCase() + .contains(value.toLowerCase())) + .toList(); + return list; + }, + ), + ), + bottomNavigationBar: _buildBottomNavigationBar(), + floatingActionButton: GestureDetector( + onTap: () => showSnackBar('Meta AI Button Tapped'), + child: Container( + width: 46, + height: 46, + padding: const EdgeInsetsGeometry.all(8), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: _theme.floatingButton, + boxShadow: const [ + BoxShadow( + color: Colors.black12, + blurRadius: 10, + spreadRadius: 10, + offset: Offset(0, 6), + ), + ], + ), + child: SvgPicture.asset(AppIcons.ai), + ), + ), + ), + ), + ); + } + + void _onThemeIconTap() { + setState(() { + if (_isDarkTheme) { + _theme = ChatViewListTheme.uiTwoLight; + _isDarkTheme = false; + } else { + _theme = ChatViewListTheme.uiTwoDark; + _isDarkTheme = true; + } + }); + } + + Widget _buildFooter() { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 19), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset(AppIcons.lock), + const SizedBox(width: 3), + const Text.rich( + TextSpan( + text: 'Your personal messages are ', + children: [ + TextSpan( + text: 'end-to-end encrypted', + style: TextStyle(color: AppColors.uiTwoGreen), + ), + ], + style: TextStyle( + fontSize: 11, + color: AppColors.uiTwoGrey, + fontWeight: FontWeight.normal, + ), + ), + ), + ], + ), + ); + } + + Widget _buildHeader() { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 16), + GestureDetector( + onTap: () => Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => const ExampleOneListScreen(), + ), + ), + child: Text( + '✨ Check out another UI', + style: TextStyle( + color: _theme.textColor, + shadows: [ + Shadow( + color: _isDarkTheme ? Colors.white54 : Colors.black54, + offset: const Offset(0, -1), + blurRadius: 1, + ), + ], + decorationColor: _theme.textColor, + decoration: TextDecoration.underline, + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0, bottom: 8), + child: SizedBox( + height: 34, + child: ListView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 16), + children: [ + _buildFilterChip('All'), + const SizedBox(width: 8), + _buildFilterChip('Unread'), + const SizedBox(width: 8), + _buildFilterChip('Favourites'), + const SizedBox(width: 8), + _buildFilterChip('Groups'), + const SizedBox(width: 8), + _buildAddFilterChip(), + ], + ), + ), + ), + _archiveWidget(), + Divider( + height: 1, + indent: 80, + endIndent: 0, + color: _theme.divider, + ), + ], + ); + } + + Widget _buildFilterChip(String label) { + final isSelected = _selectedFilter == label; + return ChoiceChip( + label: Text(label), + selected: isSelected, + showCheckmark: false, + onSelected: (selected) { + if (selected) { + _onFilterSelected(label); + } + }, + backgroundColor: _theme.chipBg, + selectedColor: _theme.selectedChipBg, + labelStyle: TextStyle( + color: isSelected ? _theme.selectedChip : _theme.chipText, + fontWeight: FontWeight.w600, + fontSize: 14, + ), + shape: const StadiumBorder(), + side: BorderSide.none, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + visualDensity: VisualDensity.compact, + ); + } + + Widget _buildAddFilterChip() { + return InkWell( + onTap: () => showSnackBar('Add Filter Tapped'), + borderRadius: const BorderRadius.all(Radius.circular(19)), + child: Container( + width: 34, + height: 34, + decoration: BoxDecoration( + color: _theme.chipBg, + borderRadius: const BorderRadius.all(Radius.circular(19)), + ), + child: Icon( + Icons.add, + size: 20, + color: _theme.chipText, + ), + ), + ); + } + + Widget _buildBottomNavigationBar() { + return BottomNavigationBar( + backgroundColor: _theme.backgroundColor, + items: [ + BottomNavigationBarItem( + icon: SvgPicture.asset( + AppIcons.status, + colorFilter: const ColorFilter.mode( + AppColors.uiTwoGrey, + BlendMode.srcIn, + ), + ), + label: 'Updates', + ), + BottomNavigationBarItem( + icon: SvgPicture.asset( + AppIcons.calls, + colorFilter: + const ColorFilter.mode(AppColors.uiTwoGrey, BlendMode.srcIn), + ), + label: 'Calls', + ), + BottomNavigationBarItem( + icon: SvgPicture.asset( + AppIcons.community, + colorFilter: + const ColorFilter.mode(AppColors.uiTwoGrey, BlendMode.srcIn), + ), + label: 'Communities', + ), + BottomNavigationBarItem( + icon: SvgPicture.asset( + AppIcons.messages, + colorFilter: + const ColorFilter.mode(AppColors.uiTwoGrey, BlendMode.srcIn), + ), + activeIcon: Badge( + label: const Text('1'), + offset: const Offset(10, 0), + backgroundColor: AppColors.uiTwoGreen, + child: SvgPicture.asset( + AppIcons.messages, + colorFilter: ColorFilter.mode(_theme.iconColor, BlendMode.srcIn), + ), + ), + label: 'Chats', + ), + BottomNavigationBarItem( + icon: SvgPicture.asset( + AppIcons.settings, + colorFilter: + const ColorFilter.mode(AppColors.uiTwoGrey, BlendMode.srcIn), + ), + label: 'Settings', + ), + ], + currentIndex: 3, + selectedItemColor: _theme.textColor, + unselectedItemColor: AppColors.uiTwoGrey, + showUnselectedLabels: true, + onTap: (index) {}, + type: BottomNavigationBarType.fixed, + ); + } + + Widget _archiveWidget() { + return Padding( + padding: const EdgeInsets.only(top: 10, left: 32, bottom: 10, right: 32), + child: Row( + children: [ + SvgPicture.asset( + AppIcons.archived, + width: 24, + height: 24, + colorFilter: ColorFilter.mode( + _theme.searchText, + BlendMode.srcIn, + ), + ), + const SizedBox(width: 28.66), + Expanded( + child: Text( + 'Archived', + style: TextStyle( + fontFamily: 'SF Pro Text', + fontWeight: FontWeight.w600, + fontSize: 16, + color: _theme.searchText, + ), + ), + ), + ], + ), + ); + } + + void _onFilterSelected(String filter) { + setState(() { + _selectedFilter = filter; + }); + + if (filter == 'All') { + _chatListController?.clearSearch(); + return; + } + + final List filteredList; + switch (filter) { + case 'Unread': + filteredList = _chatListController?.chatListMap.values + .where((chat) => (chat.unreadCount ?? 0) > 0) + .toList() ?? + []; + break; + case 'Favourites': + filteredList = _chatListController?.chatListMap.values + .where((chat) => chat.settings.pinStatus.isPinned) + .toList() ?? + []; + break; + case 'Groups': + filteredList = _chatListController?.chatListMap.values + .where((chat) => chat.chatRoomType == ChatRoomType.group) + .toList() ?? + []; + break; + default: + filteredList = _chatListController?.chatListMap.values.toList() ?? []; + break; + } + _chatListController?.setSearchChats(filteredList); + } + + void showSnackBar(String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(message)), + ); + } + + @override + void dispose() { + _chatListController?.dispose(); + _searchController.dispose(); + super.dispose(); + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart index 58fb0a8e..f994bf81 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -4,6 +4,7 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:intl/intl.dart'; import 'data.dart'; +import 'example_two/example_two_list_screen.dart'; import 'models/chatview_list_theme.dart'; import 'models/chatview_theme.dart'; import 'values/colors.dart'; @@ -22,12 +23,12 @@ class Example extends StatelessWidget { title: 'Flutter Chat UI Demo', debugShowCheckedModeBanner: false, theme: ThemeData( - primaryColor: AppColors.instaPurple, - colorScheme: ColorScheme.fromSwatch(accentColor: AppColors.instaPurple), + primaryColor: AppColors.uiOnePurple, + colorScheme: ColorScheme.fromSwatch(accentColor: AppColors.uiOnePurple), ), darkTheme: ThemeData( - primaryColor: AppColors.instaPurple, - colorScheme: ColorScheme.fromSwatch(accentColor: AppColors.instaPurple), + primaryColor: AppColors.uiOnePurple, + colorScheme: ColorScheme.fromSwatch(accentColor: AppColors.uiOnePurple), ), home: const ExampleOneListScreen(), ); @@ -63,10 +64,6 @@ class _ExampleOneListScreenState extends State { header: _headerWidget(), appbar: ChatViewListAppBar( backgroundColor: _theme.backgroundColor, - leading: Icon( - Icons.arrow_back_ios_rounded, - color: _theme.iconColor, - ), centerTitle: false, scrolledUnderElevation: 0, titleText: 'ChatViewList', @@ -203,7 +200,7 @@ class _ExampleOneListScreenState extends State { _theme = ChatViewListTheme.uiOneLight; _isDarkTheme = false; } else { - _theme = ChatViewListTheme.uiOneDart; + _theme = ChatViewListTheme.uiOneDark; _isDarkTheme = true; } }); @@ -217,14 +214,14 @@ class _ExampleOneListScreenState extends State { if (highlight) ...[ const CircleAvatar( radius: 4, - backgroundColor: AppColors.instaUnreadCountDot, + backgroundColor: AppColors.uiOneUnreadCountDot, ), const SizedBox(width: 12), ], SvgPicture.asset( AppIcons.camera2, colorFilter: ColorFilter.mode( - highlight ? _theme.iconColor : AppColors.instaDarkGrey, + highlight ? _theme.iconColor : AppColors.uiOneDarkGrey, BlendMode.srcIn, ), ), @@ -233,30 +230,58 @@ class _ExampleOneListScreenState extends State { } 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, - ), - overflow: TextOverflow.ellipsis, + return Column( + children: [ + const SizedBox(height: 16), + GestureDetector( + onTap: () => Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => const ExampleTwoListScreen(), ), ), - Text( - 'Requests', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle(color: _theme.searchText), + child: Text( + '✨ Check out another UI', + style: TextStyle( + color: _theme.textColor, + shadows: [ + Shadow( + color: _isDarkTheme ? Colors.white54 : Colors.black54, + offset: const Offset(0, -1), + blurRadius: 1, + ), + ], + decorationColor: _theme.textColor, + decoration: TextDecoration.underline, + ), ), - ], - ), + ), + 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, + ), + overflow: TextOverflow.ellipsis, + ), + ), + Text( + 'Requests', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle(color: _theme.searchText), + ), + ], + ), + ), + ], ); } @@ -597,7 +622,7 @@ class _ExampleOneChatScreenState extends State { ), ), sendButtonStyle: IconButton.styleFrom( - backgroundColor: AppColors.instaPurple, + backgroundColor: AppColors.uiOnePurple, padding: const EdgeInsets.symmetric(horizontal: 16), ), textFieldConfig: TextFieldConfiguration( @@ -619,7 +644,7 @@ class _ExampleOneChatScreenState extends State { color: Colors.white, ), style: IconButton.styleFrom( - backgroundColor: AppColors.instaPurple, + backgroundColor: AppColors.uiOnePurple, ), onPressed: (path, replyMessage) { if (path?.isEmpty ?? true) return; @@ -658,7 +683,7 @@ class _ExampleOneChatScreenState extends State { color: Colors.white, ), style: IconButton.styleFrom( - backgroundColor: AppColors.instaPurple, + backgroundColor: AppColors.uiOnePurple, ), onPressed: () => ScaffoldMessenger.of(context).showSnackBar( @@ -735,7 +760,7 @@ class _ExampleOneChatScreenState extends State { color: Colors.white, ), ), - color: AppColors.instaPurple, + color: AppColors.uiOnePurple, textStyle: TextStyle(color: Colors.white, fontSize: 16), receiptsWidgetConfig: ReceiptsWidgetConfig( showReceiptsIn: ShowReceiptsIn.lastMessage, @@ -1025,17 +1050,17 @@ class _ExampleOneChatScreenState extends State { children: [ CircleAvatar( radius: 2.75, - backgroundColor: AppColors.instaDarkGrey, + backgroundColor: AppColors.uiOneDarkGrey, ), SizedBox(width: 3), CircleAvatar( radius: 2.75, - backgroundColor: AppColors.instaDarkGrey, + backgroundColor: AppColors.uiOneDarkGrey, ), SizedBox(width: 3), CircleAvatar( radius: 2.75, - backgroundColor: AppColors.instaDarkGrey, + backgroundColor: AppColors.uiOneDarkGrey, ), ], ), diff --git a/example/lib/models/chatview_list_theme.dart b/example/lib/models/chatview_list_theme.dart index 29f9ac64..984a756b 100644 --- a/example/lib/models/chatview_list_theme.dart +++ b/example/lib/models/chatview_list_theme.dart @@ -9,6 +9,13 @@ class ChatViewListTheme { required this.lastMessageText, required this.backgroundColor, required this.secondaryBg, + required this.iconButton, + required this.divider, + required this.floatingButton, + required this.selectedChip, + required this.selectedChipBg, + this.chipBg = Colors.transparent, + this.chipText = Colors.black, }); final Color textColor; @@ -18,24 +25,75 @@ class ChatViewListTheme { final Color lastMessageText; final Color backgroundColor; final Color secondaryBg; + final Color iconButton; + final Color divider; + final Color floatingButton; + final Color selectedChip; + final Color selectedChipBg; + final Color chipBg; + final Color chipText; - static const ChatViewListTheme uiOneDart = ChatViewListTheme( + static const ChatViewListTheme uiOneDark = ChatViewListTheme( iconColor: Colors.white, + divider: Colors.white, + iconButton: Colors.white, textColor: Colors.white, searchBg: Color(0xff26292E), searchText: Color(0xffA7ADB6), lastMessageText: Color(0xff74787F), backgroundColor: Color(0xff0B1014), secondaryBg: Color(0xff26292E), + floatingButton: Colors.white, + selectedChip: Colors.black, + selectedChipBg: Colors.white, + ); + + static const ChatViewListTheme uiTwoDark = ChatViewListTheme( + iconColor: Colors.white, + iconButton: Color(0xff222222), + textColor: Colors.white, + searchBg: Color(0xff222222), + searchText: Color(0xff969494), + lastMessageText: Color(0xff74787F), + backgroundColor: Color(0xff0A0A0A), + secondaryBg: Color(0xff26292E), + divider: Color(0xff212121), + floatingButton: Color(0xFF242626), + selectedChipBg: Color(0xFF1A342A), + selectedChip: Color(0xFFE0FCD6), + chipBg: Color(0xFF161717), + chipText: Color(0xFF969595), + ); + + static const ChatViewListTheme uiTwoLight = ChatViewListTheme( + iconColor: Colors.black, + iconButton: Color(0x080A0A0A), + textColor: Colors.black, + searchBg: Color(0xFFF4F4F4), + searchText: Color(0xff767779), + lastMessageText: Colors.black, + backgroundColor: Color(0xffFEFFFE), + secondaryBg: Color(0xffF3F5F7), + divider: Color(0x33000000), + floatingButton: Color(0xFFF5F2EB), + selectedChipBg: Color(0xFFD0FECF), + selectedChip: Color(0xFF15603E), + chipBg: Color(0xFFF4F4F4), + chipText: Color(0xFF767779), ); static const ChatViewListTheme uiOneLight = ChatViewListTheme( iconColor: Colors.black, + divider: Colors.black, + iconButton: Colors.black, textColor: Colors.black, searchBg: Color(0xffF3F4F7), searchText: Color(0xff5D636E), lastMessageText: Colors.black, backgroundColor: Color(0xffFEFFFE), secondaryBg: Color(0xffF3F5F7), + floatingButton: Colors.black, + selectedChip: Colors.white, + selectedChipBg: Colors.black, ); } diff --git a/example/lib/values/borders.dart b/example/lib/values/borders.dart new file mode 100644 index 00000000..bc8b73a8 --- /dev/null +++ b/example/lib/values/borders.dart @@ -0,0 +1,11 @@ +import 'package:flutter/material.dart'; + +class AppBorders { + static const Border exampleTwoMessageBorder = Border.fromBorderSide( + BorderSide( + width: 0.66, + color: Color(0x0F000000), + strokeAlign: BorderSide.strokeAlignOutside, + ), + ); +} diff --git a/example/lib/values/colors.dart b/example/lib/values/colors.dart index fc88ea58..a9e521ba 100644 --- a/example/lib/values/colors.dart +++ b/example/lib/values/colors.dart @@ -1,16 +1,14 @@ 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); + static const Color uiTwoGreen = Color(0xff1DAB61); + static const Color uiTwoGrey = Color(0xff767779); + static const Color uiTwoBackground = Color(0xFFF5F2EB); + static const Color uiTwoReplyLineColor = Color(0xffD42A66); + static const Color uiTwoSenderBgColor = Color(0xffD0FECF); + static const Color uiOneDarkGrey = Color(0xff767779); + static const Color uiOnePurple = Color(0xFF574FF0); + static const Color uiOneUnreadCountDot = Color(0xff5C6BF6); } diff --git a/example/lib/values/icons.dart b/example/lib/values/icons.dart index ccf0e5da..9a86368e 100644 --- a/example/lib/values/icons.dart +++ b/example/lib/values/icons.dart @@ -1,5 +1,7 @@ class AppIcons { + static const String camera = 'assets/vectors/camera.svg'; static const String camera2 = 'assets/vectors/camera_2.svg'; + static const String cameraOutline = "assets/vectors/camera_outline.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"; @@ -8,4 +10,17 @@ class AppIcons { 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"; + static const String add = 'assets/vectors/add.svg'; + static const String calls = "assets/vectors/calls.svg"; + static const String archived = "assets/vectors/archived.svg"; + static const String closeCircular = "assets/vectors/close_circular.svg"; + static const String plus = 'assets/vectors/plus.svg'; + static const String community = "assets/vectors/community.svg"; + static const String lock = "assets/vectors/lock.svg"; + static const String status = "assets/vectors/status.svg"; + static const String messages = "assets/vectors/messages.svg"; + static const String settings = "assets/vectors/settings.svg"; + static const String mic = "assets/vectors/mic.svg"; + static const String send = "assets/vectors/send.svg"; + static const String moreHoriz = 'assets/vectors/more_horizontal.svg'; } diff --git a/example/lib/values/images.dart b/example/lib/values/images.dart new file mode 100644 index 00000000..b93fe83f --- /dev/null +++ b/example/lib/values/images.dart @@ -0,0 +1,3 @@ +class AppImages { + static const String wpChatBackground = "assets/images/chat_background.png"; +} diff --git a/example/lib/widgets/reply_message_tile.dart b/example/lib/widgets/reply_message_tile.dart new file mode 100644 index 00000000..f54d566d --- /dev/null +++ b/example/lib/widgets/reply_message_tile.dart @@ -0,0 +1,106 @@ +import 'package:audio_waveforms/audio_waveforms.dart'; +import 'package:chatview/chatview.dart'; +import 'package:flutter/material.dart'; + +import '../../values/colors.dart'; + +class ReplyMessageTile extends StatelessWidget { + const ReplyMessageTile({ + required this.replyMessage, + required this.chatController, + super.key, + }); + + final ReplyMessage? replyMessage; + final ChatController chatController; + + @override + Widget build(BuildContext context) { + const textStyle = TextStyle( + fontSize: 12, + fontWeight: FontWeight.w400, + height: 1.33, + color: Color(0xFF232626), + ); + final reply = replyMessage; + if (reply == null) { + return const SizedBox.shrink(); + } + final replyBySender = reply.replyBy == chatController.currentUser.id; + final messagedUser = chatController.getUserFromId(reply.replyBy); + final replyBy = + replyBySender ? PackageStrings.currentLocale.you : messagedUser.name; + return Container( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * 0.75, + ), + margin: const EdgeInsets.only(bottom: 6), + decoration: BoxDecoration( + color: replyBySender ? AppColors.uiTwoSenderBgColor : Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + ), + child: Container( + padding: const EdgeInsets.fromLTRB(9, 9.5, 9, 10.5), + decoration: const BoxDecoration( + color: Color(0x0A0A0A0A), + borderRadius: BorderRadius.all(Radius.circular(8)), + border: Border( + left: BorderSide(color: AppColors.uiTwoReplyLineColor, width: 4), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + replyBy, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 14, + height: 1.36, + letterSpacing: -0.01, + fontWeight: FontWeight.w600, + color: AppColors.uiTwoReplyLineColor, + ), + ), + const SizedBox(height: 2), + switch (reply.messageType) { + MessageType.voice => Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.mic), + const SizedBox(width: 4), + if (reply.voiceMessageDuration != null) + Text( + reply.voiceMessageDuration!.toHHMMSS(), + style: textStyle, + ), + ], + ), + MessageType.image => Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.photo, + size: 20, + color: Colors.grey.shade700, + ), + Text( + PackageStrings.currentLocale.photo, + style: textStyle, + ), + ], + ), + MessageType.custom || MessageType.text => Text( + reply.message, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: textStyle, + ), + }, + ], + ), + ), + ); + } +} diff --git a/example/lib/widgets/wp_custom_chat_bar.dart b/example/lib/widgets/wp_custom_chat_bar.dart new file mode 100644 index 00000000..f3e0a52c --- /dev/null +++ b/example/lib/widgets/wp_custom_chat_bar.dart @@ -0,0 +1,399 @@ +import 'dart:io'; + +import 'package:chatview/chatview.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:audio_waveforms/audio_waveforms.dart'; + +import '../../values/colors.dart'; +import '../../values/icons.dart'; + +class WpCustomChatBar extends StatefulWidget { + const WpCustomChatBar({ + required this.chatController, + required this.replyMessage, + this.onAttachPressed, + super.key, + }); + + final ChatController chatController; + final ReplyMessage replyMessage; + final VoidCallback? onAttachPressed; + + @override + State createState() => _WpCustomChatBarState(); +} + +class _WpCustomChatBarState extends State { + RecorderController? controller; + late ReplyMessage? _replyMessage = widget.replyMessage; + final voiceRecordingConfig = const VoiceRecordingConfiguration(); + final isRecording = ValueNotifier(false); + final _focusNode = FocusNode(); + final _textController = TextEditingController(); + final _hasTextNotifier = ValueNotifier(false); + + @override + void initState() { + super.initState(); + if (!kIsWeb && (Platform.isIOS || Platform.isAndroid)) { + controller = RecorderController(); + } + _textController.addListener(_onTextChanged); + } + + @override + Widget build(BuildContext context) { + if (_replyMessage != null) { + _replyMessage = widget.replyMessage; + } + final repliedUser = _replyMessage?.replyTo.isNotEmpty ?? false + ? widget.chatController.getUserFromId(_replyMessage?.replyTo ?? '') + : null; + String replyTo = + _replyMessage?.replyTo == widget.chatController.currentUser.id + ? PackageStrings.currentLocale.you + : repliedUser?.name ?? ''; + return Container( + color: AppColors.uiTwoBackground, + child: SafeArea( + top: false, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (_replyMessage?.message.isNotEmpty ?? false) + Container( + padding: const EdgeInsets.fromLTRB(8, 8, 7.5, 7.5), + decoration: const BoxDecoration( + border: Border( + left: BorderSide(color: AppColors.uiTwoReplyLineColor, width: 4), + ), + ), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + replyTo, + style: const TextStyle( + fontStyle: FontStyle.normal, + fontWeight: FontWeight.w600, + fontSize: 14, + height: 1.3571428571, + letterSpacing: -0.01, + color: Color(0xFFD42A66), + ), + ), + const SizedBox(height: 1.5), + Text( + _replyMessage?.message ?? '', + style: const TextStyle( + fontSize: 12, + height: 1.33, + color: Color(0xFF0A0A0A), + ), + ), + ], + ), + ), + const SizedBox(width: 16), + SizedBox.square( + dimension: 32, + child: IconButton( + onPressed: () => ChatView.closeReplyMessageView( + context, + ), + padding: EdgeInsets.zero, + icon: SvgPicture.asset(AppIcons.closeCircular), + ), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(7, 5.5, 9, 5.5), + child: ValueListenableBuilder( + valueListenable: isRecording, + builder: (context, isRecordingValue, _) => Row( + children: [ + if (!isRecordingValue) ...[ + SizedBox.square( + dimension: 32, + child: IconButton( + padding: EdgeInsets.zero, + onPressed: widget.onAttachPressed, + icon: SvgPicture.asset(AppIcons.plus), + ), + ), + const SizedBox(width: 8), + ], + Expanded( + child: DecoratedBox( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.all(Radius.circular(15)), + border: Border.fromBorderSide( + BorderSide(width: 0.33, color: Color(0xFFB2B2B2)), + ), + ), + child: Row( + children: [ + if (isRecordingValue && + controller != null && + !kIsWeb) + Expanded( + child: AudioWaveforms( + size: const Size(double.maxFinite, 50), + recorderController: controller!, + margin: voiceRecordingConfig.margin, + padding: voiceRecordingConfig.padding ?? + const EdgeInsets.symmetric(horizontal: 5), + decoration: voiceRecordingConfig.decoration ?? + BoxDecoration( + color: voiceRecordingConfig + .backgroundColor, + borderRadius: BorderRadius.circular(12), + ), + waveStyle: voiceRecordingConfig.waveStyle ?? + WaveStyle( + extendWaveform: true, + showMiddleLine: false, + waveColor: voiceRecordingConfig + .waveStyle?.waveColor ?? + Colors.black, + ), + ), + ) + else + Expanded( + child: TextField( + maxLines: null, + focusNode: _focusNode, + controller: _textController, + textAlignVertical: TextAlignVertical.bottom, + style: const TextStyle( + fontSize: 16, + letterSpacing: -0.02, + fontWeight: FontWeight.w400, + ), + decoration: const InputDecoration( + isDense: true, + border: InputBorder.none, + contentPadding: + EdgeInsets.fromLTRB(10, 4, 16, 4), + hintStyle: TextStyle( + color: Color(0xFF999999), + fontSize: 16, + ), + ), + onSubmitted: (_) => _sendMessage(), + ), + ), + if (!isRecordingValue) ...[ + SizedBox.square( + dimension: 24, + child: EmojiPickerActionButton( + context: context, + style: IconButton.styleFrom( + padding: EdgeInsets.zero, + ), + icon: SvgPicture.asset(AppIcons.sticker), + onPressed: (emoji, replyMessage) { + if (emoji?.isEmpty ?? true) return; + _textController.text = + _textController.text + emoji!; + }, + ), + ), + const SizedBox(width: 9), + ], + ], + ), + ), + ), + const SizedBox(width: 7), + AnimatedSize( + alignment: Alignment.centerRight, + curve: Curves.easeInOut, + duration: const Duration(milliseconds: 400), + child: ValueListenableBuilder( + valueListenable: _hasTextNotifier, + builder: (_, hasText, __) { + if (isRecordingValue) { + return Row( + children: [ + SizedBox.square( + dimension: 32, + child: IconButton( + onPressed: _cancelRecording, + padding: EdgeInsets.zero, + icon: const Icon( + Icons.stop_circle_outlined), + ), + ), + const SizedBox(width: 7), + SizedBox.square( + dimension: 32, + child: IconButton( + onPressed: _recordOrStop, + padding: EdgeInsets.zero, + style: IconButton.styleFrom( + backgroundColor: AppColors.uiTwoGreen, + ), + icon: SvgPicture.asset(AppIcons.send), + ), + ), + ], + ); + } + return Row( + children: hasText + ? [ + SizedBox.square( + dimension: 32, + child: IconButton( + onPressed: _sendMessage, + padding: EdgeInsets.zero, + style: IconButton.styleFrom( + backgroundColor: AppColors.uiTwoGreen, + ), + icon: + SvgPicture.asset(AppIcons.send), + ), + ), + ] + : [ + SizedBox.square( + dimension: 32, + child: CameraActionButton( + icon: SvgPicture.asset( + AppIcons.cameraOutline, + ), + style: IconButton.styleFrom( + padding: EdgeInsets.zero, + ), + onPressed: (path, replyMessage) { + if (path?.isEmpty ?? true) return; + ChatView.closeReplyMessageView( + context); + widget.chatController.addMessage( + Message( + id: DateTime.now() + .millisecondsSinceEpoch + .toString(), + message: path!, + createdAt: DateTime.now(), + messageType: MessageType.image, + replyMessage: _replyMessage ?? + const ReplyMessage(), + sentBy: widget.chatController + .currentUser.id, + ), + ); + }, + ), + ), + if (!kIsWeb) ...[ + const SizedBox(width: 7), + SizedBox.square( + dimension: 32, + child: IconButton( + onPressed: _recordOrStop, + padding: EdgeInsets.zero, + icon: SvgPicture.asset( + AppIcons.mic), + ), + ), + ], + ], + ); + }), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } + + @override + void dispose() { + _textController.removeListener(_onTextChanged); + _textController.dispose(); + _focusNode.dispose(); + super.dispose(); + } + + void _onTextChanged() { + _hasTextNotifier.value = _textController.text.trim().isNotEmpty; + } + + Future _cancelRecording() async { + if (!isRecording.value) return; + final path = await controller?.stop(); + if (path == null) { + isRecording.value = false; + return; + } + final file = File(path); + + if (await file.exists()) { + await file.delete(); + } + + isRecording.value = false; + } + + void _sendMessage() { + final text = _textController.text.trim(); + if (text.isNotEmpty) { + ChatView.closeReplyMessageView(context); + widget.chatController.addMessage( + Message( + id: DateTime.now().millisecondsSinceEpoch.toString(), + message: text, + createdAt: DateTime.now(), + replyMessage: _replyMessage ?? const ReplyMessage(), + messageType: MessageType.text, + sentBy: widget.chatController.currentUser.id, + ), + ); + _textController.clear(); + _hasTextNotifier.value = false; + } + } + + Future _recordOrStop() async { + if (!isRecording.value) { + await controller?.record( + sampleRate: voiceRecordingConfig.sampleRate, + bitRate: voiceRecordingConfig.bitRate, + androidEncoder: voiceRecordingConfig.androidEncoder, + iosEncoder: voiceRecordingConfig.iosEncoder, + androidOutputFormat: voiceRecordingConfig.androidOutputFormat, + ); + isRecording.value = true; + } else { + final path = await controller?.stop(); + isRecording.value = false; + if (path?.isEmpty ?? true) return; + if (mounted) ChatView.closeReplyMessageView(context); + widget.chatController.addMessage( + Message( + id: DateTime.now().millisecondsSinceEpoch.toString(), + message: path!, + createdAt: DateTime.now(), + messageType: MessageType.voice, + replyMessage: _replyMessage ?? const ReplyMessage(), + sentBy: widget.chatController.currentUser.id, + ), + ); + } + } +} 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 f3f4564e..3be1f667 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 @@ -57,6 +57,7 @@ class ListTileConfig { fontWeight: FontWeight.normal, ), this.highlightTextStyle = const TextStyle(fontWeight: FontWeight.bold), + this.lastMessageIconColor = Colors.black, this.userNameTextStyle, this.onTap, this.lastMessageTileBuilder, @@ -91,6 +92,12 @@ class ListTileConfig { /// Defaults to `1`. final int? userNameMaxLines; + /// Color for icons used in the last message for message types + /// like image, voice. + /// + /// Defaults to `Colors.black`. + final Color lastMessageIconColor; + /// Text styles for the last message text in the user widget. final TextStyle? lastMessageTextStyle; 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 38a9a748..13a3eeff 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 @@ -139,6 +139,7 @@ class ChatViewListItemTile extends StatelessWidget { : ValueKey(lastMessage?.id), unreadCount: unreadCount, lastMessage: lastMessage!, + iconColor: config.lastMessageIconColor, lastMessageType: lastMessage.messageType, lastMessageMaxLines: config.lastMessageMaxLines, 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 88ce648f..206039f9 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, + required this.iconColor, this.highlightTextStyle, this.lastMessageType, this.lastMessageBuilder, @@ -39,6 +40,7 @@ class LastMessageView extends StatelessWidget { final bool showStatusIcon; final TextStyle? highlightTextStyle; final LastMessageStatusConfig statusConfig; + final Color iconColor; @override Widget build(BuildContext context) { @@ -66,7 +68,7 @@ class LastMessageView extends StatelessWidget { MessageType.image => Row( mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.photo, size: 14), + Icon(Icons.photo, size: 14, color: iconColor), const SizedBox(width: 5), Flexible( child: Text( @@ -92,7 +94,7 @@ class LastMessageView extends StatelessWidget { MessageType.voice => Row( mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.mic, size: 14), + Icon(Icons.mic, size: 14, color: iconColor), const SizedBox(width: 5), Flexible( child: Text( diff --git a/pubspec.yaml b/pubspec.yaml index 631d2af7..53b86451 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,7 +23,6 @@ dependencies: chatview_utils: git: url: https://github.com/SimformSolutionsPvtLtd/chatview_utils - ref: feature/update_changelog emoji_picker_flutter: ^4.3.0 flutter: