diff --git a/lib/ui/screens/conversation_screen.dart b/lib/ui/screens/conversation_screen.dart index ed0fca6..df8fe9f 100644 --- a/lib/ui/screens/conversation_screen.dart +++ b/lib/ui/screens/conversation_screen.dart @@ -16,6 +16,7 @@ import 'package:comunic/ui/tiles/conversation_message_tile.dart'; import 'package:comunic/ui/tiles/server_conversation_message_tile.dart'; import 'package:comunic/ui/widgets/safe_state.dart'; import 'package:comunic/ui/widgets/scroll_watcher.dart'; +import 'package:comunic/utils/date_utils.dart'; import 'package:comunic/utils/files_utils.dart'; import 'package:comunic/utils/intl_utils.dart'; import 'package:comunic/utils/list_utils.dart'; @@ -80,6 +81,10 @@ class _ConversationScreenState extends SafeState { showEmojiContainer() => setState(() => _showEmojiPicker = true); // Colors definition + Color get _senderColor => + _conversation.color ?? + (darkTheme() ? Color(0xff2b343b) : Colors.blue.shade900); + Color get _receiverColor => darkTheme() ? Color(0xff3a3d40) : Colors.grey.shade600; @@ -337,19 +342,6 @@ class _ConversationScreenState extends SafeState { setState(() => _textController = TextEditingController()); } - /// Check if a message is the last message of a user or not - bool _isLastMessage(int index) { - return index == 0 || - (index > 0 && _messages[index - 1].userID != _messages[index].userID); - } - - /// Check if a message is the first message of a user or not - bool _isFirstMessage(int index) { - return index == _messages.length - 1 || - (index < _messages.length - 1 && - _messages[index + 1].userID != _messages[index].userID); - } - /// Error handling Widget _buildError() { return buildErrorCard(tr("Could not load the list of messages!")); @@ -375,30 +367,111 @@ class _ConversationScreenState extends SafeState { /// Messages list Widget _buildMessagesList() { return Expanded( - child: ListView.builder( - controller: _scrollController, - reverse: true, - itemCount: _messages.length, - itemBuilder: (c, i) { - return _messages[i].isServerMessage - ? ServerConversationMessageTile( - message: _messages[i].serverMessage, - users: _usersInfo, - ) - : ConversationMessageTile( - conversation: _conversation, - message: _messages.elementAt(i), - userInfo: _usersInfo.getUser(_messages[i].userID), - isLastMessage: _isLastMessage(i), - isFirstMessage: _isFirstMessage(i), - onRequestMessageStats: _requestMessageStats, - onRequestMessageUpdate: _updateMessage, - onRequestMessageDelete: _deleteMessage, - ); - }), + child: ListView.builder( + controller: _scrollController, + reverse: true, + itemCount: _messages.length, + itemBuilder: (c, i) => _buildMessageItem(i), + )); + } + + Widget _buildMessageItem(int msgIndex) { + final msg = _messages[msgIndex]; + final nextMessage = + msgIndex + 1 < _messages.length ? _messages[msgIndex + 1] : null; + + return Column( + children: [ + Container( + child: !isSameDate(msg.date, nextMessage?.date) + ? _buildDateWidget(msg.date) + : null, + ), + msg.isServerMessage + ? Container( + alignment: Alignment.center, + child: ServerConversationMessageTile( + message: msg.serverMessage, users: _usersInfo), + ) + : Container( + margin: EdgeInsets.symmetric(vertical: 5), + alignment: + msg.isOwner ? Alignment.centerRight : Alignment.centerLeft, + child: msg.isOwner + ? _buildSenderLayout(msg, nextMessage) + : _buildReceiverLayout(msg, nextMessage), + ), + ], ); } + Widget _buildSenderLayout( + ConversationMessage message, ConversationMessage previousMessage) { + final messageRadius = Radius.circular(10); + + return Container( + margin: EdgeInsets.only(top: previousMessage?.isOwner == true ? 0 : 12), + constraints: + BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.65), + decoration: BoxDecoration( + color: _senderColor, + borderRadius: BorderRadius.only( + topLeft: messageRadius, + topRight: messageRadius, + bottomLeft: messageRadius, + ), + ), + child: Padding( + padding: EdgeInsets.all(10), + child: _buildMessage(message), + ), + ); + } + + Widget _buildReceiverLayout( + ConversationMessage message, ConversationMessage previousMessage) { + final messageRadius = Radius.circular(10); + + return Container( + margin: EdgeInsets.only( + top: previousMessage == null || + message.userID != previousMessage.userID + ? 12 + : 0), + constraints: + BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.65), + decoration: BoxDecoration( + color: _receiverColor, + borderRadius: BorderRadius.only( + bottomRight: messageRadius, + topRight: messageRadius, + bottomLeft: messageRadius, + ), + ), + child: Padding( + padding: EdgeInsets.all(10), + child: _buildMessage(message), + ), + ); + } + + Widget _buildMessage(ConversationMessage msg) => ConversationMessageTile( + message: msg, + user: _usersInfo.getUser(msg.userID), + onRequestMessageStats: _requestMessageStats, + onRequestMessageUpdate: _updateMessage, + onRequestMessageDelete: _deleteMessage, + ); + + Widget _buildDateWidget(DateTime dt) => Center( + child: Container( + child: Text( + formatDisplayDate(dt, time: false), + style: TextStyle(fontWeight: FontWeight.bold), + ), + padding: EdgeInsets.only(top: 50, bottom: 5), + )); + /// Send new message form Widget _buildSendMessageForm() => Container( padding: EdgeInsets.fromLTRB(10, 5, 10, 5), diff --git a/lib/ui/tiles/conversation_message_tile.dart b/lib/ui/tiles/conversation_message_tile.dart index 1b0cb9b..2b25f9f 100644 --- a/lib/ui/tiles/conversation_message_tile.dart +++ b/lib/ui/tiles/conversation_message_tile.dart @@ -1,8 +1,6 @@ import 'package:clipboard/clipboard.dart'; -import 'package:comunic/models/conversation.dart'; import 'package:comunic/models/conversation_message.dart'; import 'package:comunic/models/user.dart'; -import 'package:comunic/ui/widgets/account_image_widget.dart'; import 'package:comunic/ui/widgets/conversation_file_tile.dart'; import 'package:comunic/ui/widgets/text_widget.dart'; import 'package:comunic/utils/date_utils.dart'; @@ -27,253 +25,95 @@ typedef OnRequestMessageUpdate = void Function(ConversationMessage); typedef OnRequestMessageDelete = void Function(ConversationMessage); class ConversationMessageTile extends StatelessWidget { - final Conversation conversation; final ConversationMessage message; - final User userInfo; - final bool isLastMessage; - final bool isFirstMessage; + final User user; final OnRequestMessageStats onRequestMessageStats; final OnRequestMessageUpdate onRequestMessageUpdate; final OnRequestMessageDelete onRequestMessageDelete; const ConversationMessageTile({ Key key, - @required this.conversation, @required this.message, - @required this.userInfo, - @required this.isLastMessage, - @required this.isFirstMessage, + @required this.user, @required this.onRequestMessageStats, @required this.onRequestMessageUpdate, @required this.onRequestMessageDelete, }) : assert(message != null), - assert(userInfo != null), - assert(isLastMessage != null), - assert(isFirstMessage != null), + assert(user != null), assert(onRequestMessageStats != null), assert(onRequestMessageUpdate != null), assert(onRequestMessageDelete != null), super(key: key); - Color get backgroundColor => message.isOwner - ? conversation.color ?? Colors.blueAccent - : darkTheme() - ? Colors.white12 - : Colors.black12; - - /// Build account image - Widget _buildAccountImage(BuildContext context) { - return Container( - margin: EdgeInsets.all(10.0), - child: PopupMenuButton<_MenuChoices>( - child: AccountImageWidget( - user: userInfo, - width: 35.0, - ), - itemBuilder: (c) => >[ - PopupMenuItem( - enabled: (message.message?.content ?? "") != "", - value: _MenuChoices.COPY_MESSAGE, - child: Text(tr("Copy message")), - ), - - PopupMenuItem( - enabled: message.file != null, - value: _MenuChoices.COPY_URL, - child: Text(tr("Copy URL")), - ), - - PopupMenuItem( - value: _MenuChoices.GET_STATS, - child: Text(tr("Statistics")), - ), - - // Update message content - PopupMenuItem( - enabled: message.isOwner && - message.message != null && - message.message.content.isNotEmpty, - value: _MenuChoices.REQUEST_UPDATE_CONTENT, - child: Text(tr("Update")), - ), - - // Delete the message - PopupMenuItem( - enabled: message.isOwner, - value: _MenuChoices.DELETE, - child: Text(tr("Delete")), - ), - ]..removeWhere((element) => !element.enabled), - onSelected: _menuOptionSelected, - ), - ); - } - - /// Build widget image - Widget _buildMessageFile(BuildContext context) => ConversationFileWidget( - messageID: message.id, - file: message.file, - defaultBackgroundColor: backgroundColor, - ); - - /// Build message date - Widget _buildMessageDate() { - return isLastMessage - ? Container( - margin: EdgeInsets.only(top: 5.0), - child: Text( - dateTimeToString(message.date), - style: TextStyle( - color: darkTheme() ? Colors.white : Colors.black54, - fontSize: 12.0), - textAlign: TextAlign.center, - ), - ) - : Container(); - } - - /// Build a message of the current user - Widget _buildRightMessage(BuildContext context) { - return Container( - margin: EdgeInsets.only(bottom: isLastMessage ? 20.0 : 2.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Column( - children: [ - Row( - children: [ - Column( - children: [ - // Text image - Container( - child: - message.hasFile ? _buildMessageFile(context) : null, - ), - - // Text message - Container( - child: message.hasMessage - ? Container( - width: 200.0, - alignment: Alignment.centerRight, - child: Container( - child: TextWidget( - content: message.message, - textAlign: TextAlign.justify, - style: TextStyle(color: Colors.white), - linksColor: Colors.indigo, - ), - padding: EdgeInsets.fromLTRB( - 15.0, 10.0, 15.0, 10.0), - decoration: BoxDecoration( - color: backgroundColor, - borderRadius: BorderRadius.circular(8.0), - ), - ), - ) - : null, - ), - ], - ), - - // Account image - _buildAccountImage(context) - ], - ), - - // Date - Container( - child: _buildMessageDate(), - margin: EdgeInsets.only(right: 45.0), - ) - ], - ), - ], - ), - ); - // Text - } - - /// Build a message of a peer user - Widget _buildLeftMessage(BuildContext context) { - return Container( - margin: EdgeInsets.only(bottom: isLastMessage ? 20.0 : 5.0), - child: Column( + @override + Widget build(BuildContext context) => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - //User name - Container( - margin: EdgeInsets.only(left: 55.0), - child: isFirstMessage - ? Text( - userInfo.fullName, - textAlign: TextAlign.left, - style: TextStyle(fontSize: 12.0), + RichText( + text: TextSpan( + text: + "${user.fullName} - ${formatDisplayDate(message.date, date: false)}", + style: TextStyle(color: Colors.white, fontSize: 11), + children: [ + WidgetSpan( + child: PopupMenuButton<_MenuChoices>( + child: Icon( + Icons.more_vert, + color: Colors.white, + size: 12, + ), + onSelected: _menuOptionSelected, + itemBuilder: (c) => >[ + PopupMenuItem( + enabled: (message.message?.content ?? "") != "", + value: _MenuChoices.COPY_MESSAGE, + child: Text(tr("Copy message")), + ), + + PopupMenuItem( + enabled: message.file != null, + value: _MenuChoices.COPY_URL, + child: Text(tr("Copy URL")), + ), + + PopupMenuItem( + value: _MenuChoices.GET_STATS, + child: Text(tr("Statistics")), + ), + + // Update message content + PopupMenuItem( + enabled: message.isOwner && + message.message != null && + message.message.content.isNotEmpty, + value: _MenuChoices.REQUEST_UPDATE_CONTENT, + child: Text(tr("Update")), + ), + + // Delete the message + PopupMenuItem( + enabled: message.isOwner, + value: _MenuChoices.DELETE, + child: Text(tr("Delete")), + ), + ]..removeWhere((element) => !element.enabled), + ), ) - : null, + ]), ), - - Row( - children: [ - // Account image - _buildAccountImage(context), - - Column( - children: [ - // Text image - Container( - child: message.hasFile ? _buildMessageFile(context) : null, - ), - - // Text message - Container( - child: message.hasMessage - ? Container( - width: 200.0, - alignment: Alignment.centerLeft, - child: Container( - child: TextWidget( - content: message.message, - textAlign: TextAlign.justify, - style: TextStyle( - color: darkTheme() - ? Colors.white - : Colors.black), - ), - padding: - EdgeInsets.fromLTRB(15.0, 10.0, 15.0, 10.0), - decoration: BoxDecoration( - color: backgroundColor, - borderRadius: BorderRadius.circular(8.0), - ), - ), - ) - : null, - ), - ], - ), - ], - ), - - // Date - Container( - margin: EdgeInsets.only(left: 50.0), - child: Align( - alignment: Alignment.topLeft, - child: _buildMessageDate(), - ), - ) + _buildMessageContent(), ], - ), - ); - } + ); - @override - Widget build(BuildContext context) { - return message.isOwner - ? _buildRightMessage(context) - : _buildLeftMessage(context); + Widget _buildMessageContent() { + if (!message.hasFile) + return TextWidget( + content: message.message, + textAlign: TextAlign.justify, + style: TextStyle(color: darkTheme() ? Colors.white : Colors.black), + ); + + return ConversationFileWidget(messageID: message.id, file: message.file); } /// Process menu choice diff --git a/lib/ui/widgets/conversation_file_tile.dart b/lib/ui/widgets/conversation_file_tile.dart index 4e567b5..badf66e 100644 --- a/lib/ui/widgets/conversation_file_tile.dart +++ b/lib/ui/widgets/conversation_file_tile.dart @@ -13,11 +13,9 @@ import 'package:url_launcher/url_launcher.dart'; class ConversationFileWidget extends StatefulWidget { final int messageID; final ConversationMessageFile file; - final Color defaultBackgroundColor; const ConversationFileWidget({ Key key, - @required this.defaultBackgroundColor, @required this.messageID, @required this.file, }) : assert(messageID != null), @@ -53,7 +51,6 @@ class _ConversationFileWidgetState extends State { // We open it in the browser default: return Container( - color: widget.defaultBackgroundColor, child: Center( child: MaterialButton( child: Column( diff --git a/lib/utils/date_utils.dart b/lib/utils/date_utils.dart index 5a4cbb9..a32b4c8 100644 --- a/lib/utils/date_utils.dart +++ b/lib/utils/date_utils.dart @@ -1,4 +1,5 @@ import 'package:comunic/utils/intl_utils.dart'; +import 'package:intl/intl.dart'; /// Date utilities /// @@ -56,3 +57,22 @@ String dateTimeToString(DateTime time) { /// Format a [Duration] in the form "MM:SS" String formatDuration(Duration d) => "${d.inMinutes < 10 ? "0" + d.inMinutes.toString() : d.inMinutes.toString()}:${d.inSeconds % 60 < 10 ? "0" + (d.inSeconds % 60).toString() : (d.inSeconds % 60).toString()}"; + +/// Compare two [DateTime] on their date +bool isSameDate(DateTime one, DateTime other) { + if (other == null) return false; + return one.year == other.year && + one.month == other.month && + one.day == other.day; +} + +/// Format a date to make it easily shown +String formatDisplayDate(DateTime dt, {bool date = true, bool time = true}) { + final format = Intl(Intl.systemLocale).date(); + if (date) { + format.add_EEEE(); + format.add_yMMMMd(); + } + if (time) format.add_jm(); + return format.format(dt); +} \ No newline at end of file