diff --git a/lib/core/viewmodels/main_page/chat/chat_inside_view_model.dart b/lib/core/viewmodels/main_page/chat/chat_inside_view_model.dart index 1007056e..4e48272c 100644 --- a/lib/core/viewmodels/main_page/chat/chat_inside_view_model.dart +++ b/lib/core/viewmodels/main_page/chat/chat_inside_view_model.dart @@ -11,6 +11,7 @@ import 'package:unn_mobile/core/misc/date_time_utilities/date_time_extensions.da import 'package:unn_mobile/core/misc/objects_with_pagination.dart'; import 'package:unn_mobile/core/misc/user/current_user_sync_storage.dart'; import 'package:unn_mobile/core/models/common/file_data.dart'; +import 'package:unn_mobile/core/models/dialog/base_dialog_info.dart'; import 'package:unn_mobile/core/models/dialog/dialog.dart'; import 'package:unn_mobile/core/models/dialog/message/enum/message_state.dart'; import 'package:unn_mobile/core/models/dialog/message/message.dart'; @@ -25,7 +26,9 @@ class ChatInsideViewModel extends BaseViewModel { ChatScreenViewModel? _dialogsViewModel; - Dialog? _dialog; + BaseDialogInfo? _dialog; + + int? chatId; bool _hasError = false; @@ -55,7 +58,10 @@ class ChatInsideViewModel extends BaseViewModel { ); int? get currentUserId => _currentUserSyncStorage.currentUserData?.bitrixId; - Dialog? get dialog => _dialog; + int get unreadMessagesCount => + (_dialog is Dialog) ? (_dialog! as Dialog).unreadMessagesCount : 0; + + BaseDialogInfo? get dialog => _dialog; bool get hasError => _hasError; bool get hasMessagesAfter => _hasMessagesAfter; @@ -71,21 +77,38 @@ class ChatInsideViewModel extends BaseViewModel { notifyListeners(); } + Future?> getMessagesByDialogId( + String dialogId, + ) async { + final messages = + await _messagesAggregator.fetchByDialogId(dialogId: dialogId); + if (messages == null) { + return null; + } + if (messages is PaginatedResultWithChatId) { + chatId = messages.chatId; + } + return messages; + } + FutureOr getNewMessages() async { if (_dialog == null) { return; } final messages = await tryLoginAndRetrieveData>( - () => _messagesAggregator.fetchByChatId(chatId: _dialog!.chatId), + () => chatId == null + ? getMessagesByDialogId(_dialog!.dialogId.stringValue) + : _messagesAggregator.fetchByChatId(chatId: chatId!), () => null, ); if (messages == null) { _hasError = true; return; } - await _readMessages(_dialog!.chatId, messages.items); - + if (chatId != null) { + await _readMessages(chatId!, messages.items); + } final messagesToRemove = messages.items.length - messages.items.reversed .takeWhile( @@ -97,8 +120,7 @@ class ChatInsideViewModel extends BaseViewModel { if (refreshLoopRunning && !refreshLoopStopFlag) { await _dialogsViewModel!.init(); - _dialog = _dialogsViewModel!.dialogs - .firstWhere((d) => d.chatId == _dialog!.chatId); + fetchDialogFromRoot(chatId); _dialogsViewModel!.notifyListeners(); } @@ -114,7 +136,14 @@ class ChatInsideViewModel extends BaseViewModel { ..addAll(_partitionMessages(_unpartitionedMessages)); } - FutureOr init(int chatId) async { + void fetchDialogFromRoot(int? chatId) { + _dialog = _dialogsViewModel!.dialogs.cast().firstWhere( + (d) => d is Dialog && d.chatId == chatId, + orElse: () => _dialogsViewModel!.storedDialogInfo!, + ); + } + + FutureOr init(int? chatId) async { _isInitializing = true; try { await busyCallAsync(() => _init(chatId)); @@ -135,17 +164,21 @@ class ChatInsideViewModel extends BaseViewModel { } final messages = await tryLoginAndRetrieveData?>( - () => _messagesAggregator.fetchByChatId( - chatId: _dialog!.chatId, - lastMessageId: _unpartitionedMessages.last.messageId, - ), + () => chatId == null + ? getMessagesByDialogId(_dialog!.dialogId.stringValue) + : _messagesAggregator.fetchByChatId( + chatId: chatId!, + lastMessageId: _unpartitionedMessages.last.messageId, + ), () => null, ); if (messages == null) { _hasError = true; return; } - await _readMessages(_dialog!.chatId, messages.items); + if (chatId != null) { + await _readMessages(chatId!, messages.items); + } _unpartitionedMessages.addAll(messages.items); _messages ..clear() @@ -167,7 +200,6 @@ class ChatInsideViewModel extends BaseViewModel { await Future.delayed(const Duration(seconds: 5)); if (!_isInitializing) { await getNewMessages(); - notifyListeners(); } await refreshLoop(checkStartConditions: false); @@ -176,7 +208,7 @@ class ChatInsideViewModel extends BaseViewModel { FutureOr sendFiles(Iterable uris, {String? text}) => _sendMessageWrapper>( () => _messagesAggregator.sendFiles( - chatId: _dialog!.chatId, + chatId: chatId!, files: [for (final uri in uris) File(uri)], text: text, ), @@ -196,26 +228,29 @@ class ChatInsideViewModel extends BaseViewModel { ), ); - FutureOr _init(int chatId) async { + FutureOr _init(int? chatId) async { _hasError = false; _hasMessagesBefore = false; _hasMessagesAfter = false; + this.chatId = chatId; _messages.clear(); _unpartitionedMessages.clear(); _dialogsViewModel = _routesViewModelFactory.getViewModelByType(); - _dialog = _dialogsViewModel!.dialogs.firstWhere((d) => d.chatId == chatId); - + fetchDialogFromRoot(chatId); final messages = await tryLoginAndRetrieveData>( - () => _messagesAggregator.fetchByChatId(chatId: chatId), + () => this.chatId == null + ? getMessagesByDialogId(_dialog!.dialogId.stringValue) + : _messagesAggregator.fetchByChatId(chatId: this.chatId!), () => null, ); if (messages == null) { _hasError = true; return; } - await _readMessages(chatId, messages.items); - + if (this.chatId != null) { + await _readMessages(this.chatId!, messages.items); + } _unpartitionedMessages.addAll(messages.items.reversed); _messages.addAll(_partitionMessages(messages.items.reversed)); _hasMessagesBefore = messages.hasPreviousPage; @@ -226,7 +261,6 @@ class ChatInsideViewModel extends BaseViewModel { List>> _partitionMessages(Iterable messages) { const maxTimeDifference = 5; - final unreadMessagesCount = _dialog?.unreadMessagesCount ?? 0; final List>> partitions = []; for (final (index, message) in messages.indexed) { final lastDatePartition = partitions.lastOrNull; @@ -282,7 +316,9 @@ class ChatInsideViewModel extends BaseViewModel { if (_dialog == null) { return false; } - + if (chatId == null) { + return false; + } final result = await tryLoginAndRetrieveData( sendFunction, () => null, diff --git a/lib/core/viewmodels/main_page/chat/chat_screen_view_model.dart b/lib/core/viewmodels/main_page/chat/chat_screen_view_model.dart index 5c7aeaf9..ed7e7856 100644 --- a/lib/core/viewmodels/main_page/chat/chat_screen_view_model.dart +++ b/lib/core/viewmodels/main_page/chat/chat_screen_view_model.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'package:unn_mobile/core/misc/authorisation/try_login_and_retrieve_data.dart'; import 'package:unn_mobile/core/misc/objects_with_pagination.dart'; import 'package:unn_mobile/core/misc/user/current_user_sync_storage.dart'; +import 'package:unn_mobile/core/models/dialog/base_dialog_info.dart'; import 'package:unn_mobile/core/models/dialog/dialog.dart'; import 'package:unn_mobile/core/models/dialog/dialog_query_parameter.dart'; import 'package:unn_mobile/core/services/interfaces/dialog/dialog_service.dart'; @@ -21,6 +22,8 @@ class ChatScreenViewModel extends BaseViewModel { final List _dialogs = []; + BaseDialogInfo? storedDialogInfo; + int? _currentUserId; bool _hasError = false; diff --git a/lib/ui/views/main_page/chat/chat.dart b/lib/ui/views/main_page/chat/chat.dart index 046a2a1e..728e27b8 100644 --- a/lib/ui/views/main_page/chat/chat.dart +++ b/lib/ui/views/main_page/chat/chat.dart @@ -10,6 +10,8 @@ import 'package:unn_mobile/core/constants/date_pattern.dart'; import 'package:unn_mobile/core/misc/date_time_utilities/date_time_extensions.dart'; import 'package:unn_mobile/core/misc/user/user_functions.dart'; import 'package:unn_mobile/core/models/dialog/dialog.dart' as d; +import 'package:unn_mobile/core/models/dialog/preview_dialog.dart'; +import 'package:unn_mobile/core/services/interfaces/dialog/dialog_search_service.dart'; import 'package:unn_mobile/core/viewmodels/factories/main_page_routes_view_models_factory.dart'; import 'package:unn_mobile/core/viewmodels/main_page/chat/chat_screen_view_model.dart'; import 'package:unn_mobile/ui/views/base_view.dart'; @@ -35,12 +37,45 @@ class _ChatScreenViewState extends State { .getViewModelByRouteIndex( widget.bottomRouteIndex!, ); - return BaseView( builder: (context, model, child) => Scaffold( appBar: AppBar( title: const Text('Сообщения'), leading: getSubpageLeading(widget.bottomRouteIndex), + actions: [ + if (!model.isBusy) + SearchAnchor( + builder: (context, controller) => IconButton( + onPressed: () { + controller.openView(); + }, + icon: const Icon(Icons.search), + ), + suggestionsBuilder: (context, controller) async { + final suggService = + Injector.appInstance.get(); + if (controller.text.length < 3) { + final history = await suggService.getHistory(); + return history + ?.where((e) => e.title.contains(controller.text)) + .map( + (e) => + searchItemTile(context, e, controller, model), + ) ?? + []; + } + final sugg = await suggService.search(controller.text); + final data = sugg + ?.map( + (e) => + searchItemTile(context, e, controller, model), + ) + .toList() ?? + []; + return data; + }, + ), + ], ), body: Builder( builder: (context) { @@ -106,6 +141,54 @@ class _ChatScreenViewState extends State { onModelReady: (model) => model.init(), ); } + + Widget searchItemTile( + BuildContext context, + PreviewDialog dialog, + SearchController controller, + ChatScreenViewModel model, + ) { + final theme = Theme.of(context); + + return ListTile( + leading: CircleAvatar( + radius: MediaQuery.of(context).textScaler.scale(26.0), + foregroundImage: dialog.avatarUrl.isNotEmpty + ? CachedNetworkImageProvider(dialog.avatarUrl) + : null, + child: dialog.avatarUrl.isEmpty + ? FittedBox( + fit: BoxFit.cover, + child: Padding( + padding: const EdgeInsets.all(4.0), + child: Text( + generateInitials( + dialog.title.split(' '), + ), + style: theme.textTheme.headlineSmall!.copyWith( + color: theme.colorScheme.onSurface, + ), + ), + ), + ) + : null, + ), + title: Text( + dialog.title, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.titleMedium, + ), + enableFeedback: true, + visualDensity: VisualDensity.adaptivePlatformDensity, + onTap: () { + controller.closeView(''); + model.storedDialogInfo = dialog; + GoRouter.of(context).go( + '${GoRouter.of(context).state.path}/stored', + ); + }, + ); + } } class DialogInfo extends StatelessWidget { diff --git a/lib/ui/views/main_page/chat/chat_inside.dart b/lib/ui/views/main_page/chat/chat_inside.dart index be0e6a52..75f69433 100644 --- a/lib/ui/views/main_page/chat/chat_inside.dart +++ b/lib/ui/views/main_page/chat/chat_inside.dart @@ -13,10 +13,9 @@ import 'package:unn_mobile/ui/views/main_page/chat/widgets/message_group.dart'; import 'package:unn_mobile/ui/views/main_page/chat/widgets/send_field.dart'; class ChatInside extends StatefulWidget { - const ChatInside({required this.chatId, super.key}); - - final int chatId; + const ChatInside({this.chatId, super.key}); + final int? chatId; @override State createState() => _ChatInsideState(); } @@ -31,7 +30,7 @@ class _ChatInsideState extends State { WidgetsBinding.instance.addPostFrameCallback((_) { if (!hasScrolledOnce && model.dialog != null && - model.dialog!.unreadMessagesCount > 0 && + model.unreadMessagesCount > 0 && scrollController.hasClients) { final renderBox = newMessagesKey.currentContext ?.findRenderObject() as RenderBox?; @@ -142,7 +141,8 @@ class _ChatInsideState extends State { ), ), ], - if (model.lastReadMessageId == null) + if (model.messages.isNotEmpty && + model.lastReadMessageId == null) _buildNewMessagesBar(), if (model.isBusy) const Center( diff --git a/lib/ui/views/main_page/main_page_routing.dart b/lib/ui/views/main_page/main_page_routing.dart index 4860206b..e0e2e98b 100644 --- a/lib/ui/views/main_page/main_page_routing.dart +++ b/lib/ui/views/main_page/main_page_routing.dart @@ -132,6 +132,14 @@ class MainPageRouting { bottomRouteIndex: 2, ), subroutes: [ + MainPageRouteData( + Icons.chat, + Icons.chat, + 'Чат', + 'stored', + builder: (_, state) => const ChatInside(), + userTypes: [], + ), MainPageRouteData( Icons.chat, Icons.chat, diff --git a/pubspec.yaml b/pubspec.yaml index 126406a6..5f8ee076 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: unn_mobile description: A mobile application for UNN Portal website publish_to: 'none' -version: 0.6.0+375 +version: 0.6.0+376 environment: sdk: '>=3.1.2 <4.0.0'