diff --git a/lib/helpers/conversations_helper.dart b/lib/helpers/conversations_helper.dart index d50cdad..3cd4e2c 100644 --- a/lib/helpers/conversations_helper.dart +++ b/lib/helpers/conversations_helper.dart @@ -29,21 +29,16 @@ class ConversationsHelper { /// /// Return the ID of the newly created conversation or -1 in case of failure Future createConversation(ConversationSettings settings) async { + final response = + await APIRequest(uri: "conversations/create", needLogin: true, args: { + "name": settings.hasName ? settings.name : "false", + "follow": settings.following ? "true" : "false", + "users": settings.members.join(",") + }).exec(); - final response = await APIRequest( - uri: "conversations/create", - needLogin: true, - args: { - "name" : settings.hasName ? settings.name : "false", - "follow" : settings.following ? "true" : "false", - "users": settings.members.join(",") - } - ).exec(); - - if(response.code != 200) return -1; + if (response.code != 200) return -1; return response.getObject()["conversationID"]; - } /// Download the list of conversations from the server @@ -156,6 +151,28 @@ class ConversationsHelper { ); } + /// Parse a list of messages given by the server + Future _parseConversationMessageFromServer( + int conversationID, APIResponse response) async { + if (response.code != 200) return null; + + // Parse the response of the server + ConversationMessagesList list = ConversationMessagesList(); + response.getArray().forEach((f) { + list.add( + _apiToConversationMessage( + conversationID: conversationID, + map: f, + ), + ); + }); + + // Save messages in the cache + _conversationMessagesDatabaseHelper.insertOrUpdateAll(list); + + return list; + } + /// Refresh the list of messages of a conversation /// /// Set [lastMessageID] to 0 to specify that we do not have any message of the @@ -172,24 +189,26 @@ class ConversationsHelper { "last_message_id": lastMessageID.toString() }).exec(); - if (response.code != 200) return null; + return await _parseConversationMessageFromServer(conversationID, response); + } - // Parse the response of the server - ConversationMessagesList list = ConversationMessagesList(); - response.getArray().forEach((f) { - list.add(ConversationMessage( - id: f["ID"], - conversationID: conversationID, - userID: f["ID_user"], - timeInsert: f["time_insert"], - message: f["message"], - imageURL: f["image_path"])); - }); + /// Get older messages for a given conversation from an online source + Future getOlderMessages({ + @required int conversationID, + @required int oldestMessagesID, + int limit = 15, + }) async { + // Perform the request online + final response = await APIRequest( + uri: "conversations/get_older_messages", + needLogin: true, + args: { + "conversationID": conversationID.toString(), + "oldest_message_id": oldestMessagesID.toString(), + "limit": limit.toString() + }).exec(); - // Save messages in the cache - _conversationMessagesDatabaseHelper.insertOrUpdateAll(list); - - return list; + return await _parseConversationMessageFromServer(conversationID, response); } /// Get new messages for a given conversation @@ -237,4 +256,19 @@ class ConversationsHelper { return SendMessageResult.SUCCESS; } + + /// Turn an API response into a ConversationMessage object + ConversationMessage _apiToConversationMessage({ + @required int conversationID, + @required Map map, + }) { + return ConversationMessage( + id: map["ID"], + conversationID: conversationID, + userID: map["ID_user"], + timeInsert: map["time_insert"], + message: map["message"], + imageURL: map["image_path"], + ); + } } diff --git a/lib/lists/conversation_messages_list.dart b/lib/lists/conversation_messages_list.dart index 98616ab..8eacd5b 100644 --- a/lib/lists/conversation_messages_list.dart +++ b/lib/lists/conversation_messages_list.dart @@ -40,4 +40,12 @@ class ConversationMessagesList extends ListBase { if (message.id > lastMessageID) lastMessageID = message.id; return lastMessageID; } + + /// Get the ID of the first message present in this list + int get firstMessageID { + int firstMessageID = this[0].id; + for (ConversationMessage message in this) + if (message.id < firstMessageID) firstMessageID = message.id; + return firstMessageID; + } } diff --git a/lib/ui/screens/conversation_screen.dart b/lib/ui/screens/conversation_screen.dart index 2272039..0287f7d 100644 --- a/lib/ui/screens/conversation_screen.dart +++ b/lib/ui/screens/conversation_screen.dart @@ -6,6 +6,7 @@ import 'package:comunic/lists/conversation_messages_list.dart'; import 'package:comunic/lists/users_list.dart'; import 'package:comunic/models/new_conversation_message.dart'; import 'package:comunic/ui/tiles/conversation_message_tile.dart'; +import 'package:comunic/ui/widgets/scroll_watcher.dart'; import 'package:comunic/utils/files_utils.dart'; import 'package:comunic/utils/intl_utils.dart'; import 'package:comunic/utils/list_utils.dart'; @@ -17,6 +18,7 @@ import 'package:flutter/material.dart'; /// @author Pierre HUBERT enum ErrorLevel { NONE, MINOR, MAJOR } +enum _OlderMessagesLevel { NONE, LOADING, NO_MORE_AVAILABLE } class ConversationScreen extends StatefulWidget { final int conversationID; @@ -42,6 +44,14 @@ class _ConversationScreenState extends State { bool _isSendingMessage = false; TextEditingController _textEditingController = TextEditingController(); Timer _refreshTime; + ScrollWatcher _scrollController; + _OlderMessagesLevel _loadingOlderMessages = _OlderMessagesLevel.NONE; + + @override + void initState() { + super.initState(); + _scrollController = ScrollWatcher(onReachBottom: _loadOlderMessages); + } @override void didChangeDependencies() { @@ -59,6 +69,10 @@ class _ConversationScreenState extends State { void _setSending(bool sending) => setState(() => _isSendingMessage = sending); + void _setLoadingOlderMessagesState(_OlderMessagesLevel state) => setState(() { + _loadingOlderMessages = state; + }); + /// Method called when an error occurred while loading messages void _errorLoading() { _setError(_messages == null ? ErrorLevel.MAJOR : ErrorLevel.MINOR); @@ -89,6 +103,47 @@ class _ConversationScreenState extends State { // anything (we wait for the online request) if (messages.length == 0 && !online) return; + await _applyNewMessages(messages); + } + + /// Get older messages + Future _loadOlderMessages() async { + if (_loadingOlderMessages != _OlderMessagesLevel.NONE || + _messages == null || + _messages.length == 0) return; + + // Let's start to load older messages + _setLoadingOlderMessagesState(_OlderMessagesLevel.LOADING); + + final messages = await _conversationsHelper.getOlderMessages( + conversationID: widget.conversationID, + oldestMessagesID: _messages.firstMessageID); + + // Mark as not loading anymore + _setLoadingOlderMessagesState(_OlderMessagesLevel.NONE); + + // Check for errors + if (messages == null) { + _errorLoading(); + return; + } + + // Check if there is no more unread messages + if (messages.length == 0) { + _setLoadingOlderMessagesState(_OlderMessagesLevel.NO_MORE_AVAILABLE); + return; + } + + // Apply the messages + _applyNewMessages(messages); + } + + /// Apply new messages [messages] must not be null + Future _applyNewMessages(ConversationMessagesList messages) async { + + // We ignore new messages once the area is no longer visible + if(!this.mounted) return; + //Then get information about users final usersToGet = findMissingFromList(_usersInfo.usersID, messages.getUsersID()); @@ -204,10 +259,19 @@ class _ConversationScreenState extends State { return buildErrorCard(tr("Could not load the list of messages!")); } + /// Widget shown when loading older messages + Widget _buildLoadingOlderMessage() { + return Container( + padding: EdgeInsets.all(8.0), + child: CircularProgressIndicator(), + ); + } + /// Messages list Widget _buildMessagesList() { return Expanded( child: ListView.builder( + controller: _scrollController, reverse: true, itemCount: _messages.length, itemBuilder: (c, i) { @@ -221,7 +285,7 @@ class _ConversationScreenState extends State { ); } - /// Send message from + /// Send message form Widget _buildSendMessageForm() { return new Container( margin: const EdgeInsets.symmetric(horizontal: 8.0), @@ -244,6 +308,7 @@ class _ConversationScreenState extends State { // Message area new Flexible( child: new TextField( + keyboardType: TextInputType.text, maxLines: null, maxLength: 200, maxLengthEnforced: true, @@ -302,10 +367,17 @@ class _ConversationScreenState extends State { Container( child: _error == ErrorLevel.MINOR ? _buildError() : null, ), + Container( + child: _loadingOlderMessages == _OlderMessagesLevel.LOADING + ? _buildLoadingOlderMessage() + : null, + ), _buildMessagesList(), Divider(), _buildSendMessageForm() ], ); } + + } diff --git a/lib/ui/widgets/scroll_watcher.dart b/lib/ui/widgets/scroll_watcher.dart new file mode 100644 index 0000000..42e2256 --- /dev/null +++ b/lib/ui/widgets/scroll_watcher.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; + +/// Scroll watcher +/// +/// @author Pierre HUBERT + +typedef OnReachBottomCallback = void Function(); + +class ScrollWatcher extends ScrollController { + + // Callbacks + OnReachBottomCallback onReachBottom; + + ScrollWatcher({this.onReachBottom}) { + addListener(_updatePosition); + } + + + + void _updatePosition() { + + // Refresh bottom position + if(position.pixels.floor() == position.maxScrollExtent.floor()) + onReachBottom(); + + } +} \ No newline at end of file