1
0
mirror of https://gitlab.com/comunic/comunicmobile synced 2024-12-29 14:28:51 +00:00
comunicmobile/lib/ui/screens/conversation_screen.dart

460 lines
14 KiB
Dart
Raw Normal View History

2019-04-27 07:52:16 +00:00
import 'dart:async';
2019-04-25 06:56:16 +00:00
import 'package:comunic/helpers/conversations_helper.dart';
import 'package:comunic/helpers/users_helper.dart';
import 'package:comunic/lists/conversation_messages_list.dart';
import 'package:comunic/lists/users_list.dart';
2019-05-04 06:58:14 +00:00
import 'package:comunic/models/conversation_message.dart';
2019-04-25 07:48:52 +00:00
import 'package:comunic/models/new_conversation_message.dart';
2019-04-25 06:56:16 +00:00
import 'package:comunic/ui/tiles/conversation_message_tile.dart';
2019-04-27 16:29:30 +00:00
import 'package:comunic/ui/widgets/scroll_watcher.dart';
2019-04-25 09:13:02 +00:00
import 'package:comunic/utils/files_utils.dart';
2019-04-25 06:56:16 +00:00
import 'package:comunic/utils/intl_utils.dart';
import 'package:comunic/utils/list_utils.dart';
import 'package:comunic/utils/ui_utils.dart';
import 'package:flutter/material.dart';
/// Conversation screen
///
/// @author Pierre HUBERT
enum ErrorLevel { NONE, MINOR, MAJOR }
2019-04-27 16:29:30 +00:00
enum _OlderMessagesLevel { NONE, LOADING, NO_MORE_AVAILABLE }
2019-04-25 06:56:16 +00:00
class ConversationScreen extends StatefulWidget {
final int conversationID;
const ConversationScreen({Key key, this.conversationID})
: assert(conversationID != null),
super(key: key);
@override
State<StatefulWidget> createState() => _ConversationScreenState();
}
class _ConversationScreenState extends State<ConversationScreen> {
2019-04-25 07:48:52 +00:00
//Helpers
2019-04-25 06:56:16 +00:00
final ConversationsHelper _conversationsHelper = ConversationsHelper();
final UsersHelper _usersHelper = UsersHelper();
2019-04-25 07:48:52 +00:00
// Class members
2019-04-25 06:56:16 +00:00
ConversationMessagesList _messages;
UsersList _usersInfo = UsersList();
ErrorLevel _error = ErrorLevel.NONE;
2019-04-25 07:48:52 +00:00
bool _isMessageValid = false;
bool _isSendingMessage = false;
TextEditingController _textEditingController = TextEditingController();
2019-04-27 07:52:16 +00:00
Timer _refreshTime;
2019-04-27 16:29:30 +00:00
ScrollWatcher _scrollController;
_OlderMessagesLevel _loadingOlderMessages = _OlderMessagesLevel.NONE;
@override
void initState() {
super.initState();
_scrollController = ScrollWatcher(onReachBottom: _loadOlderMessages);
}
2019-04-25 06:56:16 +00:00
@override
void didChangeDependencies() {
super.didChangeDependencies();
_initializeLoading();
2019-04-25 06:56:16 +00:00
}
2019-04-27 07:52:16 +00:00
@override
void deactivate() {
super.deactivate();
_refreshTime.cancel();
}
2019-04-25 06:56:16 +00:00
void _setError(ErrorLevel err) => setState(() => _error = err);
2019-04-25 07:48:52 +00:00
void _setSending(bool sending) => setState(() => _isSendingMessage = sending);
2019-04-27 16:29:30 +00:00
void _setLoadingOlderMessagesState(_OlderMessagesLevel state) => setState(() {
_loadingOlderMessages = state;
});
2019-04-25 06:56:16 +00:00
/// Method called when an error occurred while loading messages
void _errorLoading() {
_setError(_messages == null ? ErrorLevel.MAJOR : ErrorLevel.MINOR);
}
/// Load the first conversations
Future<void> _initializeLoading() async {
await _loadMessages(false);
await _loadMessages(true);
2019-04-27 07:52:16 +00:00
// Set a timer to regularly update conversation messages
if (_refreshTime == null || !_refreshTime.isActive)
_refreshTime = Timer.periodic(
Duration(milliseconds: 1500), (t) => _loadMessages(true));
}
2019-04-25 06:56:16 +00:00
/// Load a list of messages
Future<void> _loadMessages(bool online) async {
2019-04-25 06:56:16 +00:00
//First, get the messages
2019-04-27 06:51:58 +00:00
final messages = await _conversationsHelper.getNewMessages(
conversationID: widget.conversationID,
lastMessageID: _messages == null ? 0 : _messages.lastMessageID,
online: online);
2019-04-25 06:56:16 +00:00
if (messages == null) return _errorLoading();
// In case we are offline and we did not get any message we do not do
// anything (we wait for the online request)
2019-04-27 07:52:16 +00:00
if (messages.length == 0 && !online) return;
2019-04-27 16:29:30 +00:00
await _applyNewMessages(messages);
}
/// Get older messages
Future<void> _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<void> _applyNewMessages(ConversationMessagesList messages) async {
// We ignore new messages once the area is no longer visible
if (!this.mounted) return;
2019-04-27 16:29:30 +00:00
2019-04-25 06:56:16 +00:00
//Then get information about users
final usersToGet =
findMissingFromList(_usersInfo.usersID, messages.getUsersID());
final users = await _usersHelper.getUsersInfo(usersToGet);
if (users == null) _errorLoading();
// Save the new list of messages
setState(() {
_usersInfo.addAll(users);
if (_messages == null)
_messages = messages;
else
_messages.addAll(messages);
2019-04-25 07:48:52 +00:00
2019-04-27 07:52:16 +00:00
//Reverse the order of the messages (if required)
if (messages.length > 0) {
_messages.sort();
final reverse = _messages.reversed;
_messages = ConversationMessagesList();
_messages.addAll(reverse);
}
2019-04-25 07:48:52 +00:00
});
2019-04-27 07:52:16 +00:00
// Remove previous errors
_setError(ErrorLevel.NONE);
2019-04-25 07:48:52 +00:00
}
2019-04-25 09:13:02 +00:00
/// Pick and send an image
Future<void> _sendImage(BuildContext context) async {
final image = await pickImage(context);
2019-04-25 18:14:19 +00:00
if (image == null) return null;
_submitMessage(
context,
NewConversationMessage(
conversationID: widget.conversationID,
message: null,
image: image,
),
);
2019-04-25 18:22:53 +00:00
// In case a message was already written in the input
_updatedText(_textEditingController.text);
2019-04-25 18:14:19 +00:00
}
/// Send a new text message
Future<void> _submitTextMessage(BuildContext context, String content) async {
2019-04-25 18:22:53 +00:00
if (await _submitMessage(
context,
NewConversationMessage(
conversationID: widget.conversationID,
message: content,
)) ==
SendMessageResult.SUCCESS) _clearSendMessageForm();
2019-04-25 09:13:02 +00:00
}
2019-04-25 18:14:19 +00:00
/// Submit a new message
2019-04-25 18:22:53 +00:00
Future<SendMessageResult> _submitMessage(
2019-04-25 18:14:19 +00:00
BuildContext context, NewConversationMessage message) async {
2019-04-25 07:48:52 +00:00
//Send the message
_setSending(true);
2019-04-25 18:14:19 +00:00
final result = await _conversationsHelper.sendMessage(message);
2019-04-25 07:48:52 +00:00
_setSending(false);
//Check the result of the operation
2019-04-25 18:22:53 +00:00
if (result != SendMessageResult.SUCCESS)
2019-04-25 07:48:52 +00:00
Scaffold.of(context).showSnackBar(
SnackBar(
content: Text(
result == SendMessageResult.MESSAGE_REJECTED
? tr("Message rejected by the server!")
: tr("Could not send message!"),
),
duration: Duration(milliseconds: 500),
),
);
2019-04-25 18:22:53 +00:00
return result;
}
void _updatedText(String text) {
setState(() {
_isMessageValid = text.length > 4;
});
2019-04-25 07:48:52 +00:00
}
/// Clear send message form
void _clearSendMessageForm() {
setState(() {
_textEditingController = TextEditingController();
_isMessageValid = false;
2019-04-25 06:56:16 +00:00
});
}
2019-04-26 09:04:06 +00:00
/// Check if a message is the last message of a user or not
2019-04-26 06:58:18 +00:00
bool _isLastMessage(int index) {
return index == 0 ||
2019-04-26 09:04:06 +00:00
(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 ||
2019-04-27 06:51:58 +00:00
(index < _messages.length - 1 &&
_messages[index + 1].userID != _messages[index].userID);
2019-04-26 06:58:18 +00:00
}
2019-04-25 06:56:16 +00:00
/// Error handling
Widget _buildError() {
return buildErrorCard(tr("Could not load the list of messages!"));
}
2019-04-27 16:29:30 +00:00
/// Widget shown when loading older messages
Widget _buildLoadingOlderMessage() {
return Container(
padding: EdgeInsets.all(8.0),
child: CircularProgressIndicator(),
);
}
/// Notice shown when there is no messages to show
Widget _buildNoMessagesNotice() {
return Expanded(
child: Center(
child: Text(tr("There is no message yet in this converation.")),
),
);
}
2019-04-25 07:48:52 +00:00
/// Messages list
Widget _buildMessagesList() {
return Expanded(
child: ListView.builder(
2019-04-27 16:29:30 +00:00
controller: _scrollController,
2019-04-27 06:51:58 +00:00
reverse: true,
2019-04-25 07:48:52 +00:00
itemCount: _messages.length,
itemBuilder: (c, i) {
return ConversationMessageTile(
message: _messages.elementAt(i),
userInfo: _usersInfo.getUser(_messages[i].userID),
2019-04-26 06:58:18 +00:00
isLastMessage: _isLastMessage(i),
2019-04-26 09:04:06 +00:00
isFirstMessage: _isFirstMessage(i),
2019-05-04 08:24:38 +00:00
onRequestMessageUpdate: _updateMessage,
2019-05-04 06:58:14 +00:00
onRequestMessageDelete: _deleteMessage,
2019-04-25 07:48:52 +00:00
);
}),
);
}
2019-04-27 16:29:30 +00:00
/// Send message form
2019-04-25 07:48:52 +00:00
Widget _buildSendMessageForm() {
return new Container(
margin: const EdgeInsets.symmetric(horizontal: 8.0),
child: new Row(
children: <Widget>[
// Image area
2019-04-25 09:13:02 +00:00
new Container(
2019-04-25 18:14:19 +00:00
margin: new EdgeInsets.symmetric(horizontal: 4.0),
child: new IconButton(
2019-04-25 18:22:53 +00:00
icon: new Icon(
Icons.photo_camera,
color: _isSendingMessage
? Theme.of(context).disabledColor
: Theme.of(context).accentColor,
),
onPressed: () => _sendImage(context),
),
2019-04-25 18:14:19 +00:00
),
2019-04-25 07:48:52 +00:00
// Message area
new Flexible(
child: new TextField(
2019-04-27 16:29:30 +00:00
keyboardType: TextInputType.text,
2019-04-27 08:13:29 +00:00
maxLines: null,
maxLength: 200,
maxLengthEnforced: true,
// Show max length only when there is some text already typed
buildCounter: (
BuildContext context, {
@required int currentLength,
@required int maxLength,
@required bool isFocused,
}) =>
currentLength > 0
? Text("$currentLength/$maxLength")
: Container(),
2019-04-25 18:22:53 +00:00
enabled: !_isSendingMessage,
2019-04-25 07:48:52 +00:00
controller: _textEditingController,
2019-04-25 18:22:53 +00:00
onChanged: _updatedText,
2019-04-25 18:14:19 +00:00
onSubmitted: _isMessageValid
? (s) => _submitTextMessage(context, s)
: null,
2019-04-27 08:13:29 +00:00
decoration: new InputDecoration.collapsed(
hintText: tr("Send a message"),
),
2019-04-25 07:48:52 +00:00
),
),
// Send button
new Container(
margin: const EdgeInsets.symmetric(horizontal: 4.0),
child: new IconButton(
icon: new Icon(
Icons.send,
color: !_isSendingMessage && _isMessageValid
? Theme.of(context).accentColor
: Theme.of(context).disabledColor,
),
onPressed: !_isSendingMessage && _isMessageValid
2019-04-25 18:14:19 +00:00
? () =>
_submitTextMessage(context, _textEditingController.text)
2019-04-25 07:48:52 +00:00
: null,
),
),
],
),
);
}
2019-04-25 06:56:16 +00:00
@override
Widget build(BuildContext context) {
if (_error == ErrorLevel.MAJOR) return _buildError();
if (_messages == null) return buildCenteredProgressBar();
2019-04-25 07:48:52 +00:00
return Column(
children: <Widget>[
Container(
child: _error == ErrorLevel.MINOR ? _buildError() : null,
),
2019-04-27 16:29:30 +00:00
Container(
child: _loadingOlderMessages == _OlderMessagesLevel.LOADING
? _buildLoadingOlderMessage()
: null,
),
_messages.length == 0 ? _buildNoMessagesNotice() : _buildMessagesList(),
2019-04-25 07:48:52 +00:00
Divider(),
_buildSendMessageForm()
],
);
2019-04-25 06:56:16 +00:00
}
2019-05-04 06:58:14 +00:00
2019-05-04 08:24:38 +00:00
/// Request message content update
Future<void> _updateMessage(ConversationMessage message) async {
final newContent = await askUserString(
context: context,
title: tr("Update message"),
message: tr("Please enter new message content:"),
defaultValue: message.message,
hint: tr("New message"));
if (newContent == null) return;
if (!await _conversationsHelper.updateMessage(message.id, newContent)) {
showSimpleSnack(context, tr("Could not update message content!"));
return;
}
// Get the new version of the conversation message
final newMessage =
await _conversationsHelper.getSingleMessageFromCache(message.id);
setState(() {
final index = _messages.indexOf(message);
_messages.insert(index, newMessage);
_messages.removeAt(index + 1);
});
}
2019-05-04 06:58:14 +00:00
/// Request message deletion
Future<void> _deleteMessage(ConversationMessage message) async {
final choice = await showDialog<bool>(
context: context,
builder: (c) => AlertDialog(
title: Text(tr("Confirm deletion")),
content: Text(
tr("Do you really want to delete this message ? The operation can not be cancelled !"),
textAlign: TextAlign.justify,
),
actions: <Widget>[
FlatButton(
child: Text(
tr("Cancel").toUpperCase(),
),
onPressed: () => Navigator.pop(c, false),
),
FlatButton(
child: Text(
tr("Confirm").toUpperCase(),
style: TextStyle(color: Colors.red),
),
onPressed: () => Navigator.pop(c, true),
),
],
),
);
2019-05-04 08:24:38 +00:00
if (choice == null || !choice) return;
2019-05-04 06:58:14 +00:00
// Execute the request
2019-05-04 08:24:38 +00:00
if (!await _conversationsHelper.deleteMessage(message.id))
2019-05-04 06:58:14 +00:00
showSimpleSnack(context, tr("Could not delete conversation message!"));
// Remove the message from the list
setState(() {
_messages.remove(message);
});
}
2019-04-25 06:56:16 +00:00
}