diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b9ab8e..17b4b0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## 0.6.0 - December 1 2023 - Made the message controller nullable +- Improved chat UI and added showTime option for chatDetailScreen to always show the time ## 0.5.0 - November 29 2023 diff --git a/packages/flutter_community_chat_view/example/lib/main.dart b/packages/flutter_community_chat_view/example/lib/main.dart index f07cec9..ee9aed6 100644 --- a/packages/flutter_community_chat_view/example/lib/main.dart +++ b/packages/flutter_community_chat_view/example/lib/main.dart @@ -75,6 +75,7 @@ class _MyStatefulWidgetState extends State { users: [pietUser, janUser], lastUsed: DateTime.now().subtract(const Duration(days: 1)), messages: messages, + canBeDeleted: false, ); Stream> get chatStream => (() { diff --git a/packages/flutter_community_chat_view/example/pubspec.yaml b/packages/flutter_community_chat_view/example/pubspec.yaml index 789e229..29a9f3d 100644 --- a/packages/flutter_community_chat_view/example/pubspec.yaml +++ b/packages/flutter_community_chat_view/example/pubspec.yaml @@ -15,10 +15,7 @@ dependencies: flutter: sdk: flutter flutter_community_chat_view: - git: - url: https://github.com/Iconica-Development/flutter_community_chat - path: packages/flutter_community_chat_view - ref: 0.6.0 + path: .. dev_dependencies: flutter_test: diff --git a/packages/flutter_community_chat_view/lib/src/components/chat_detail_row.dart b/packages/flutter_community_chat_view/lib/src/components/chat_detail_row.dart index cf14149..e4d2619 100644 --- a/packages/flutter_community_chat_view/lib/src/components/chat_detail_row.dart +++ b/packages/flutter_community_chat_view/lib/src/components/chat_detail_row.dart @@ -11,16 +11,18 @@ import 'package:flutter_community_chat_view/src/services/date_formatter.dart'; class ChatDetailRow extends StatefulWidget { const ChatDetailRow({ required this.translations, - required this.isFirstMessage, required this.message, required this.userAvatarBuilder, + this.previousMessage, + this.showTime = false, super.key, }); final ChatTranslations translations; - final bool isFirstMessage; final ChatMessageModel message; final UserAvatarBuilder userAvatarBuilder; + final bool showTime; + final ChatMessageModel? previousMessage; @override State createState() => _ChatDetailRowState(); @@ -30,82 +32,119 @@ class _ChatDetailRowState extends State { final DateFormatter _dateFormatter = DateFormatter(); @override - Widget build(BuildContext context) => Padding( - padding: EdgeInsets.only(top: widget.isFirstMessage ? 25.0 : 0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Opacity( - opacity: widget.isFirstMessage ? 1 : 0, - child: Padding( - padding: const EdgeInsets.only(left: 10.0), - child: widget.message.sender.imageUrl != null && - widget.message.sender.imageUrl!.isNotEmpty - ? ChatImage( - image: widget.message.sender.imageUrl!, - ) - : widget.userAvatarBuilder( - widget.message.sender, - 30, - ), - ), + Widget build(BuildContext context) { + var isNewDate = widget.previousMessage != null && + widget.message.timestamp.day != widget.previousMessage!.timestamp.day; + return Padding( + padding: EdgeInsets.only( + top: isNewDate || + widget.previousMessage == null || + widget.previousMessage?.sender.id != widget.message.sender.id + ? 25.0 + : 0, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (isNewDate || + widget.previousMessage == null || + widget.previousMessage?.sender.id != + widget.message.sender.id) ...[ + Padding( + padding: const EdgeInsets.only(left: 10.0), + child: widget.message.sender.imageUrl != null && + widget.message.sender.imageUrl!.isNotEmpty + ? ChatImage( + image: widget.message.sender.imageUrl!, + ) + : widget.userAvatarBuilder( + widget.message.sender, + 30, + ), ), - Expanded( - child: Container( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 22.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - if (widget.isFirstMessage) - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - widget.message.sender.fullName?.toUpperCase() ?? - widget.translations.anonymousUser, + ] else ...[ + const SizedBox( + width: 50, + ), + ], + Expanded( + child: Container( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 22.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + if (isNewDate || + widget.previousMessage == null || + widget.previousMessage?.sender.id != + widget.message.sender.id) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + widget.message.sender.fullName?.toUpperCase() ?? + widget.translations.anonymousUser, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + Padding( + padding: const EdgeInsets.only(top: 5.0), + child: Text( + _dateFormatter.format( + date: widget.message.timestamp, + showFullDate: true, + ), style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, + fontSize: 12, + color: Color(0xFFBBBBBB), ), ), - Padding( - padding: const EdgeInsets.only(top: 5.0), - child: Text( - _dateFormatter.format( - date: widget.message.timestamp, - showFullDate: true, - ), - style: const TextStyle( - fontSize: 12, - color: Color(0xFFBBBBBB), - ), - ), - ), - ], - ), - Padding( - padding: const EdgeInsets.only(top: 3.0), - child: widget.message is ChatTextMessageModel - ? Text( - (widget.message as ChatTextMessageModel).text, - style: const TextStyle(fontSize: 16), - overflow: TextOverflow.ellipsis, - maxLines: 999, - ) - : CachedNetworkImage( - imageUrl: - (widget.message as ChatImageMessageModel) - .imageUrl, - ), + ), + ], ), - ], - ), + Padding( + padding: const EdgeInsets.only(top: 3.0), + child: widget.message is ChatTextMessageModel + ? RichText( + text: TextSpan( + text: (widget.message as ChatTextMessageModel) + .text, + style: const TextStyle(fontSize: 16), + children: [ + if (widget.showTime) + TextSpan( + text: " ${_dateFormatter.format( + date: widget.message.timestamp, + showFullDate: true, + ).split(' ').last}", + style: const TextStyle( + fontSize: 12, + color: Color(0xFFBBBBBB), + ), + ) + else + const TextSpan(), + ], + ), + overflow: TextOverflow.ellipsis, + maxLines: 999, + ) + : CachedNetworkImage( + imageUrl: + (widget.message as ChatImageMessageModel) + .imageUrl, + ), + ), + ], ), ), ), - ], - ), - ); + ), + ], + ), + ); + } } diff --git a/packages/flutter_community_chat_view/lib/src/screens/chat_detail_screen.dart b/packages/flutter_community_chat_view/lib/src/screens/chat_detail_screen.dart index ecb0b87..3c9fe7c 100644 --- a/packages/flutter_community_chat_view/lib/src/screens/chat_detail_screen.dart +++ b/packages/flutter_community_chat_view/lib/src/screens/chat_detail_screen.dart @@ -23,6 +23,7 @@ class ChatDetailScreen extends StatefulWidget { this.chatMessages, this.onPressChatTitle, this.iconColor, + this.showTime = false, super.key, }); @@ -43,6 +44,7 @@ class ChatDetailScreen extends StatefulWidget { /// The color of the icon buttons in the chat bottom. final Color? iconColor; + final bool showTime; @override State createState() => _ChatDetailScreenState(); @@ -154,21 +156,21 @@ class _ChatDetailScreenState extends State { stream: _chatMessages, builder: (BuildContext context, snapshot) { var messages = snapshot.data ?? widget.chat?.messages ?? []; - ChatMessageModel? lastMessage; + ChatMessageModel? previousMessage; + var messageWidgets = []; for (var message in messages) { - var isFirstMessage = lastMessage == null || - lastMessage.sender.id != message.sender.id; messageWidgets.add( ChatDetailRow( + previousMessage: previousMessage, + showTime: widget.showTime, translations: widget.translations, message: message, - isFirstMessage: isFirstMessage, userAvatarBuilder: widget.options.userAvatarBuilder, ), ); - lastMessage = message; + previousMessage = message; } return ListView( diff --git a/packages/flutter_community_chat_view/lib/src/screens/chat_screen.dart b/packages/flutter_community_chat_view/lib/src/screens/chat_screen.dart index 6d7e251..919ee48 100644 --- a/packages/flutter_community_chat_view/lib/src/screens/chat_screen.dart +++ b/packages/flutter_community_chat_view/lib/src/screens/chat_screen.dart @@ -2,6 +2,8 @@ // // SPDX-License-Identifier: BSD-3-Clause +// ignore_for_file: lines_longer_than_80_chars + import 'package:flutter/material.dart'; import 'package:flutter_community_chat_view/flutter_community_chat_view.dart'; import 'package:flutter_community_chat_view/src/services/date_formatter.dart'; @@ -16,6 +18,7 @@ class ChatScreen extends StatefulWidget { this.deleteChatDialog, this.unreadChats, this.translations = const ChatTranslations(), + this.disableDismiss = false, super.key, }); @@ -26,6 +29,7 @@ class ChatScreen extends StatefulWidget { final VoidCallback? onPressStartChat; final void Function(ChatModel chat) onDeleteChat; final void Function(ChatModel chat) onPressChat; + final bool disableDismiss; /// Method to optionally change the bottomsheetdialog final Future Function(BuildContext, ChatModel)? deleteChatDialog; @@ -76,143 +80,104 @@ class _ChatScreenState extends State { children: [ for (ChatModel chat in snapshot.data ?? []) ...[ Builder( - builder: (context) => Dismissible( - confirmDismiss: (_) => - widget.deleteChatDialog?.call(context, chat) ?? - showModalBottomSheet( - context: context, - builder: (BuildContext context) => Container( - padding: const EdgeInsets.all(16.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - chat.canBeDeleted - ? translations - .deleteChatModalTitle - : translations.chatCantBeDeleted, - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 16), - if (chat.canBeDeleted) - Text( - translations - .deleteChatModalDescription, - style: - const TextStyle(fontSize: 16), - ), - const SizedBox(height: 16), - Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - TextButton( - child: Text( - translations - .deleteChatModalCancel, + builder: (context) => !widget.disableDismiss + ? Dismissible( + confirmDismiss: (_) => + widget.deleteChatDialog + ?.call(context, chat) ?? + showModalBottomSheet( + context: context, + builder: (BuildContext context) => + Container( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + chat.canBeDeleted + ? translations + .deleteChatModalTitle + : translations + .chatCantBeDeleted, style: const TextStyle( - fontSize: 16, + fontSize: 20, + fontWeight: FontWeight.bold, ), ), - onPressed: () => - Navigator.of(context) - .pop(false), - ), - if (chat.canBeDeleted) - ElevatedButton( - onPressed: () => - Navigator.of(context) - .pop(true), - child: Text( + const SizedBox(height: 16), + if (chat.canBeDeleted) + Text( translations - .deleteChatModalConfirm, + .deleteChatModalDescription, style: const TextStyle( fontSize: 16, ), ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + TextButton( + child: Text( + translations + .deleteChatModalCancel, + style: const TextStyle( + fontSize: 16, + ), + ), + onPressed: () => + Navigator.of(context) + .pop(false), + ), + if (chat.canBeDeleted) + ElevatedButton( + onPressed: () => + Navigator.of(context) + .pop(true), + child: Text( + translations + .deleteChatModalConfirm, + style: const TextStyle( + fontSize: 16, + ), + ), + ), + ], ), - ], + ], + ), ), - ], + ), + onDismissed: (_) => widget.onDeleteChat(chat), + background: Container( + color: Colors.red, + child: Align( + alignment: Alignment.centerRight, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + translations.deleteChatButton, + ), + ), ), ), - ), - onDismissed: (_) => widget.onDeleteChat(chat), - background: Container( - color: Colors.red, - child: Align( - alignment: Alignment.centerRight, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - translations.deleteChatButton, + key: ValueKey( + chat.id.toString(), ), + child: ChatListItem( + widget: widget, + chat: chat, + translations: translations, + dateFormatter: _dateFormatter, + ), + ) + : ChatListItem( + widget: widget, + chat: chat, + translations: translations, + dateFormatter: _dateFormatter, ), - ), - ), - key: ValueKey( - chat.id.toString(), - ), - child: GestureDetector( - onTap: () => widget.onPressChat(chat), - child: widget.options.chatRowContainerBuilder( - (chat is PersonalChatModel) - ? ChatRow( - unreadMessages: - chat.unreadMessages ?? 0, - avatar: - widget.options.userAvatarBuilder( - chat.user, - 40.0, - ), - title: chat.user.fullName ?? - translations.anonymousUser, - subTitle: chat.lastMessage != null - ? chat.lastMessage - is ChatTextMessageModel - ? (chat.lastMessage! - as ChatTextMessageModel) - .text - : '📷 ' - '${translations.image}' - : '', - lastUsed: chat.lastUsed != null - ? _dateFormatter.format( - date: chat.lastUsed!, - ) - : null, - ) - : ChatRow( - title: (chat as GroupChatModel).title, - unreadMessages: - chat.unreadMessages ?? 0, - subTitle: chat.lastMessage != null - ? chat.lastMessage - is ChatTextMessageModel - ? (chat.lastMessage! - as ChatTextMessageModel) - .text - : '📷 ' - '${translations.image}' - : '', - avatar: - widget.options.groupAvatarBuilder( - chat.title, - chat.imageUrl, - 40.0, - ), - lastUsed: chat.lastUsed != null - ? _dateFormatter.format( - date: chat.lastUsed!, - ) - : null, - ), - ), - ), - ), ), ], ], @@ -232,3 +197,68 @@ class _ChatScreenState extends State { ); } } + +class ChatListItem extends StatelessWidget { + const ChatListItem({ + required this.widget, + required this.chat, + required this.translations, + required DateFormatter dateFormatter, + super.key, + }) : _dateFormatter = dateFormatter; + + final ChatScreen widget; + final ChatModel chat; + final ChatTranslations translations; + final DateFormatter _dateFormatter; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () => widget.onPressChat(chat), + child: widget.options.chatRowContainerBuilder( + (chat is PersonalChatModel) + ? ChatRow( + unreadMessages: chat.unreadMessages ?? 0, + avatar: widget.options.userAvatarBuilder( + (chat as PersonalChatModel).user, + 40.0, + ), + title: (chat as PersonalChatModel).user.fullName ?? + translations.anonymousUser, + subTitle: chat.lastMessage != null + ? chat.lastMessage is ChatTextMessageModel + ? (chat.lastMessage! as ChatTextMessageModel).text + : '📷 ' + '${translations.image}' + : '', + lastUsed: chat.lastUsed != null + ? _dateFormatter.format( + date: chat.lastUsed!, + ) + : null, + ) + : ChatRow( + title: (chat as GroupChatModel).title, + unreadMessages: chat.unreadMessages ?? 0, + subTitle: chat.lastMessage != null + ? chat.lastMessage is ChatTextMessageModel + ? (chat.lastMessage! as ChatTextMessageModel).text + : '📷 ' + '${translations.image}' + : '', + avatar: widget.options.groupAvatarBuilder( + (chat as GroupChatModel).title, + (chat as GroupChatModel).imageUrl, + 40.0, + ), + lastUsed: chat.lastUsed != null + ? _dateFormatter.format( + date: chat.lastUsed!, + ) + : null, + ), + ), + ); + } +}