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';
|
2020-04-19 11:58:24 +00:00
|
|
|
import 'package:comunic/helpers/events_helper.dart';
|
2021-03-12 15:37:21 +00:00
|
|
|
import 'package:comunic/helpers/server_config_helper.dart';
|
2019-04-25 06:56:16 +00:00
|
|
|
import 'package:comunic/helpers/users_helper.dart';
|
|
|
|
import 'package:comunic/lists/conversation_messages_list.dart';
|
|
|
|
import 'package:comunic/lists/users_list.dart';
|
2021-03-12 18:36:42 +00:00
|
|
|
import 'package:comunic/models/api_request.dart';
|
2021-03-10 23:23:11 +00:00
|
|
|
import 'package:comunic/models/conversation.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';
|
2021-03-12 17:54:15 +00:00
|
|
|
import 'package:comunic/ui/dialogs/pick_file_dialog.dart';
|
2021-03-11 16:27:20 +00:00
|
|
|
import 'package:comunic/ui/routes/main_route/main_route.dart';
|
2019-04-25 06:56:16 +00:00
|
|
|
import 'package:comunic/ui/tiles/conversation_message_tile.dart';
|
2021-03-10 18:08:18 +00:00
|
|
|
import 'package:comunic/ui/tiles/server_conversation_message_tile.dart';
|
2021-03-13 07:26:56 +00:00
|
|
|
import 'package:comunic/ui/widgets/account_image_widget.dart';
|
2020-04-19 11:42:47 +00:00
|
|
|
import 'package:comunic/ui/widgets/safe_state.dart';
|
2019-04-27 16:29:30 +00:00
|
|
|
import 'package:comunic/ui/widgets/scroll_watcher.dart';
|
2021-03-13 08:29:54 +00:00
|
|
|
import 'package:comunic/ui/widgets/user_writing_in_conv_notifier.dart';
|
2021-03-13 07:17:54 +00:00
|
|
|
import 'package:comunic/utils/date_utils.dart';
|
2021-03-12 18:36:42 +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';
|
2021-03-12 16:47:09 +00:00
|
|
|
import 'package:comunic/utils/log_utils.dart';
|
2019-04-25 06:56:16 +00:00
|
|
|
import 'package:comunic/utils/ui_utils.dart';
|
2021-03-12 18:36:42 +00:00
|
|
|
import 'package:comunic/utils/video_utils.dart';
|
2021-03-13 08:00:00 +00:00
|
|
|
import 'package:dio/dio.dart';
|
2021-03-12 21:18:32 +00:00
|
|
|
import 'package:emoji_picker/emoji_picker.dart';
|
2019-04-25 06:56:16 +00:00
|
|
|
import 'package:flutter/material.dart';
|
2021-03-12 18:36:42 +00:00
|
|
|
import 'package:mime/mime.dart';
|
2019-04-25 06:56:16 +00:00
|
|
|
|
|
|
|
/// 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();
|
|
|
|
}
|
|
|
|
|
2020-04-19 11:42:47 +00:00
|
|
|
class _ConversationScreenState extends SafeState<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
|
2021-03-10 23:23:11 +00:00
|
|
|
Conversation _conversation;
|
2019-04-25 06:56:16 +00:00
|
|
|
ConversationMessagesList _messages;
|
|
|
|
UsersList _usersInfo = UsersList();
|
|
|
|
ErrorLevel _error = ErrorLevel.NONE;
|
2021-03-12 21:18:32 +00:00
|
|
|
final _textFieldFocus = FocusNode();
|
|
|
|
|
|
|
|
bool _showEmojiPicker = false;
|
2021-03-12 16:47:09 +00:00
|
|
|
|
2019-04-25 07:48:52 +00:00
|
|
|
bool _isSendingMessage = false;
|
2021-03-12 21:18:32 +00:00
|
|
|
TextEditingController _textController = TextEditingController();
|
2019-04-27 16:29:30 +00:00
|
|
|
ScrollWatcher _scrollController;
|
|
|
|
_OlderMessagesLevel _loadingOlderMessages = _OlderMessagesLevel.NONE;
|
|
|
|
|
2021-03-13 08:09:26 +00:00
|
|
|
int _lastWritingEventSent = 0;
|
|
|
|
|
2021-03-13 08:00:00 +00:00
|
|
|
CancelToken _sendCancel;
|
|
|
|
double _sendProgress;
|
|
|
|
|
2021-03-12 21:18:32 +00:00
|
|
|
String get textMessage => _textController.text;
|
2021-03-12 16:47:09 +00:00
|
|
|
|
|
|
|
bool get _isMessageValid =>
|
|
|
|
textMessage.length >=
|
|
|
|
ServerConfigurationHelper.config.conversationsPolicy.minMessageLen &&
|
|
|
|
textMessage.length <
|
|
|
|
ServerConfigurationHelper.config.conversationsPolicy.maxMessageLen;
|
|
|
|
|
2021-03-12 21:18:32 +00:00
|
|
|
showKeyboard() => _textFieldFocus.requestFocus();
|
|
|
|
|
|
|
|
hideKeyboard() => _textFieldFocus.unfocus();
|
|
|
|
|
|
|
|
hideEmojiContainer() => setState(() => _showEmojiPicker = false);
|
|
|
|
|
|
|
|
showEmojiContainer() => setState(() => _showEmojiPicker = true);
|
|
|
|
|
2021-03-12 21:40:33 +00:00
|
|
|
// Colors definition
|
2021-03-13 07:17:54 +00:00
|
|
|
Color get _senderColor =>
|
|
|
|
_conversation.color ??
|
2021-03-13 07:41:54 +00:00
|
|
|
/*(darkTheme() ? Color(0xff2b343b) :*/ Colors.blue.shade700; //);
|
2021-03-13 07:17:54 +00:00
|
|
|
|
2021-03-12 21:40:33 +00:00
|
|
|
Color get _receiverColor =>
|
|
|
|
darkTheme() ? Color(0xff3a3d40) : Colors.grey.shade600;
|
|
|
|
|
|
|
|
Color get _greyColor => Color(0xff8f8f8f);
|
|
|
|
|
|
|
|
Color get _gradientColorStart =>
|
2021-03-12 22:09:40 +00:00
|
|
|
_conversation.color ??
|
|
|
|
(darkTheme() ? Color(0xff00b6f3) : Colors.blue.shade300);
|
2021-03-12 21:40:33 +00:00
|
|
|
|
|
|
|
Color get _gradientColorEnd =>
|
2021-03-12 21:45:55 +00:00
|
|
|
_conversation.color?.withOpacity(0.7) ??
|
2021-03-12 22:09:40 +00:00
|
|
|
(darkTheme() ? Color(0xff0184dc) : Colors.blueAccent.shade700);
|
2021-03-12 21:40:33 +00:00
|
|
|
|
|
|
|
Color get _separatorColor =>
|
|
|
|
darkTheme() ? Color(0xff272c35) : Color(0xffBEBEBE);
|
|
|
|
|
|
|
|
LinearGradient get _fabGradient => LinearGradient(
|
|
|
|
colors: [_gradientColorStart, _gradientColorEnd],
|
|
|
|
begin: Alignment.topLeft,
|
|
|
|
end: Alignment.bottomRight,
|
|
|
|
);
|
|
|
|
|
|
|
|
LinearGradient get _disabledGradient => LinearGradient(
|
|
|
|
colors: [_greyColor, _receiverColor],
|
|
|
|
begin: Alignment.topLeft,
|
|
|
|
end: Alignment.bottomRight,
|
|
|
|
);
|
|
|
|
|
2019-04-27 16:29:30 +00:00
|
|
|
@override
|
|
|
|
void initState() {
|
|
|
|
super.initState();
|
2020-04-19 11:42:47 +00:00
|
|
|
_init();
|
2019-04-27 16:29:30 +00:00
|
|
|
}
|
2019-04-25 06:56:16 +00:00
|
|
|
|
|
|
|
@override
|
2020-04-19 11:42:47 +00:00
|
|
|
void dispose() {
|
|
|
|
super.dispose();
|
|
|
|
_deallocate();
|
2019-04-27 07:52:16 +00:00
|
|
|
}
|
|
|
|
|
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
|
2021-03-10 16:54:41 +00:00
|
|
|
void _errorLoading() =>
|
|
|
|
_setError(_messages == null ? ErrorLevel.MAJOR : ErrorLevel.MINOR);
|
2019-04-25 06:56:16 +00:00
|
|
|
|
2019-04-27 07:24:29 +00:00
|
|
|
/// Load the first conversations
|
2020-04-19 11:42:47 +00:00
|
|
|
Future<void> _init() async {
|
|
|
|
_scrollController = ScrollWatcher(onReachBottom: _loadOlderMessages);
|
|
|
|
|
2021-03-10 23:23:11 +00:00
|
|
|
_conversation =
|
|
|
|
await ConversationsHelper().getSingle(widget.conversationID);
|
|
|
|
|
2020-04-19 11:42:47 +00:00
|
|
|
// Fetch latest messages
|
2019-04-27 07:24:29 +00:00
|
|
|
await _loadMessages(false);
|
|
|
|
await _loadMessages(true);
|
2019-04-27 07:52:16 +00:00
|
|
|
|
2020-04-19 11:42:47 +00:00
|
|
|
await _conversationsHelper
|
|
|
|
.registerConversationEvents(widget.conversationID);
|
2020-04-19 11:58:24 +00:00
|
|
|
|
|
|
|
this.listen<NewConversationMessageEvent>((ev) async {
|
2021-03-10 16:54:41 +00:00
|
|
|
if (ev.msg.convID == widget.conversationID) {
|
|
|
|
try {
|
|
|
|
await _conversationsHelper.saveMessage(ev.msg);
|
|
|
|
await _applyNewMessages(ConversationMessagesList()..add(ev.msg));
|
|
|
|
} catch (e, s) {
|
|
|
|
print("Failed to show new message! $e => $s");
|
|
|
|
_errorLoading();
|
|
|
|
}
|
2020-04-19 12:16:35 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
this.listen<UpdatedConversationMessageEvent>((ev) async {
|
2021-03-10 16:54:41 +00:00
|
|
|
if (ev.msg.convID == widget.conversationID) {
|
2020-04-19 12:16:35 +00:00
|
|
|
await _conversationsHelper.saveMessage(ev.msg);
|
|
|
|
setState(() => _messages.replace(ev.msg));
|
|
|
|
}
|
2020-04-19 11:58:24 +00:00
|
|
|
});
|
2020-04-19 12:29:01 +00:00
|
|
|
|
|
|
|
this.listen<DeletedConversationMessageEvent>((ev) async {
|
2021-03-10 16:54:41 +00:00
|
|
|
if (ev.msg.convID == widget.conversationID) {
|
|
|
|
await _conversationsHelper.removeMessage(ev.msg);
|
2020-04-19 12:29:01 +00:00
|
|
|
setState(() => _messages.removeMsg(ev.msg.id));
|
|
|
|
}
|
|
|
|
});
|
2020-04-19 11:42:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Free resources when this conversation widget is no longer required
|
|
|
|
void _deallocate() {
|
|
|
|
_conversationsHelper.unregisterConversationEvents(widget.conversationID);
|
2019-04-27 07:24:29 +00:00
|
|
|
}
|
|
|
|
|
2019-04-25 06:56:16 +00:00
|
|
|
/// Load a list of messages
|
2019-04-27 07:24:29 +00:00
|
|
|
Future<void> _loadMessages(bool online) async {
|
2019-11-02 08:18:15 +00:00
|
|
|
if (!mounted) return;
|
|
|
|
|
2021-03-10 16:54:41 +00:00
|
|
|
try {
|
|
|
|
//First, get the messages
|
|
|
|
final messages = await _conversationsHelper.getNewMessages(
|
2019-04-27 06:51:58 +00:00
|
|
|
conversationID: widget.conversationID,
|
2019-04-27 07:24:29 +00:00
|
|
|
lastMessageID: _messages == null ? 0 : _messages.lastMessageID,
|
2021-03-10 16:54:41 +00:00
|
|
|
online: online,
|
|
|
|
);
|
2019-04-25 06:56:16 +00:00
|
|
|
|
2021-03-10 16:54:41 +00:00
|
|
|
// 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;
|
2019-04-27 07:24:29 +00:00
|
|
|
|
2021-03-10 16:54:41 +00:00
|
|
|
await _applyNewMessages(messages);
|
|
|
|
} catch (e, s) {
|
2021-03-10 18:08:18 +00:00
|
|
|
debugPrint("Failed to load messages! $e => $s", wrapWidth: 4096);
|
2021-03-10 16:54:41 +00:00
|
|
|
_errorLoading();
|
|
|
|
}
|
2019-04-27 16:29:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Get older messages
|
|
|
|
Future<void> _loadOlderMessages() async {
|
|
|
|
if (_loadingOlderMessages != _OlderMessagesLevel.NONE ||
|
|
|
|
_messages == null ||
|
|
|
|
_messages.length == 0) return;
|
2021-03-10 16:54:41 +00:00
|
|
|
try {
|
|
|
|
// Let's start to load older messages
|
|
|
|
_setLoadingOlderMessagesState(_OlderMessagesLevel.LOADING);
|
2019-04-27 16:29:30 +00:00
|
|
|
|
2021-03-10 16:54:41 +00:00
|
|
|
final messages = await _conversationsHelper.getOlderMessages(
|
|
|
|
conversationID: widget.conversationID,
|
|
|
|
oldestMessagesID: _messages.firstMessageID);
|
2019-04-27 16:29:30 +00:00
|
|
|
|
2021-03-10 16:54:41 +00:00
|
|
|
// Mark as not loading anymore
|
|
|
|
_setLoadingOlderMessagesState(_OlderMessagesLevel.NONE);
|
2019-04-27 16:29:30 +00:00
|
|
|
|
2021-03-10 16:54:41 +00:00
|
|
|
// Check if there is no more unread messages
|
|
|
|
if (messages.length == 0) {
|
|
|
|
_setLoadingOlderMessagesState(_OlderMessagesLevel.NO_MORE_AVAILABLE);
|
|
|
|
return;
|
|
|
|
}
|
2019-04-27 16:29:30 +00:00
|
|
|
|
2021-03-10 16:54:41 +00:00
|
|
|
// Apply the messages
|
|
|
|
_applyNewMessages(messages);
|
|
|
|
} catch (e, s) {
|
|
|
|
print("Failed to load older messages! $e => $s");
|
2019-04-27 16:29:30 +00:00
|
|
|
_errorLoading();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Apply new messages [messages] must not be null
|
2021-03-10 16:54:41 +00:00
|
|
|
///
|
|
|
|
/// Throws in case of failure
|
2019-04-27 16:29:30 +00:00
|
|
|
Future<void> _applyNewMessages(ConversationMessagesList messages) async {
|
|
|
|
// We ignore new messages once the area is no longer visible
|
2019-04-27 16:32:07 +00:00
|
|
|
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 =
|
2021-03-10 18:08:18 +00:00
|
|
|
findMissingFromSet(_usersInfo.usersID.toSet(), messages.getUsersID());
|
2019-04-25 06:56:16 +00:00
|
|
|
|
2021-03-10 16:54:41 +00:00
|
|
|
final users = await _usersHelper.getList(usersToGet);
|
2019-04-25 06:56:16 +00:00
|
|
|
|
|
|
|
// 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
|
|
|
}
|
|
|
|
|
2021-03-12 17:54:15 +00:00
|
|
|
/// Send a file message
|
|
|
|
Future<void> _sendFileMessage() async {
|
2021-03-12 16:47:09 +00:00
|
|
|
try {
|
2021-03-12 17:54:15 +00:00
|
|
|
final file = await showPickFileDialog(
|
|
|
|
context: context,
|
|
|
|
maxFileSize: srvConfig.conversationsPolicy.filesMaxSize,
|
|
|
|
allowedMimeTypes: srvConfig.conversationsPolicy.allowedFilesType,
|
2021-03-12 18:36:42 +00:00
|
|
|
imageMaxWidth: srvConfig.conversationsPolicy.maxMessageImageWidth,
|
|
|
|
imageMaxHeight: srvConfig.conversationsPolicy.maxMessageImageHeight,
|
2021-03-12 17:54:15 +00:00
|
|
|
);
|
2019-04-25 18:14:19 +00:00
|
|
|
|
2021-03-12 17:54:15 +00:00
|
|
|
if (file == null) return;
|
2021-03-12 16:47:09 +00:00
|
|
|
|
2021-03-12 18:36:42 +00:00
|
|
|
BytesFile thumbnail;
|
|
|
|
|
|
|
|
if (isVideo(lookupMimeType(file.filename)))
|
2021-03-12 18:37:13 +00:00
|
|
|
thumbnail = await generateVideoThumbnail(
|
|
|
|
videoFile: file,
|
|
|
|
maxWidth: srvConfig.conversationsPolicy.maxThumbnailWidth,
|
|
|
|
);
|
2021-03-12 18:36:42 +00:00
|
|
|
|
2021-03-13 08:00:00 +00:00
|
|
|
_sendCancel = CancelToken();
|
|
|
|
final progressCb =
|
|
|
|
(count, total) => setState(() => _sendProgress = count / total);
|
|
|
|
final res = await ConversationsHelper().sendMessage(
|
2021-03-12 17:54:15 +00:00
|
|
|
NewConversationMessage(
|
2021-03-12 18:36:42 +00:00
|
|
|
conversationID: widget.conversationID,
|
|
|
|
message: null,
|
|
|
|
file: file,
|
|
|
|
thumbnail: thumbnail),
|
2021-03-13 08:00:00 +00:00
|
|
|
sendProgress: progressCb,
|
|
|
|
cancelToken: _sendCancel,
|
2021-03-12 17:54:15 +00:00
|
|
|
);
|
2021-03-13 08:00:00 +00:00
|
|
|
assert(res == SendMessageResult.SUCCESS);
|
2021-03-12 16:47:09 +00:00
|
|
|
} catch (e, s) {
|
|
|
|
logError(e, s);
|
2021-03-12 17:54:15 +00:00
|
|
|
showSimpleSnack(context, tr("Failed to send a file!"));
|
2021-03-12 16:47:09 +00:00
|
|
|
}
|
2021-03-13 08:00:00 +00:00
|
|
|
|
|
|
|
setState(() {
|
|
|
|
_sendCancel = null;
|
|
|
|
_sendProgress = null;
|
|
|
|
});
|
2021-03-12 16:47:09 +00:00
|
|
|
}
|
2019-04-25 18:14:19 +00:00
|
|
|
|
|
|
|
/// Send a new text message
|
2021-03-12 17:54:15 +00:00
|
|
|
Future<void> _submitTextMessage() async {
|
|
|
|
if (await _submitMessage(NewConversationMessage(
|
|
|
|
conversationID: widget.conversationID,
|
|
|
|
message: textMessage,
|
|
|
|
)) ==
|
2019-04-25 18:22:53 +00:00
|
|
|
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(
|
2021-03-12 17:54:15 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2019-04-25 07:48:52 +00:00
|
|
|
/// Clear send message form
|
|
|
|
void _clearSendMessageForm() {
|
2021-03-12 21:18:32 +00:00
|
|
|
setState(() => _textController = TextEditingController());
|
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(),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2019-04-27 16:32:07 +00:00
|
|
|
/// 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(
|
2021-03-13 07:17:54 +00:00
|
|
|
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: <Widget>[
|
|
|
|
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(
|
2021-03-13 07:26:56 +00:00
|
|
|
margin: EdgeInsets.only(
|
|
|
|
top: previousMessage?.isOwner == true ? 0 : 12, right: 5),
|
2021-03-13 07:17:54 +00:00
|
|
|
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),
|
|
|
|
),
|
2019-04-25 07:48:52 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-03-13 07:17:54 +00:00
|
|
|
Widget _buildReceiverLayout(
|
|
|
|
ConversationMessage message, ConversationMessage previousMessage) {
|
|
|
|
final messageRadius = Radius.circular(10);
|
|
|
|
|
2021-03-13 07:26:56 +00:00
|
|
|
return Row(children: [
|
|
|
|
SizedBox(width: 5),
|
|
|
|
AccountImageWidget(
|
|
|
|
user: _usersInfo.getUser(message.userID),
|
2021-03-13 07:17:54 +00:00
|
|
|
),
|
2021-03-13 07:26:56 +00:00
|
|
|
SizedBox(width: 5),
|
|
|
|
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),
|
|
|
|
),
|
2021-03-13 07:17:54 +00:00
|
|
|
),
|
2021-03-13 07:26:56 +00:00
|
|
|
]);
|
2021-03-13 07:17:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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),
|
|
|
|
));
|
|
|
|
|
2021-03-12 21:18:32 +00:00
|
|
|
/// Send new message form
|
|
|
|
Widget _buildSendMessageForm() => Container(
|
2021-03-12 21:40:33 +00:00
|
|
|
padding: EdgeInsets.fromLTRB(10, 5, 10, 5),
|
2021-03-12 21:18:32 +00:00
|
|
|
child: Row(
|
|
|
|
children: <Widget>[
|
|
|
|
GestureDetector(
|
|
|
|
onTap: !_isSendingMessage ? _sendFileMessage : null,
|
|
|
|
child: Container(
|
2021-03-12 21:40:33 +00:00
|
|
|
padding: EdgeInsets.all(6),
|
2021-03-12 21:18:32 +00:00
|
|
|
decoration: BoxDecoration(
|
|
|
|
gradient:
|
|
|
|
_isSendingMessage ? _disabledGradient : _fabGradient,
|
|
|
|
shape: BoxShape.circle,
|
|
|
|
),
|
|
|
|
child: Icon(
|
|
|
|
Icons.add,
|
2021-03-13 08:10:55 +00:00
|
|
|
color: Colors.white,
|
2021-03-12 21:18:32 +00:00
|
|
|
),
|
2019-04-25 18:22:53 +00:00
|
|
|
),
|
|
|
|
),
|
2021-03-12 21:18:32 +00:00
|
|
|
SizedBox(width: 5),
|
|
|
|
Expanded(
|
|
|
|
child: Stack(
|
|
|
|
alignment: Alignment.centerRight,
|
|
|
|
children: [
|
|
|
|
TextField(
|
|
|
|
enabled: !_isSendingMessage,
|
|
|
|
maxLines: 10,
|
|
|
|
minLines: 1,
|
|
|
|
controller: _textController,
|
|
|
|
focusNode: _textFieldFocus,
|
|
|
|
onTap: () => hideEmojiContainer(),
|
|
|
|
textInputAction: TextInputAction.send,
|
|
|
|
onSubmitted: (s) => _submitTextMessage(),
|
|
|
|
style: TextStyle(
|
|
|
|
color: darkTheme() ? Colors.white : Colors.black,
|
|
|
|
),
|
2021-03-13 08:09:26 +00:00
|
|
|
onChanged: (s) {
|
|
|
|
_sendWritingEvent();
|
|
|
|
setState(() {});
|
|
|
|
},
|
2021-03-12 21:18:32 +00:00
|
|
|
decoration: InputDecoration(
|
|
|
|
hintText: tr("New message..."),
|
|
|
|
hintStyle: TextStyle(
|
|
|
|
color: _greyColor,
|
|
|
|
),
|
|
|
|
border: OutlineInputBorder(
|
|
|
|
borderRadius: const BorderRadius.all(
|
|
|
|
const Radius.circular(50.0),
|
|
|
|
),
|
|
|
|
borderSide: BorderSide.none),
|
2021-03-12 21:40:33 +00:00
|
|
|
contentPadding: EdgeInsets.fromLTRB(20, 8, 32, 8),
|
2021-03-12 21:18:32 +00:00
|
|
|
filled: true,
|
|
|
|
fillColor: _separatorColor,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
IconButton(
|
|
|
|
splashColor: Colors.transparent,
|
|
|
|
highlightColor: Colors.transparent,
|
|
|
|
onPressed: () {
|
|
|
|
if (!_showEmojiPicker) {
|
|
|
|
// keyboard is visible
|
|
|
|
hideKeyboard();
|
|
|
|
Future.delayed(Duration(milliseconds: 100),
|
|
|
|
() => showEmojiContainer());
|
|
|
|
} else {
|
|
|
|
//keyboard is hidden
|
|
|
|
showKeyboard();
|
|
|
|
hideEmojiContainer();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
icon: Icon(
|
|
|
|
Icons.face,
|
2021-03-12 21:45:55 +00:00
|
|
|
color: _showEmojiPicker
|
|
|
|
? (_conversation.color ?? Colors.blue)
|
|
|
|
: null,
|
2021-03-12 21:18:32 +00:00
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
2019-04-27 08:13:29 +00:00
|
|
|
),
|
2019-04-25 07:48:52 +00:00
|
|
|
),
|
2021-03-12 21:40:33 +00:00
|
|
|
SizedBox(width: 5),
|
|
|
|
GestureDetector(
|
|
|
|
onTap: _isMessageValid ? _submitTextMessage : null,
|
|
|
|
child: Container(
|
|
|
|
padding: EdgeInsets.all(8),
|
2021-03-12 21:18:32 +00:00
|
|
|
decoration: BoxDecoration(
|
2021-03-12 21:40:33 +00:00
|
|
|
gradient: !_isMessageValid ? _disabledGradient : _fabGradient,
|
2021-03-12 21:18:32 +00:00
|
|
|
shape: BoxShape.circle,
|
|
|
|
),
|
2021-03-12 21:40:33 +00:00
|
|
|
child: Icon(
|
|
|
|
Icons.send,
|
2021-03-13 08:10:55 +00:00
|
|
|
color: Colors.white,
|
2021-03-12 21:40:33 +00:00
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
2021-03-12 21:18:32 +00:00
|
|
|
],
|
|
|
|
),
|
|
|
|
);
|
2019-04-25 07:48:52 +00:00
|
|
|
|
2021-03-12 21:18:32 +00:00
|
|
|
Widget _buildEmojiContainer() => EmojiPicker(
|
2021-03-12 22:09:40 +00:00
|
|
|
bgColor: _conversation.color ?? Colors.blue.shade900,
|
|
|
|
indicatorColor: _conversation.color ?? Colors.blue.shade900,
|
2021-03-12 21:18:32 +00:00
|
|
|
rows: 3,
|
|
|
|
columns: 7,
|
|
|
|
onEmojiSelected: (emoji, category) {
|
|
|
|
_textController.text = _textController.text + emoji.emoji;
|
|
|
|
},
|
|
|
|
recommendKeywords: ["face", "happy", "party", "sad"],
|
|
|
|
numRecommended: 50,
|
|
|
|
);
|
2019-04-25 07:48:52 +00:00
|
|
|
|
2021-03-13 08:00:00 +00:00
|
|
|
Widget _buildSendingWidget() => Container(
|
|
|
|
height: 68,
|
|
|
|
color: _senderColor,
|
|
|
|
child: Row(
|
|
|
|
children: <Widget>[
|
|
|
|
Spacer(flex: 1),
|
|
|
|
Flexible(
|
|
|
|
child: LinearProgressIndicator(value: _sendProgress),
|
|
|
|
flex: 5,
|
|
|
|
),
|
|
|
|
Spacer(flex: 1),
|
|
|
|
Text("${(_sendProgress * 100).toInt()}%"),
|
|
|
|
Spacer(flex: 1),
|
|
|
|
OutlineButton(
|
|
|
|
onPressed: () => _sendCancel.cancel(),
|
|
|
|
child: Text(tr("Cancel").toUpperCase()),
|
|
|
|
),
|
|
|
|
Spacer(flex: 1),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
);
|
|
|
|
|
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>[
|
2019-04-27 07:24:29 +00:00
|
|
|
Container(
|
|
|
|
child: _error == ErrorLevel.MINOR ? _buildError() : null,
|
|
|
|
),
|
2019-04-27 16:29:30 +00:00
|
|
|
Container(
|
|
|
|
child: _loadingOlderMessages == _OlderMessagesLevel.LOADING
|
|
|
|
? _buildLoadingOlderMessage()
|
|
|
|
: null,
|
|
|
|
),
|
2019-04-27 16:32:07 +00:00
|
|
|
_messages.length == 0 ? _buildNoMessagesNotice() : _buildMessagesList(),
|
2021-03-13 08:29:54 +00:00
|
|
|
UserWritingInConvNotifier(convID: _conversation.id),
|
2021-03-13 08:00:00 +00:00
|
|
|
_sendCancel != null ? _buildSendingWidget() : _buildSendMessageForm(),
|
2021-03-12 21:18:32 +00:00
|
|
|
_showEmojiPicker ? _buildEmojiContainer() : Container(),
|
2019-04-25 07:48:52 +00:00
|
|
|
],
|
|
|
|
);
|
2019-04-25 06:56:16 +00:00
|
|
|
}
|
2019-05-04 06:58:14 +00:00
|
|
|
|
2021-03-13 08:09:26 +00:00
|
|
|
void _sendWritingEvent() async {
|
|
|
|
try {
|
|
|
|
if (textMessage.isEmpty) return;
|
|
|
|
final t = time();
|
|
|
|
|
|
|
|
if (t - _lastWritingEventSent <
|
|
|
|
srvConfig.conversationsPolicy.writingEventInterval) return;
|
|
|
|
|
|
|
|
_lastWritingEventSent = t;
|
|
|
|
await ConversationsHelper.sendWritingEvent(_conversation.id);
|
|
|
|
} catch (e, s) {
|
|
|
|
logError(e, s);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-11 16:27:20 +00:00
|
|
|
/// Request message statistics
|
|
|
|
void _requestMessageStats(ConversationMessage message) async {
|
|
|
|
MainController.of(context)
|
|
|
|
.openConversationMessageStats(_conversation, message);
|
|
|
|
}
|
|
|
|
|
2019-05-04 08:24:38 +00:00
|
|
|
/// Request message content update
|
|
|
|
Future<void> _updateMessage(ConversationMessage message) async {
|
|
|
|
final newContent = await askUserString(
|
2021-03-12 15:37:21 +00:00
|
|
|
context: context,
|
|
|
|
title: tr("Update message"),
|
|
|
|
message: tr("Please enter new message content:"),
|
|
|
|
defaultValue: message.message.content,
|
|
|
|
hint: tr("New message"),
|
|
|
|
minLength:
|
|
|
|
ServerConfigurationHelper.config.conversationsPolicy.minMessageLen,
|
|
|
|
maxLength:
|
|
|
|
ServerConfigurationHelper.config.conversationsPolicy.maxMessageLen,
|
|
|
|
);
|
2019-05-04 08:24:38 +00:00
|
|
|
|
|
|
|
if (newContent == null) return;
|
|
|
|
|
|
|
|
if (!await _conversationsHelper.updateMessage(message.id, newContent)) {
|
|
|
|
showSimpleSnack(context, tr("Could not update message content!"));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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(
|
2019-11-02 08:18:15 +00:00
|
|
|
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(),
|
2019-05-04 06:58:14 +00:00
|
|
|
),
|
2019-11-02 08:18:15 +00:00
|
|
|
onPressed: () => Navigator.pop(c, false),
|
2019-05-04 06:58:14 +00:00
|
|
|
),
|
2019-11-02 08:18:15 +00:00
|
|
|
FlatButton(
|
|
|
|
child: Text(
|
|
|
|
tr("Confirm").toUpperCase(),
|
|
|
|
style: TextStyle(color: Colors.red),
|
|
|
|
),
|
|
|
|
onPressed: () => Navigator.pop(c, true),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
),
|
2019-05-04 06:58:14 +00:00
|
|
|
);
|
|
|
|
|
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!"));
|
|
|
|
}
|
2019-04-25 06:56:16 +00:00
|
|
|
}
|