mirror of
https://gitlab.com/comunic/comunicmobile
synced 2024-12-26 21:08:52 +00:00
462 lines
14 KiB
Dart
462 lines
14 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:comunic/helpers/conversations_helper.dart';
|
|
import 'package:comunic/helpers/events_helper.dart';
|
|
import 'package:comunic/helpers/users_helper.dart';
|
|
import 'package:comunic/lists/conversation_messages_list.dart';
|
|
import 'package:comunic/lists/users_list.dart';
|
|
import 'package:comunic/models/conversation_message.dart';
|
|
import 'package:comunic/models/new_conversation_message.dart';
|
|
import 'package:comunic/ui/tiles/conversation_message_tile.dart';
|
|
import 'package:comunic/ui/widgets/safe_state.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';
|
|
import 'package:comunic/utils/ui_utils.dart';
|
|
import 'package:flutter/material.dart';
|
|
|
|
/// Conversation screen
|
|
///
|
|
/// @author Pierre HUBERT
|
|
|
|
enum ErrorLevel { NONE, MINOR, MAJOR }
|
|
enum _OlderMessagesLevel { NONE, LOADING, NO_MORE_AVAILABLE }
|
|
|
|
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 SafeState<ConversationScreen> {
|
|
//Helpers
|
|
final ConversationsHelper _conversationsHelper = ConversationsHelper();
|
|
final UsersHelper _usersHelper = UsersHelper();
|
|
|
|
// Class members
|
|
ConversationMessagesList _messages;
|
|
UsersList _usersInfo = UsersList();
|
|
ErrorLevel _error = ErrorLevel.NONE;
|
|
bool _isMessageValid = false;
|
|
bool _isSendingMessage = false;
|
|
TextEditingController _textEditingController = TextEditingController();
|
|
ScrollWatcher _scrollController;
|
|
_OlderMessagesLevel _loadingOlderMessages = _OlderMessagesLevel.NONE;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_init();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
super.dispose();
|
|
_deallocate();
|
|
}
|
|
|
|
void _setError(ErrorLevel err) => setState(() => _error = err);
|
|
|
|
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);
|
|
}
|
|
|
|
/// Load the first conversations
|
|
Future<void> _init() async {
|
|
_scrollController = ScrollWatcher(onReachBottom: _loadOlderMessages);
|
|
|
|
// Fetch latest messages
|
|
await _loadMessages(false);
|
|
await _loadMessages(true);
|
|
|
|
await _conversationsHelper
|
|
.registerConversationEvents(widget.conversationID);
|
|
|
|
this.listen<NewConversationMessageEvent>((ev) async {
|
|
if (ev.msg.conversationID == widget.conversationID) {
|
|
await _conversationsHelper.saveMessage(ev.msg);
|
|
await _applyNewMessages(ConversationMessagesList()..add(ev.msg));
|
|
}
|
|
});
|
|
|
|
this.listen<UpdatedConversationMessageEvent>((ev) async {
|
|
if (ev.msg.conversationID == widget.conversationID) {
|
|
await _conversationsHelper.saveMessage(ev.msg);
|
|
setState(() => _messages.replace(ev.msg));
|
|
}
|
|
});
|
|
|
|
this.listen<DeletedConversationMessageEvent>((ev) async {
|
|
if (ev.msg.conversationID == widget.conversationID) {
|
|
await _conversationsHelper.removeMessage(ev.msg.id);
|
|
setState(() => _messages.removeMsg(ev.msg.id));
|
|
}
|
|
});
|
|
}
|
|
|
|
/// Free resources when this conversation widget is no longer required
|
|
void _deallocate() {
|
|
_conversationsHelper.unregisterConversationEvents(widget.conversationID);
|
|
}
|
|
|
|
/// Load a list of messages
|
|
Future<void> _loadMessages(bool online) async {
|
|
if (!mounted) return;
|
|
|
|
//First, get the messages
|
|
final messages = await _conversationsHelper.getNewMessages(
|
|
conversationID: widget.conversationID,
|
|
lastMessageID: _messages == null ? 0 : _messages.lastMessageID,
|
|
online: online);
|
|
|
|
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)
|
|
if (messages.length == 0 && !online) return;
|
|
|
|
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;
|
|
|
|
//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);
|
|
|
|
//Reverse the order of the messages (if required)
|
|
if (messages.length > 0) {
|
|
_messages.sort();
|
|
final reverse = _messages.reversed;
|
|
_messages = ConversationMessagesList();
|
|
_messages.addAll(reverse);
|
|
}
|
|
});
|
|
|
|
// Remove previous errors
|
|
_setError(ErrorLevel.NONE);
|
|
}
|
|
|
|
/// Pick and send an image
|
|
Future<void> _sendImage(BuildContext context) async {
|
|
final image = await pickImage(context);
|
|
|
|
if (image == null) return null;
|
|
|
|
_submitMessage(
|
|
context,
|
|
NewConversationMessage(
|
|
conversationID: widget.conversationID,
|
|
message: null,
|
|
image: image,
|
|
),
|
|
);
|
|
|
|
// In case a message was already written in the input
|
|
_updatedText(_textEditingController.text);
|
|
}
|
|
|
|
/// Send a new text message
|
|
Future<void> _submitTextMessage(BuildContext context, String content) async {
|
|
if (await _submitMessage(
|
|
context,
|
|
NewConversationMessage(
|
|
conversationID: widget.conversationID,
|
|
message: content,
|
|
)) ==
|
|
SendMessageResult.SUCCESS) _clearSendMessageForm();
|
|
}
|
|
|
|
/// Submit a new message
|
|
Future<SendMessageResult> _submitMessage(
|
|
BuildContext context, NewConversationMessage message) async {
|
|
//Send the message
|
|
_setSending(true);
|
|
final result = await _conversationsHelper.sendMessage(message);
|
|
_setSending(false);
|
|
|
|
//Check the result of the operation
|
|
if (result != SendMessageResult.SUCCESS)
|
|
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),
|
|
),
|
|
);
|
|
|
|
return result;
|
|
}
|
|
|
|
void _updatedText(String text) {
|
|
setState(() {
|
|
_isMessageValid = text.length > 2;
|
|
});
|
|
}
|
|
|
|
/// Clear send message form
|
|
void _clearSendMessageForm() {
|
|
setState(() {
|
|
_textEditingController = TextEditingController();
|
|
_isMessageValid = false;
|
|
});
|
|
}
|
|
|
|
/// 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!"));
|
|
}
|
|
|
|
/// 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.")),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Messages list
|
|
Widget _buildMessagesList() {
|
|
return Expanded(
|
|
child: ListView.builder(
|
|
controller: _scrollController,
|
|
reverse: true,
|
|
itemCount: _messages.length,
|
|
itemBuilder: (c, i) {
|
|
return ConversationMessageTile(
|
|
message: _messages.elementAt(i),
|
|
userInfo: _usersInfo.getUser(_messages[i].userID),
|
|
isLastMessage: _isLastMessage(i),
|
|
isFirstMessage: _isFirstMessage(i),
|
|
onRequestMessageUpdate: _updateMessage,
|
|
onRequestMessageDelete: _deleteMessage,
|
|
);
|
|
}),
|
|
);
|
|
}
|
|
|
|
/// Send message form
|
|
Widget _buildSendMessageForm() {
|
|
return new Container(
|
|
margin: const EdgeInsets.symmetric(horizontal: 8.0),
|
|
child: new Row(
|
|
children: <Widget>[
|
|
// Image area
|
|
new Container(
|
|
margin: new EdgeInsets.symmetric(horizontal: 4.0),
|
|
child: new IconButton(
|
|
icon: new Icon(
|
|
Icons.photo_camera,
|
|
color: _isSendingMessage
|
|
? Theme.of(context).disabledColor
|
|
: Theme.of(context).accentColor,
|
|
),
|
|
onPressed: () => _sendImage(context),
|
|
),
|
|
),
|
|
|
|
// Message area
|
|
new Flexible(
|
|
child: new TextField(
|
|
keyboardType: TextInputType.text,
|
|
maxLines: null,
|
|
maxLength: 200,
|
|
maxLengthEnforced: true,
|
|
|
|
// Show max length only when there is some text already typed
|
|
buildCounter: smartInputCounterWidgetBuilder,
|
|
|
|
enabled: !_isSendingMessage,
|
|
controller: _textEditingController,
|
|
onChanged: _updatedText,
|
|
onSubmitted: _isMessageValid
|
|
? (s) => _submitTextMessage(context, s)
|
|
: null,
|
|
decoration: new InputDecoration.collapsed(
|
|
hintText: tr("Send a message"),
|
|
),
|
|
),
|
|
),
|
|
|
|
// 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
|
|
? () =>
|
|
_submitTextMessage(context, _textEditingController.text)
|
|
: null,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (_error == ErrorLevel.MAJOR) return _buildError();
|
|
|
|
if (_messages == null) return buildCenteredProgressBar();
|
|
|
|
return Column(
|
|
children: <Widget>[
|
|
Container(
|
|
child: _error == ErrorLevel.MINOR ? _buildError() : null,
|
|
),
|
|
Container(
|
|
child: _loadingOlderMessages == _OlderMessagesLevel.LOADING
|
|
? _buildLoadingOlderMessage()
|
|
: null,
|
|
),
|
|
_messages.length == 0 ? _buildNoMessagesNotice() : _buildMessagesList(),
|
|
Divider(),
|
|
_buildSendMessageForm()
|
|
],
|
|
);
|
|
}
|
|
|
|
/// 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.content,
|
|
hint: tr("New message"));
|
|
|
|
if (newContent == null) return;
|
|
|
|
if (!await _conversationsHelper.updateMessage(message.id, newContent)) {
|
|
showSimpleSnack(context, tr("Could not update message content!"));
|
|
return;
|
|
}
|
|
}
|
|
|
|
/// 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),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
if (choice == null || !choice) return;
|
|
|
|
// Execute the request
|
|
if (!await _conversationsHelper.deleteMessage(message.id))
|
|
showSimpleSnack(context, tr("Could not delete conversation message!"));
|
|
}
|
|
}
|