diff --git a/lib/app_router.dart b/lib/app_router.dart index 30cda0b9..b8f7008e 100644 --- a/lib/app_router.dart +++ b/lib/app_router.dart @@ -1,3 +1,4 @@ +import 'package:eatvio_app/core/initialization_cubit/initialization_cubit.dart'; import 'package:eatvio_app/core/token_storage.dart'; import 'package:eatvio_app/features/chat/presentation/pages/chat_screen.dart'; import 'package:eatvio_app/features/chat/presentation/pages/chat_select_screen/chat_select_screen.dart'; @@ -32,6 +33,7 @@ import 'package:eatvio_app/presentation/registration/weight_screen/weight_screen import 'package:eatvio_app/widgets/bottom_navigation_bar_widget.dart'; import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; class AppRouter { @@ -46,6 +48,7 @@ class AppRouter { name: 'authScreen', redirect: (context, state) async { if (await TokenStorage.haveTokenInStorage()) { + await context.read().runInitialization(); return '/my-profile-screen'; } return null; diff --git a/lib/core/initialization_cubit/initialization_cubit.dart b/lib/core/initialization_cubit/initialization_cubit.dart index 4db6546f..89b67949 100644 --- a/lib/core/initialization_cubit/initialization_cubit.dart +++ b/lib/core/initialization_cubit/initialization_cubit.dart @@ -9,6 +9,38 @@ class InitializationCubit extends Cubit { InitializationCubit({required this.chatRepository}) : super(const InitializationState.loading()); + Future subscribeNotifications(int userId, PusherChannelsClient client) async { + final privateNotificationChannel = await client.privateChannel( + 'private-message-events.$userId', + authorizationDelegate: EndpointAuthorizableChannelTokenAuthorizationDelegate.forPrivateChannel( + authorizationEndpoint: Uri.parse('https://mobile-stage.eatvio.ru/broadcasting/auth'), + headers: { + 'Authorization': 'Bearer ${await TokenStorage.getTokenFromStorage()}', + }, + ), + ); + + privateNotificationChannel.subscribe(); + + privateNotificationChannel.bindToAll().listen((event) {}); + } + + Future subscribeChatMessages(int userId, PusherChannelsClient client) async { + final privateMessagesChannel = await client.privateChannel( + 'private-new-chat-messages.$userId', + authorizationDelegate: EndpointAuthorizableChannelTokenAuthorizationDelegate.forPrivateChannel( + authorizationEndpoint: Uri.parse('https://mobile-stage.eatvio.ru/broadcasting/auth'), + headers: { + 'Authorization': 'Bearer ${await TokenStorage.getTokenFromStorage()}', + }, + ), + ); + + privateMessagesChannel.subscribe(); + + privateMessagesChannel.bindToAll().listen((event) {}); + } + Future runInitialization() async { try { await TokenStorage.haveTokenInStorage(); @@ -40,6 +72,10 @@ class InitializationCubit extends Cubit { ), ); await client.connect(); + + await subscribeNotifications(wsUrl.userId, client); + await subscribeChatMessages(wsUrl.userId, client); + emit(InitializationState.success( client: client, )); diff --git a/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart b/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart index 712adf04..96e2bd2c 100644 --- a/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart +++ b/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:io'; import 'package:dart_pusher_channels/dart_pusher_channels.dart'; import 'package:easy_debounce/easy_debounce.dart'; @@ -8,43 +9,47 @@ import 'package:eatvio_app/core/token_storage.dart'; import 'package:eatvio_app/features/chat/models/chat_message_model.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.dart'; import 'package:eatvio_app/features/chat/repositories/chat_repository.dart'; -import 'package:eatvio_app/locator_service.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class ChatRoomCubit extends Cubit { final ChatRepository chatRepository; + final InitializationCubit initializationCubit; int currentPage = 1; late final int lastPage; + bool isLoading = false; late final PusherChannelsClient client; - late final PrivateChannel privateChannel; + late final PrivateChannel privateChatChannel; ChatRoomCubit({ required this.chatRepository, + required this.initializationCubit, }) : super(ChatRoomState.loading()); loadNextPage(String chatId) async { - if (currentPage < lastPage) { + if (currentPage < lastPage && !isLoading) { + isLoading = true; + emit(ChatRoomState.loaded( chatRoom: (state as ChatRoomStateLoaded).chatRoom, chatMessages: (state as ChatRoomStateLoaded).chatMessages, isTyping: (state as ChatRoomStateLoaded).isTyping, - isLoading: true, + isLoading: isLoading, )); try { final chatMessages = await chatRepository.getChatMessages(chatId, currentPage + 1); + isLoading = false; emit(ChatRoomState.loaded( chatRoom: (state as ChatRoomStateLoaded).chatRoom, chatMessages: [...(state as ChatRoomStateLoaded).chatMessages, ...chatMessages.messagesList], isTyping: (state as ChatRoomStateLoaded).isTyping, - isLoading: false, + isLoading: isLoading, )); currentPage++; - } catch (e) { print(e); emit(ChatRoomState.error(errorMessage: 'Ошибка получеия данных')); @@ -59,20 +64,13 @@ class ChatRoomCubit extends Cubit { final chatMessages = await chatRepository.getChatMessages(chatId, currentPage); lastPage = chatMessages.meta.lastPage; - final initCubit = sl(); - - if (initCubit.state is InitializationStateSuccess) { - await subscribeToChatChannel(initCubit: initCubit, chatId: chatId); - } else { - await initCubit.runInitialization(); - await subscribeToChatChannel(initCubit: initCubit, chatId: chatId); - } + await subscribeToChatChannel(initCubit: initializationCubit, chatId: chatId); emit(ChatRoomState.loaded( chatRoom: chatRoom, chatMessages: chatMessages.messagesList, isTyping: false, - isLoading: false, + isLoading: isLoading, )); } catch (e) { emit(ChatRoomState.error(errorMessage: 'Ошибка получеия данных')); @@ -85,7 +83,7 @@ class ChatRoomCubit extends Cubit { }) async { client = (initCubit.state as InitializationStateSuccess).client; - privateChannel = await client.privateChannel( + privateChatChannel = await client.privateChannel( 'private-direct-chat.$chatId', authorizationDelegate: EndpointAuthorizableChannelTokenAuthorizationDelegate.forPrivateChannel( authorizationEndpoint: Uri.parse('https://mobile-stage.eatvio.ru/broadcasting/auth'), @@ -95,9 +93,9 @@ class ChatRoomCubit extends Cubit { ), ); - privateChannel.subscribe(); + privateChatChannel.subscribe(); - privateChannel.bind('App\\Events\\ChatMessageUpdated').listen((event) { + privateChatChannel.bind('App\\Events\\ChatMessageUpdated').listen((event) { List messagesList = [ ChatMessageModel.fromJson(jsonDecode(event.data)['message']), ...(state as ChatRoomStateLoaded).chatMessages, @@ -107,18 +105,18 @@ class ChatRoomCubit extends Cubit { chatRoom: (state as ChatRoomStateLoaded).chatRoom, chatMessages: messagesList, isTyping: (state as ChatRoomStateLoaded).isTyping, - isLoading: false, + isLoading: isLoading, ), ); }); - privateChannel.bind('client-typing').listen((event) { + privateChatChannel.bind('client-typing').listen((event) { emit( ChatRoomState.loaded( chatRoom: (state as ChatRoomStateLoaded).chatRoom, chatMessages: (state as ChatRoomStateLoaded).chatMessages, isTyping: true, - isLoading: false, + isLoading: isLoading, ), ); EasyDebounce.debounce( @@ -129,16 +127,24 @@ class ChatRoomCubit extends Cubit { chatRoom: (state as ChatRoomStateLoaded).chatRoom, chatMessages: (state as ChatRoomStateLoaded).chatMessages, isTyping: false, - isLoading: false, + isLoading: isLoading, ), ), ); }); } + sendMessage({ + required String chatId, + required String data, + required List filesList, + }) async { + await chatRepository.sendMessage(chatId: chatId, data: data); + } + @override Future close() async { - privateChannel.unsubscribe(); + privateChatChannel.unsubscribe(); super.close(); } } diff --git a/lib/features/chat/presentation/pages/chat_screen.dart b/lib/features/chat/presentation/pages/chat_screen.dart index 7d9528fa..258b7317 100644 --- a/lib/features/chat/presentation/pages/chat_screen.dart +++ b/lib/features/chat/presentation/pages/chat_screen.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.dart'; import 'package:eatvio_app/features/chat/presentation/widgets/chat_bubble_widget.dart'; @@ -7,6 +5,7 @@ import 'package:eatvio_app/locator_service.dart'; import 'package:flutter/cupertino.dart'; import 'package:eatvio_app/core/app_export.dart'; import 'package:flutter/material.dart'; +import 'package:lazy_load_scrollview/lazy_load_scrollview.dart'; class ChatScreen extends StatefulWidget { final String chatId; @@ -22,14 +21,13 @@ class ChatScreen extends StatefulWidget { class _ChatScreenState extends State { final scrollController = ScrollController(); - bool isLoading = false; + final TextEditingController textEditingController = TextEditingController(); Widget build(BuildContext context) { return BlocProvider( create: (BuildContext context) => sl()..initChat(widget.chatId), child: BlocBuilder( builder: (context, state) { - setupScrollController(context); if (state is ChatRoomStateLoaded) { return _buildChat(context, state); } else if (state is ChatRoomStateError) { @@ -51,17 +49,6 @@ class _ChatScreenState extends State { ); } - void setupScrollController(BuildContext context) { - scrollController.addListener(() { - /// todo change rule for request new page - if (scrollController.position.atEdge) { - if (scrollController.position.pixels != 0) { - context.read().loadNextPage(widget.chatId); - } - } - }); - } - _buildChat(BuildContext context, ChatRoomStateLoaded state) { return Scaffold( resizeToAvoidBottomInset: true, @@ -125,37 +112,39 @@ class _ChatScreenState extends State { child: Container( color: Color(0xFFF0F2F5), width: double.maxFinite, - child: CustomScrollView( - physics: BouncingScrollPhysics(), - controller: scrollController, - reverse: true, - slivers: [ - SliverPadding( - padding: EdgeInsets.symmetric(vertical: 16), - sliver: SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - if (index < state.chatMessages.length) { - return ChatBubbleWidget( - text: state.chatMessages[index].data.trim(), - isCurrentUser: state.chatRoom.users['to_chat_user']!.id != state.chatMessages[index].authorId, - mediaData: state.chatMessages[index].mediaData ?? [], - ); - } else { - Timer(const Duration(milliseconds: 30), () { - scrollController.jumpTo(scrollController.position.maxScrollExtent); - }); - return Padding( - padding: const EdgeInsets.all(8), - child: Center(child: CupertinoActivityIndicator()), - ); - } - }, - childCount: state.chatMessages.length + (state.isLoading ? 1 : 0), + child: LazyLoadScrollView( + onEndOfPage: () => context.read().loadNextPage(widget.chatId), + isLoading: state.isLoading, + scrollOffset: MediaQuery.of(context).size.height.toInt(), + child: CustomScrollView( + physics: BouncingScrollPhysics(), + controller: scrollController, + reverse: true, + slivers: [ + SliverPadding( + padding: EdgeInsets.symmetric(vertical: 16), + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + if (index < state.chatMessages.length) { + return ChatBubbleWidget( + text: state.chatMessages[index].data.trim(), + isCurrentUser: state.chatRoom.users['to_chat_user']!.id != state.chatMessages[index].authorId, + mediaData: state.chatMessages[index].mediaData ?? [], + ); + } else { + return Padding( + padding: const EdgeInsets.all(8), + child: Center(child: CupertinoActivityIndicator()), + ); + } + }, + childCount: state.chatMessages.length + (state.isLoading ? 1 : 0), + ), ), ), - ), - ], + ], + ), ), ), ), @@ -182,6 +171,7 @@ class _ChatScreenState extends State { SizedBox( width: mediaQueryData.size.width - 96, child: TextFormField( + controller: textEditingController, maxLines: 5, minLines: 1, keyboardType: TextInputType.multiline, @@ -228,7 +218,18 @@ class _ChatScreenState extends State { SizedBox(width: 8), InkWell( borderRadius: BorderRadiusStyle.circleBorder17, - onTap: () {}, + onTap: () { + if (textEditingController.text.isNotEmpty) { + context.read().sendMessage( + chatId: widget.chatId, + data: textEditingController.text.trim(), + filesList: [], + ); + setState(() { + textEditingController.clear(); + }); + } + }, child: Ink( height: 32, width: 32, diff --git a/lib/features/chat/presentation/pages/chat_select_screen/bloc/chat_select_bloc.dart b/lib/features/chat/presentation/pages/chat_select_screen/bloc/chat_select_bloc.dart index 9bd2845c..a055b5ef 100644 --- a/lib/features/chat/presentation/pages/chat_select_screen/bloc/chat_select_bloc.dart +++ b/lib/features/chat/presentation/pages/chat_select_screen/bloc/chat_select_bloc.dart @@ -13,6 +13,7 @@ import '/core/app_export.dart'; import 'package:go_router/go_router.dart'; part 'chat_select_event.dart'; + part 'chat_select_state.dart'; /// A bloc that manages the state of a ChatSelect according to the event that is dispatched to it. @@ -68,15 +69,19 @@ class ChatSelectBloc extends Bloc { if (data is Map && data.containsKey('chatRooms')) { if (state is ChatSelectLoadedState) { var currentState = state as ChatSelectLoadedState; - emit(currentState.copyWith( - chatSelectModelObj: currentState.chatSelectModelObj, - chatRooms: currentState.chatRooms, - ),); + emit( + currentState.copyWith( + chatSelectModelObj: currentState.chatSelectModelObj, + chatRooms: currentState.chatRooms, + ), + ); } else { - emit(ChatSelectLoadedState( - chatRooms: fillChatEntryList(data['chatRooms']), - //chatRooms: [], - ),); + emit( + ChatSelectLoadedState( + chatRooms: fillChatEntryList(data['chatRooms']), + //chatRooms: [], + ), + ); } } else { // Один или несколько ключевых полей отсутствуют, обрабатываем ошибку diff --git a/lib/features/chat/repositories/chat_repository.dart b/lib/features/chat/repositories/chat_repository.dart index dbc67609..c35847f2 100644 --- a/lib/features/chat/repositories/chat_repository.dart +++ b/lib/features/chat/repositories/chat_repository.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:eatvio_app/features/chat/models/chat_messages_model.dart'; import 'package:eatvio_app/features/chat/models/chat_room_model.dart'; import 'package:eatvio_app/features/chat/models/websocket_info_model.dart'; @@ -10,9 +12,26 @@ class ChatRepository { required this.chatService, }); + Future sendMessage({ + required String chatId, + String? data, + List? filesList, + }) async { + try { + await chatService.sendMessage( + id: chatId, + data: data, + filesList: filesList, + ); + } catch (e) { + print(e); + rethrow; + } + } + Future getChatRoom(String chatId) async { try { - return (await chatService.getChatRoom(chatId))['chatRoom']!; + return (await chatService.getChatRoom(id: chatId))['chatRoom']!; } catch (e) { print(e); rethrow; @@ -21,7 +40,7 @@ class ChatRepository { Future getChatMessages(String chatId, int page) async { try { - return (await chatService.getChatMessages(chatId, page)); + return (await chatService.getChatMessages(id: chatId, page: page)); } catch (e) { print(e); rethrow; diff --git a/lib/features/chat/services/chat_service.dart b/lib/features/chat/services/chat_service.dart index fa81a008..675d6806 100644 --- a/lib/features/chat/services/chat_service.dart +++ b/lib/features/chat/services/chat_service.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:eatvio_app/features/chat/models/chat_messages_model.dart'; import 'package:eatvio_app/features/chat/models/chat_room_model.dart'; import 'package:dio/dio.dart'; @@ -11,10 +13,22 @@ abstract class ChatService { factory ChatService(Dio dio, {String baseUrl}) = _ChatService; @GET('/chat/show/{id}') - Future> getChatRoom(@Path() String id); + Future> getChatRoom({ + @Path() required String id, + }); + + @POST('/chat/{id}/message/send') + Future sendMessage({ + @Path() required String id, + @Part() String? data, + @Part() List? filesList, + }); @GET('/chat/show/{id}/messages?page={page}') - Future getChatMessages(@Path() String id, @Path() int page); + Future getChatMessages({ + @Path() required String id, + @Path() required int page, + }); @GET('/profile/ws/params') Future getWebsocket(); diff --git a/lib/features/chat/services/chat_service.g.dart b/lib/features/chat/services/chat_service.g.dart index be6ed456..65eb7c5e 100644 --- a/lib/features/chat/services/chat_service.g.dart +++ b/lib/features/chat/services/chat_service.g.dart @@ -19,7 +19,7 @@ class _ChatService implements ChatService { String? baseUrl; @override - Future> getChatRoom(String id) async { + Future> getChatRoom({required String id}) async { final _extra = {}; final queryParameters = {}; final _headers = {}; @@ -47,10 +47,53 @@ class _ChatService implements ChatService { } @override - Future getChatMessages( - String id, - int page, - ) async { + Future sendMessage({ + required String id, + String? data, + List? filesList, + }) async { + final _extra = {}; + final queryParameters = {}; + queryParameters.removeWhere((k, v) => v == null); + final _headers = {}; + final _data = FormData(); + if (data != null) { + _data.fields.add(MapEntry( + 'data', + data, + )); + } + if (filesList != null) { + _data.files.addAll(filesList.map((i) => MapEntry( + 'filesList', + MultipartFile.fromFileSync( + i.path, + filename: i.path.split(Platform.pathSeparator).last, + )))); + } + await _dio.fetch(_setStreamType(Options( + method: 'POST', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/chat/${id}/message/send', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + } + + @override + Future getChatMessages({ + required String id, + required int page, + }) async { final _extra = {}; final queryParameters = {}; final _headers = {}; diff --git a/lib/locator_service.dart b/lib/locator_service.dart index fb2637ec..5a924d20 100644 --- a/lib/locator_service.dart +++ b/lib/locator_service.dart @@ -18,7 +18,7 @@ Future init() async { // BLoC / Cubit sl.registerLazySingleton(() => InitializationCubit(chatRepository: sl())); - sl.registerFactory(() => ChatRoomCubit(chatRepository: sl())); + sl.registerFactory(() => ChatRoomCubit(chatRepository: sl(), initializationCubit: sl())); sl.registerFactory(() => FeedCubit(feedRepository: sl())); sl.registerFactory(() => BlogPostCubit(blogPostRepository: sl())); diff --git a/lib/main.dart b/lib/main.dart index 684e227e..3f285adc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -42,7 +42,7 @@ class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MultiBlocProvider( providers: [ - BlocProvider(create: (context) => InitializationCubit(chatRepository: sl())), + BlocProvider(create: (context) => sl()), BlocProvider(create: (context) => ThemeBloc(ThemeState(themeType: PrefUtils().getThemeData()))), BlocProvider(create: (context) => sl()..loadFeedBlogPostList()), BlocProvider(create: (context) => MyProfileBloc(MyProfileState(myProfileModelObj: MyProfileModel()))..add(MyProfileInitialEvent())), diff --git a/pubspec.yaml b/pubspec.yaml index 256f8923..87ed29e8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,61 +13,37 @@ dependencies: flutter_localizations: sdk: flutter - # BLoC flutter_bloc: ^8.1.6 - # local storage shared_preferences: ^2.2.3 hive: ^2.2.3 hive_flutter: ^1.1.0 - # cached_network_image: ^3.3.1 - # flutter_svg: ^2.0.10 - # equatable: ^2.0.5 - # freezed_annotation: ^2.4.2 - # json_annotation: ^4.9.0 - # validators: ^3.0.0 - # isar_flutter_libs: ^3.1.0 - # path_provider: ^2.1.3 - # fl_chart: ^0.68.0 - # flutter_spinkit: ^5.2.1 - # flutter_dotenv: ^5.1.0 - # carousel_slider: ^4.2.1 - # flutter_form_builder: ^9.3.0 - # image_picker: ^1.1.2 - # flutter_typeahead: ^5.2.0 - # navigation go_router: ^14.2.0 - # keyboard_dismisser: ^3.0.0 - # service generator retrofit: ^4.1.0 - # di get_it: ^7.7.0 - # http request dio: ^5.5.0 - # pusher dart_pusher_channels: ^1.2.2 - # photo gallery viewer + dismissible page photo_view: ^0.15.0 dismissible_page: ^1.0.2 - # url launcher url_launcher: ^6.3.0 - # debounce easy_debounce: ^2.0.3 + lazy_load_scrollview: ^1.3.0 + badges: ^3.1.2 dev_dependencies: build_runner: ^2.4.11