diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index dd1b63cb..41b10c91 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -1,4 +1,4 @@ -import 'package:eatvio_app/core/initialization_cubit/initialization_cubit.dart'; +import 'package:eatvio_app/core/initialization_cubit/ws_manager.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/token_storage.dart'; import 'package:eatvio_app/features/auth/auth_screen/auth_screen.dart'; @@ -79,7 +79,7 @@ class AppRouter { path: '/my-profile-screen', name: 'myProfileScreen', redirect: (context, state) async { - await sl().runInitialization(); + await sl().initialize(); return null; }, pageBuilder: (context, state) => NoTransitionPage(child: ProfileScreen()), diff --git a/lib/core/const.dart b/lib/core/const.dart index 24e72cdd..cd822713 100644 --- a/lib/core/const.dart +++ b/lib/core/const.dart @@ -4,3 +4,7 @@ const List durationValues = [7, 14, 21]; const List dietTypeValues = ['veg', 'light', 'carb', 'meat']; const List planTypeValues = ['fit', 'slim', 'wellness', 'behavior']; + +const newMessagesChannelName = 'private-new-chat-messages'; +const newNotificationsChannelName = 'private-message-events'; +const directChatChannelName = 'private-direct-chat'; diff --git a/lib/core/initialization_cubit/initialization_cubit.dart b/lib/core/initialization_cubit/initialization_cubit.dart index 2bdc881f..e69de29b 100644 --- a/lib/core/initialization_cubit/initialization_cubit.dart +++ b/lib/core/initialization_cubit/initialization_cubit.dart @@ -1,105 +0,0 @@ -import 'package:dart_pusher_channels/dart_pusher_channels.dart'; -import 'package:eatvio_app/core/const.dart'; -import 'package:eatvio_app/core/initialization_cubit/initialization_state.dart'; -import 'package:eatvio_app/core/token_storage.dart'; -import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart'; -import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/notifications_cubit/notifications_cubit.dart'; -import 'package:eatvio_app/features/chat/repositories/chat_repository.dart'; -import 'package:eatvio_app/core/locator_service.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class InitializationCubit extends Cubit { - final ChatRepository chatRepository; - bool wasInitialized = false; - - 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(baseUrl + '/broadcasting/auth'), - headers: { - 'Authorization': 'Bearer ${await TokenStorage.getTokenFromStorage()}', - }, - ), - ); - - privateNotificationChannel.subscribe(); - - privateNotificationChannel.bindToAll().listen((event) { - sl().incrementCounter(event.name); - }); - } - - Future subscribeChatMessages(int userId, PusherChannelsClient client) async { - final privateMessagesChannel = await client.privateChannel( - 'private-new-chat-messages.$userId', - authorizationDelegate: EndpointAuthorizableChannelTokenAuthorizationDelegate.forPrivateChannel( - authorizationEndpoint: Uri.parse(baseUrl + '/broadcasting/auth'), - headers: { - 'Authorization': 'Bearer ${await TokenStorage.getTokenFromStorage()}', - }, - ), - ); - - privateMessagesChannel.subscribe(); - - privateMessagesChannel.bindToAll().listen((event) { - sl().incrementCounter(); - }); - } - - Future runInitialization() async { - if (!wasInitialized) { - wasInitialized = true; - try { - await TokenStorage.haveTokenInStorage(); - final wsUrl = await chatRepository.getWebsocket(); - final options = PusherChannelsOptions.fromHost( - scheme: 'wss', - key: wsUrl.key, - host: wsUrl.host, - shouldSupplyMetadataQueries: true, - metadata: PusherChannelsOptionsMetadata.byDefault(), - port: 443, - ); - final client = PusherChannelsClient.websocket( - options: options, - connectionErrorHandler: (exception, trace, refresh) { - refresh(); - }, - minimumReconnectDelayDuration: const Duration( - seconds: 1, - ), - defaultActivityDuration: const Duration( - seconds: 120, - ), - activityDurationOverride: const Duration( - seconds: 120, - ), - waitForPongDuration: const Duration( - seconds: 30, - ), - ); - await client.connect(); - - await subscribeNotifications(wsUrl.userId, client); - await subscribeChatMessages(wsUrl.userId, client); - - emit( - InitializationState.success( - client: client, - ), - ); - } catch (e) { - emit( - InitializationState.error( - errorMessage: e.toString(), - ), - ); - rethrow; - } - } - } -} diff --git a/lib/core/initialization_cubit/initialization_state.dart b/lib/core/initialization_cubit/initialization_state.dart deleted file mode 100644 index 43318d07..00000000 --- a/lib/core/initialization_cubit/initialization_state.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:dart_pusher_channels/dart_pusher_channels.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'initialization_state.freezed.dart'; - -@freezed -abstract class InitializationState with _$InitializationState { - const factory InitializationState() = _InitializationState; - - const factory InitializationState.loading() = InitializationStateLoading; - - const factory InitializationState.success({ - required PusherChannelsClient client, - }) = InitializationStateSuccess; - - const factory InitializationState.error({ - required String errorMessage, - }) = InitializationStateError; -} diff --git a/lib/core/initialization_cubit/initialization_state.freezed.dart b/lib/core/initialization_cubit/initialization_state.freezed.dart deleted file mode 100644 index b0535c68..00000000 --- a/lib/core/initialization_cubit/initialization_state.freezed.dart +++ /dev/null @@ -1,645 +0,0 @@ -// coverage:ignore-file -// GENERATED CODE - DO NOT MODIFY BY HAND -// ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark - -part of 'initialization_state.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -T _$identity(T value) => value; - -final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); - -/// @nodoc -mixin _$InitializationState { - @optionalTypeArgs - TResult when( - TResult Function() $default, { - required TResult Function() loading, - required TResult Function(PusherChannelsClient client) success, - required TResult Function(String errorMessage) error, - }) => - throw _privateConstructorUsedError; - @optionalTypeArgs - TResult? whenOrNull( - TResult? Function()? $default, { - TResult? Function()? loading, - TResult? Function(PusherChannelsClient client)? success, - TResult? Function(String errorMessage)? error, - }) => - throw _privateConstructorUsedError; - @optionalTypeArgs - TResult maybeWhen( - TResult Function()? $default, { - TResult Function()? loading, - TResult Function(PusherChannelsClient client)? success, - TResult Function(String errorMessage)? error, - required TResult orElse(), - }) => - throw _privateConstructorUsedError; - @optionalTypeArgs - TResult map( - TResult Function(_InitializationState value) $default, { - required TResult Function(InitializationStateLoading value) loading, - required TResult Function(InitializationStateSuccess value) success, - required TResult Function(InitializationStateError value) error, - }) => - throw _privateConstructorUsedError; - @optionalTypeArgs - TResult? mapOrNull( - TResult? Function(_InitializationState value)? $default, { - TResult? Function(InitializationStateLoading value)? loading, - TResult? Function(InitializationStateSuccess value)? success, - TResult? Function(InitializationStateError value)? error, - }) => - throw _privateConstructorUsedError; - @optionalTypeArgs - TResult maybeMap( - TResult Function(_InitializationState value)? $default, { - TResult Function(InitializationStateLoading value)? loading, - TResult Function(InitializationStateSuccess value)? success, - TResult Function(InitializationStateError value)? error, - required TResult orElse(), - }) => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $InitializationStateCopyWith<$Res> { - factory $InitializationStateCopyWith( - InitializationState value, $Res Function(InitializationState) then) = - _$InitializationStateCopyWithImpl<$Res, InitializationState>; -} - -/// @nodoc -class _$InitializationStateCopyWithImpl<$Res, $Val extends InitializationState> - implements $InitializationStateCopyWith<$Res> { - _$InitializationStateCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of InitializationState - /// with the given fields replaced by the non-null parameter values. -} - -/// @nodoc -abstract class _$$InitializationStateImplCopyWith<$Res> { - factory _$$InitializationStateImplCopyWith(_$InitializationStateImpl value, - $Res Function(_$InitializationStateImpl) then) = - __$$InitializationStateImplCopyWithImpl<$Res>; -} - -/// @nodoc -class __$$InitializationStateImplCopyWithImpl<$Res> - extends _$InitializationStateCopyWithImpl<$Res, _$InitializationStateImpl> - implements _$$InitializationStateImplCopyWith<$Res> { - __$$InitializationStateImplCopyWithImpl(_$InitializationStateImpl _value, - $Res Function(_$InitializationStateImpl) _then) - : super(_value, _then); - - /// Create a copy of InitializationState - /// with the given fields replaced by the non-null parameter values. -} - -/// @nodoc - -class _$InitializationStateImpl implements _InitializationState { - const _$InitializationStateImpl(); - - @override - String toString() { - return 'InitializationState()'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$InitializationStateImpl); - } - - @override - int get hashCode => runtimeType.hashCode; - - @override - @optionalTypeArgs - TResult when( - TResult Function() $default, { - required TResult Function() loading, - required TResult Function(PusherChannelsClient client) success, - required TResult Function(String errorMessage) error, - }) { - return $default(); - } - - @override - @optionalTypeArgs - TResult? whenOrNull( - TResult? Function()? $default, { - TResult? Function()? loading, - TResult? Function(PusherChannelsClient client)? success, - TResult? Function(String errorMessage)? error, - }) { - return $default?.call(); - } - - @override - @optionalTypeArgs - TResult maybeWhen( - TResult Function()? $default, { - TResult Function()? loading, - TResult Function(PusherChannelsClient client)? success, - TResult Function(String errorMessage)? error, - required TResult orElse(), - }) { - if ($default != null) { - return $default(); - } - return orElse(); - } - - @override - @optionalTypeArgs - TResult map( - TResult Function(_InitializationState value) $default, { - required TResult Function(InitializationStateLoading value) loading, - required TResult Function(InitializationStateSuccess value) success, - required TResult Function(InitializationStateError value) error, - }) { - return $default(this); - } - - @override - @optionalTypeArgs - TResult? mapOrNull( - TResult? Function(_InitializationState value)? $default, { - TResult? Function(InitializationStateLoading value)? loading, - TResult? Function(InitializationStateSuccess value)? success, - TResult? Function(InitializationStateError value)? error, - }) { - return $default?.call(this); - } - - @override - @optionalTypeArgs - TResult maybeMap( - TResult Function(_InitializationState value)? $default, { - TResult Function(InitializationStateLoading value)? loading, - TResult Function(InitializationStateSuccess value)? success, - TResult Function(InitializationStateError value)? error, - required TResult orElse(), - }) { - if ($default != null) { - return $default(this); - } - return orElse(); - } -} - -abstract class _InitializationState implements InitializationState { - const factory _InitializationState() = _$InitializationStateImpl; -} - -/// @nodoc -abstract class _$$InitializationStateLoadingImplCopyWith<$Res> { - factory _$$InitializationStateLoadingImplCopyWith( - _$InitializationStateLoadingImpl value, - $Res Function(_$InitializationStateLoadingImpl) then) = - __$$InitializationStateLoadingImplCopyWithImpl<$Res>; -} - -/// @nodoc -class __$$InitializationStateLoadingImplCopyWithImpl<$Res> - extends _$InitializationStateCopyWithImpl<$Res, - _$InitializationStateLoadingImpl> - implements _$$InitializationStateLoadingImplCopyWith<$Res> { - __$$InitializationStateLoadingImplCopyWithImpl( - _$InitializationStateLoadingImpl _value, - $Res Function(_$InitializationStateLoadingImpl) _then) - : super(_value, _then); - - /// Create a copy of InitializationState - /// with the given fields replaced by the non-null parameter values. -} - -/// @nodoc - -class _$InitializationStateLoadingImpl implements InitializationStateLoading { - const _$InitializationStateLoadingImpl(); - - @override - String toString() { - return 'InitializationState.loading()'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$InitializationStateLoadingImpl); - } - - @override - int get hashCode => runtimeType.hashCode; - - @override - @optionalTypeArgs - TResult when( - TResult Function() $default, { - required TResult Function() loading, - required TResult Function(PusherChannelsClient client) success, - required TResult Function(String errorMessage) error, - }) { - return loading(); - } - - @override - @optionalTypeArgs - TResult? whenOrNull( - TResult? Function()? $default, { - TResult? Function()? loading, - TResult? Function(PusherChannelsClient client)? success, - TResult? Function(String errorMessage)? error, - }) { - return loading?.call(); - } - - @override - @optionalTypeArgs - TResult maybeWhen( - TResult Function()? $default, { - TResult Function()? loading, - TResult Function(PusherChannelsClient client)? success, - TResult Function(String errorMessage)? error, - required TResult orElse(), - }) { - if (loading != null) { - return loading(); - } - return orElse(); - } - - @override - @optionalTypeArgs - TResult map( - TResult Function(_InitializationState value) $default, { - required TResult Function(InitializationStateLoading value) loading, - required TResult Function(InitializationStateSuccess value) success, - required TResult Function(InitializationStateError value) error, - }) { - return loading(this); - } - - @override - @optionalTypeArgs - TResult? mapOrNull( - TResult? Function(_InitializationState value)? $default, { - TResult? Function(InitializationStateLoading value)? loading, - TResult? Function(InitializationStateSuccess value)? success, - TResult? Function(InitializationStateError value)? error, - }) { - return loading?.call(this); - } - - @override - @optionalTypeArgs - TResult maybeMap( - TResult Function(_InitializationState value)? $default, { - TResult Function(InitializationStateLoading value)? loading, - TResult Function(InitializationStateSuccess value)? success, - TResult Function(InitializationStateError value)? error, - required TResult orElse(), - }) { - if (loading != null) { - return loading(this); - } - return orElse(); - } -} - -abstract class InitializationStateLoading implements InitializationState { - const factory InitializationStateLoading() = _$InitializationStateLoadingImpl; -} - -/// @nodoc -abstract class _$$InitializationStateSuccessImplCopyWith<$Res> { - factory _$$InitializationStateSuccessImplCopyWith( - _$InitializationStateSuccessImpl value, - $Res Function(_$InitializationStateSuccessImpl) then) = - __$$InitializationStateSuccessImplCopyWithImpl<$Res>; - @useResult - $Res call({PusherChannelsClient client}); -} - -/// @nodoc -class __$$InitializationStateSuccessImplCopyWithImpl<$Res> - extends _$InitializationStateCopyWithImpl<$Res, - _$InitializationStateSuccessImpl> - implements _$$InitializationStateSuccessImplCopyWith<$Res> { - __$$InitializationStateSuccessImplCopyWithImpl( - _$InitializationStateSuccessImpl _value, - $Res Function(_$InitializationStateSuccessImpl) _then) - : super(_value, _then); - - /// Create a copy of InitializationState - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? client = null, - }) { - return _then(_$InitializationStateSuccessImpl( - client: null == client - ? _value.client - : client // ignore: cast_nullable_to_non_nullable - as PusherChannelsClient, - )); - } -} - -/// @nodoc - -class _$InitializationStateSuccessImpl implements InitializationStateSuccess { - const _$InitializationStateSuccessImpl({required this.client}); - - @override - final PusherChannelsClient client; - - @override - String toString() { - return 'InitializationState.success(client: $client)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$InitializationStateSuccessImpl && - (identical(other.client, client) || other.client == client)); - } - - @override - int get hashCode => Object.hash(runtimeType, client); - - /// Create a copy of InitializationState - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$InitializationStateSuccessImplCopyWith<_$InitializationStateSuccessImpl> - get copyWith => __$$InitializationStateSuccessImplCopyWithImpl< - _$InitializationStateSuccessImpl>(this, _$identity); - - @override - @optionalTypeArgs - TResult when( - TResult Function() $default, { - required TResult Function() loading, - required TResult Function(PusherChannelsClient client) success, - required TResult Function(String errorMessage) error, - }) { - return success(client); - } - - @override - @optionalTypeArgs - TResult? whenOrNull( - TResult? Function()? $default, { - TResult? Function()? loading, - TResult? Function(PusherChannelsClient client)? success, - TResult? Function(String errorMessage)? error, - }) { - return success?.call(client); - } - - @override - @optionalTypeArgs - TResult maybeWhen( - TResult Function()? $default, { - TResult Function()? loading, - TResult Function(PusherChannelsClient client)? success, - TResult Function(String errorMessage)? error, - required TResult orElse(), - }) { - if (success != null) { - return success(client); - } - return orElse(); - } - - @override - @optionalTypeArgs - TResult map( - TResult Function(_InitializationState value) $default, { - required TResult Function(InitializationStateLoading value) loading, - required TResult Function(InitializationStateSuccess value) success, - required TResult Function(InitializationStateError value) error, - }) { - return success(this); - } - - @override - @optionalTypeArgs - TResult? mapOrNull( - TResult? Function(_InitializationState value)? $default, { - TResult? Function(InitializationStateLoading value)? loading, - TResult? Function(InitializationStateSuccess value)? success, - TResult? Function(InitializationStateError value)? error, - }) { - return success?.call(this); - } - - @override - @optionalTypeArgs - TResult maybeMap( - TResult Function(_InitializationState value)? $default, { - TResult Function(InitializationStateLoading value)? loading, - TResult Function(InitializationStateSuccess value)? success, - TResult Function(InitializationStateError value)? error, - required TResult orElse(), - }) { - if (success != null) { - return success(this); - } - return orElse(); - } -} - -abstract class InitializationStateSuccess implements InitializationState { - const factory InitializationStateSuccess( - {required final PusherChannelsClient client}) = - _$InitializationStateSuccessImpl; - - PusherChannelsClient get client; - - /// Create a copy of InitializationState - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - _$$InitializationStateSuccessImplCopyWith<_$InitializationStateSuccessImpl> - get copyWith => throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class _$$InitializationStateErrorImplCopyWith<$Res> { - factory _$$InitializationStateErrorImplCopyWith( - _$InitializationStateErrorImpl value, - $Res Function(_$InitializationStateErrorImpl) then) = - __$$InitializationStateErrorImplCopyWithImpl<$Res>; - @useResult - $Res call({String errorMessage}); -} - -/// @nodoc -class __$$InitializationStateErrorImplCopyWithImpl<$Res> - extends _$InitializationStateCopyWithImpl<$Res, - _$InitializationStateErrorImpl> - implements _$$InitializationStateErrorImplCopyWith<$Res> { - __$$InitializationStateErrorImplCopyWithImpl( - _$InitializationStateErrorImpl _value, - $Res Function(_$InitializationStateErrorImpl) _then) - : super(_value, _then); - - /// Create a copy of InitializationState - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? errorMessage = null, - }) { - return _then(_$InitializationStateErrorImpl( - errorMessage: null == errorMessage - ? _value.errorMessage - : errorMessage // ignore: cast_nullable_to_non_nullable - as String, - )); - } -} - -/// @nodoc - -class _$InitializationStateErrorImpl implements InitializationStateError { - const _$InitializationStateErrorImpl({required this.errorMessage}); - - @override - final String errorMessage; - - @override - String toString() { - return 'InitializationState.error(errorMessage: $errorMessage)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$InitializationStateErrorImpl && - (identical(other.errorMessage, errorMessage) || - other.errorMessage == errorMessage)); - } - - @override - int get hashCode => Object.hash(runtimeType, errorMessage); - - /// Create a copy of InitializationState - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$InitializationStateErrorImplCopyWith<_$InitializationStateErrorImpl> - get copyWith => __$$InitializationStateErrorImplCopyWithImpl< - _$InitializationStateErrorImpl>(this, _$identity); - - @override - @optionalTypeArgs - TResult when( - TResult Function() $default, { - required TResult Function() loading, - required TResult Function(PusherChannelsClient client) success, - required TResult Function(String errorMessage) error, - }) { - return error(errorMessage); - } - - @override - @optionalTypeArgs - TResult? whenOrNull( - TResult? Function()? $default, { - TResult? Function()? loading, - TResult? Function(PusherChannelsClient client)? success, - TResult? Function(String errorMessage)? error, - }) { - return error?.call(errorMessage); - } - - @override - @optionalTypeArgs - TResult maybeWhen( - TResult Function()? $default, { - TResult Function()? loading, - TResult Function(PusherChannelsClient client)? success, - TResult Function(String errorMessage)? error, - required TResult orElse(), - }) { - if (error != null) { - return error(errorMessage); - } - return orElse(); - } - - @override - @optionalTypeArgs - TResult map( - TResult Function(_InitializationState value) $default, { - required TResult Function(InitializationStateLoading value) loading, - required TResult Function(InitializationStateSuccess value) success, - required TResult Function(InitializationStateError value) error, - }) { - return error(this); - } - - @override - @optionalTypeArgs - TResult? mapOrNull( - TResult? Function(_InitializationState value)? $default, { - TResult? Function(InitializationStateLoading value)? loading, - TResult? Function(InitializationStateSuccess value)? success, - TResult? Function(InitializationStateError value)? error, - }) { - return error?.call(this); - } - - @override - @optionalTypeArgs - TResult maybeMap( - TResult Function(_InitializationState value)? $default, { - TResult Function(InitializationStateLoading value)? loading, - TResult Function(InitializationStateSuccess value)? success, - TResult Function(InitializationStateError value)? error, - required TResult orElse(), - }) { - if (error != null) { - return error(this); - } - return orElse(); - } -} - -abstract class InitializationStateError implements InitializationState { - const factory InitializationStateError({required final String errorMessage}) = - _$InitializationStateErrorImpl; - - String get errorMessage; - - /// Create a copy of InitializationState - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - _$$InitializationStateErrorImplCopyWith<_$InitializationStateErrorImpl> - get copyWith => throw _privateConstructorUsedError; -} diff --git a/lib/core/initialization_cubit/ws_manager.dart b/lib/core/initialization_cubit/ws_manager.dart new file mode 100644 index 00000000..3a72512e --- /dev/null +++ b/lib/core/initialization_cubit/ws_manager.dart @@ -0,0 +1,139 @@ +import 'dart:developer'; + +import 'package:dart_pusher_channels/dart_pusher_channels.dart'; +import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/initialization_cubit/ws_service.dart'; +import 'package:eatvio_app/core/token_storage.dart'; + +class WsManager { + final WsService wsService; + + WsManager({required this.wsService}); + + PusherChannelsClient? client; + int? userId; + + PrivateChannel? _privateNotificationChannel; + PrivateChannel? _privateMessagesChannel; + PrivateChannel? _privateDirectChatChannel; + + Stream? get newMessagesStream => _privateMessagesChannel?.bind('App\\Events\\ChatMessageUpdated'); + + // TODO: change name of event to a right one + Stream? get newNotificationsStream => _privateNotificationChannel?.bind('App\\Events\\ChatMessageUpdated'); + + Stream? get directChatStream => _privateDirectChatChannel?.bind('App\\Events\\ChatMessageUpdated'); + + Stream? get clientTypingStream => _privateDirectChatChannel?.bind('client-typing'); + + Future initialize() async { + if (this.client == null) { + try { + await TokenStorage.haveTokenInStorage(); + final wsUrl = await wsService.getWebsocket(); + + final options = PusherChannelsOptions.fromHost( + scheme: 'wss', + key: wsUrl.key, + host: wsUrl.host, + shouldSupplyMetadataQueries: true, + metadata: PusherChannelsOptionsMetadata.byDefault(), + port: 443, + ); + client = PusherChannelsClient.websocket( + options: options, + connectionErrorHandler: (exception, trace, refresh) { + refresh(); + }, + minimumReconnectDelayDuration: const Duration( + seconds: 1, + ), + defaultActivityDuration: const Duration( + seconds: 30, + ), + waitForPongDuration: const Duration( + seconds: 30, + ), + ); + + await client?.connect(); + + this.client = client; + userId = wsUrl.userId; + + await _initPrivateNotificationsChannel(); + await _initPrivateMessagesChannel(); + + client?.lifecycleStream.listen((data) { + if (data == PusherChannelsClientLifeCycleState.establishedConnection) { + _unsubscribe(); + _subscribe(); + } + }); + client?.eventStream.listen((event) { + log(event.rootObject.toString()); + }); + _subscribe(); + } catch (e, st) { + log(e.toString(), stackTrace: st); + } + } + } + + Future _initPrivateNotificationsChannel() async { + final token = await TokenStorage.getTokenFromStorage(); + _privateNotificationChannel = await client?.privateChannel( + '$newNotificationsChannelName.$userId', + authorizationDelegate: EndpointAuthorizableChannelTokenAuthorizationDelegate.forPrivateChannel( + authorizationEndpoint: Uri.parse(baseUrl + '/broadcasting/auth'), + headers: { + 'Authorization': 'Bearer $token', + }, + ), + ); + } + + Future _initPrivateMessagesChannel() async { + final token = await TokenStorage.getTokenFromStorage(); + _privateMessagesChannel = await client?.privateChannel( + '$newMessagesChannelName.$userId', + authorizationDelegate: EndpointAuthorizableChannelTokenAuthorizationDelegate.forPrivateChannel( + authorizationEndpoint: Uri.parse(baseUrl + '/broadcasting/auth'), + headers: { + 'Authorization': 'Bearer $token', + }, + ), + ); + } + + Future initPrivateDirectChatChannel(String chatId) async { + final token = await TokenStorage.getTokenFromStorage(); + _privateDirectChatChannel = await client?.privateChannel( + '$directChatChannelName.$chatId', + authorizationDelegate: EndpointAuthorizableChannelTokenAuthorizationDelegate.forPrivateChannel( + authorizationEndpoint: Uri.parse(baseUrl + '/broadcasting/auth'), + headers: { + 'Authorization': 'Bearer $token', + }, + ), + ); + } + + void _subscribe() { + _privateMessagesChannel?.subscribe(); + _privateNotificationChannel?.subscribe(); + } + + void _unsubscribe() { + _privateMessagesChannel?.unsubscribe(); + _privateNotificationChannel?.unsubscribe(); + } + + void subscribeDirectChatChannel() { + _privateDirectChatChannel?.subscribe(); + } + + void unsubscribeDirectChatChannel() { + _privateDirectChatChannel?.unsubscribe(); + } +} diff --git a/lib/core/initialization_cubit/ws_service.dart b/lib/core/initialization_cubit/ws_service.dart new file mode 100644 index 00000000..92d326f0 --- /dev/null +++ b/lib/core/initialization_cubit/ws_service.dart @@ -0,0 +1,15 @@ +import 'package:dio/dio.dart'; +import 'package:eatvio_app/features/chat/models/websocket_info_model.dart'; +import 'package:retrofit/retrofit.dart'; + +part 'ws_service.g.dart'; + + +@RestApi() +abstract class WsService { + + factory WsService(Dio dio, {String baseUrl}) = _WsService; + + @GET('/api/profile/ws/params') + Future getWebsocket(); +} \ No newline at end of file diff --git a/lib/core/initialization_cubit/ws_service.g.dart b/lib/core/initialization_cubit/ws_service.g.dart new file mode 100644 index 00000000..c5a42eeb --- /dev/null +++ b/lib/core/initialization_cubit/ws_service.g.dart @@ -0,0 +1,86 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'ws_service.dart'; + +// ************************************************************************** +// RetrofitGenerator +// ************************************************************************** + +// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers,unused_element,unnecessary_string_interpolations + +class _WsService implements WsService { + _WsService( + this._dio, { + this.baseUrl, + this.errorLogger, + }); + + final Dio _dio; + + String? baseUrl; + + final ParseErrorLogger? errorLogger; + + @override + Future getWebsocket() async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + const Map? _data = null; + final _options = _setStreamType(Options( + method: 'GET', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/profile/ws/params', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + ))); + final _result = await _dio.fetch>(_options); + late WebsocketInfoModel _value; + try { + _value = WebsocketInfoModel.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options); + rethrow; + } + return _value; + } + + RequestOptions _setStreamType(RequestOptions requestOptions) { + if (T != dynamic && + !(requestOptions.responseType == ResponseType.bytes || + requestOptions.responseType == ResponseType.stream)) { + if (T == String) { + requestOptions.responseType = ResponseType.plain; + } else { + requestOptions.responseType = ResponseType.json; + } + } + return requestOptions; + } + + String _combineBaseUrls( + String dioBaseUrl, + String? baseUrl, + ) { + if (baseUrl == null || baseUrl.trim().isEmpty) { + return dioBaseUrl; + } + + final url = Uri.parse(baseUrl); + + if (url.isAbsolute) { + return url.toString(); + } + + return Uri.parse(dioBaseUrl).resolveUri(url).toString(); + } +} diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index 3dda7501..68f93112 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -1,12 +1,13 @@ import 'package:dio/dio.dart'; import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/initialization_cubit/ws_manager.dart'; +import 'package:eatvio_app/core/initialization_cubit/ws_service.dart'; import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_cubit.dart'; import 'package:eatvio_app/features/create_diet/repositories/create_diet_repository.dart'; import 'package:eatvio_app/features/create_diet/services/create_diet_service.dart'; import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_cubit.dart'; import 'package:eatvio_app/features/diets/repositories/diet_repository.dart'; import 'package:eatvio_app/features/diets/services/diet_service.dart'; -import 'package:eatvio_app/core/initialization_cubit/initialization_cubit.dart'; import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart'; import 'package:eatvio_app/features/blog_post_create/repositories/blog_post_repository.dart'; import 'package:eatvio_app/features/blog_post_create/services/blog_post_service.dart'; @@ -69,15 +70,15 @@ final sl = GetIt.instance; Future init() async { // Cubit singletons - sl.registerLazySingleton(() => InitializationCubit(chatRepository: sl())); + sl.registerLazySingleton(() => WsManager(wsService: sl())); sl.registerLazySingleton(() => NewMessagesCubit()); - sl.registerLazySingleton(() => NotificationsCubit()); + sl.registerLazySingleton(() => NotificationsCubit(wsManager: sl())); sl.registerLazySingleton(() => FeedCubit(feedRepository: sl())); sl.registerLazySingleton(() => ProfileAvatarCubit(profileRepository: sl())); // Cubit factories sl.registerFactory(() => RegistrationCubit(registrationRepository: sl())); - sl.registerFactory(() => ChatRoomCubit(chatRepository: sl(), initializationCubit: sl())); + sl.registerFactory(() => ChatRoomCubit(chatRepository: sl(), wsManager: sl())); sl.registerFactory(() => RecipesCubit(feedRepository: sl(), recipeRepository: sl())); sl.registerFactory(() => CalendarCubit(calendarRepository: sl())); sl.registerFactory(() => ProfileCubit(profileRepository: sl(), chatRepository: sl())); @@ -138,6 +139,7 @@ Future init() async { sl.registerLazySingleton(() => PlanService(sl())); sl.registerLazySingleton(() => MyDietsService(sl())); sl.registerLazySingleton(() => MyPlansService(sl())); + sl.registerLazySingleton(() => WsService(sl())); // External sl.registerLazySingleton( diff --git a/lib/core/theme/custom_text_style.dart b/lib/core/theme/custom_text_style.dart index a4e2539d..6837e186 100644 --- a/lib/core/theme/custom_text_style.dart +++ b/lib/core/theme/custom_text_style.dart @@ -220,6 +220,12 @@ abstract class TextStyles { height: 1.255, ); + static const textFS13FW400 = TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + height: 1.255, + ); + static const textXSSemibold = TextStyle( fontSize: 12, fontWeight: FontWeight.w500, diff --git a/lib/core/theme/theme_helper.dart b/lib/core/theme/theme_helper.dart index 615e33df..5abec056 100644 --- a/lib/core/theme/theme_helper.dart +++ b/lib/core/theme/theme_helper.dart @@ -186,6 +186,8 @@ class PrimaryColors { Color get red => Color(0xFFCD4141); Color get transparentBlack => Color(0xFF1313264).withValues(alpha: 0.3); + + Color get gray60004 => Color(0xFFD4DAE2).withValues(alpha: 0.2); } PrimaryColors get appTheme => ThemeHelper().themeColor(); diff --git a/lib/features/bottom_navigation_bar/presentation/bloc/notifications_cubit/notifications_cubit.dart b/lib/features/bottom_navigation_bar/presentation/bloc/notifications_cubit/notifications_cubit.dart index d862efa7..5d9a27f6 100644 --- a/lib/features/bottom_navigation_bar/presentation/bloc/notifications_cubit/notifications_cubit.dart +++ b/lib/features/bottom_navigation_bar/presentation/bloc/notifications_cubit/notifications_cubit.dart @@ -1,11 +1,16 @@ +import 'package:eatvio_app/core/initialization_cubit/ws_manager.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/notifications_cubit/notifications_state.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class NotificationsCubit extends Cubit { int _notificationsCount = 0; List notifications = []; - - NotificationsCubit() : super(const NotificationsState.empty()); + final WsManager wsManager; + NotificationsCubit({required this.wsManager}) : super(const NotificationsState.empty()) { + wsManager.newNotificationsStream?.listen((event) { + incrementCounter(event.data); + }); + } incrementCounter(String data) { _notificationsCount++; 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 194f4da9..a072ad50 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 @@ -2,13 +2,9 @@ import 'dart:convert'; import 'dart:io'; import 'package:audioplayers/audioplayers.dart'; -import 'package:dart_pusher_channels/dart_pusher_channels.dart'; import 'package:easy_debounce/easy_debounce.dart'; -import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart'; -import 'package:eatvio_app/core/initialization_cubit/initialization_cubit.dart'; -import 'package:eatvio_app/core/initialization_cubit/initialization_state.dart'; -import 'package:eatvio_app/core/token_storage.dart'; +import 'package:eatvio_app/core/initialization_cubit/ws_manager.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'; @@ -16,41 +12,42 @@ import 'package:flutter_bloc/flutter_bloc.dart'; class ChatRoomCubit extends Cubit { final ChatRepository chatRepository; - final InitializationCubit initializationCubit; + final WsManager wsManager; int currentPage = 1; late final int lastPage; bool isLoading = false; - late final PusherChannelsClient client; - late final PrivateChannel privateChatChannel; - ChatRoomCubit({ required this.chatRepository, - required this.initializationCubit, + required this.wsManager, }) : super(ChatRoomState.loading()); loadNextPage(String chatId) async { 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: isLoading, - ),); + emit( + ChatRoomState.loaded( + chatRoom: (state as ChatRoomStateLoaded).chatRoom, + chatMessages: (state as ChatRoomStateLoaded).chatMessages, + isTyping: (state as ChatRoomStateLoaded).isTyping, + 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: isLoading, - ),); + emit( + ChatRoomState.loaded( + chatRoom: (state as ChatRoomStateLoaded).chatRoom, + chatMessages: [...(state as ChatRoomStateLoaded).chatMessages, ...chatMessages.messagesList], + isTyping: (state as ChatRoomStateLoaded).isTyping, + isLoading: isLoading, + ), + ); currentPage++; } catch (e) { @@ -66,38 +63,29 @@ class ChatRoomCubit extends Cubit { final chatMessages = await chatRepository.getChatMessages(chatId, currentPage); lastPage = chatMessages.meta.lastPage; - await _subscribeToChatChannel(initCubit: initializationCubit, chatId: chatId); + await _subscribeToChatChannel(chatId: chatId); - emit(ChatRoomState.loaded( - chatRoom: chatRoom, - chatMessages: chatMessages.messagesList, - isTyping: false, - isLoading: isLoading, - ),); + emit( + ChatRoomState.loaded( + chatRoom: chatRoom, + chatMessages: chatMessages.messagesList, + isTyping: false, + isLoading: isLoading, + ), + ); } catch (e) { emit(ChatRoomState.error(errorMessage: 'Ошибка получеия данных')); } } Future _subscribeToChatChannel({ - required InitializationCubit initCubit, required String chatId, }) async { - client = (initCubit.state as InitializationStateSuccess).client; - - privateChatChannel = await client.privateChannel( - 'private-direct-chat.$chatId', - authorizationDelegate: EndpointAuthorizableChannelTokenAuthorizationDelegate.forPrivateChannel( - authorizationEndpoint: Uri.parse(baseUrl + '/broadcasting/auth'), - headers: { - 'Authorization': 'Bearer ${await TokenStorage.getTokenFromStorage()}', - }, - ), - ); - - privateChatChannel.subscribe(); - - privateChatChannel.bind('App\\Events\\ChatMessageUpdated').listen((event) { + final client = wsManager.client; + if (client == null) return; + await wsManager.initPrivateDirectChatChannel(chatId); + wsManager.subscribeDirectChatChannel(); + wsManager.directChatStream?.listen((event) { AudioPlayer().play((AssetSource('audio/recive_message.wav'))); List messagesList = [ ChatMessageModel.fromJson(jsonDecode(event.data)['message']), @@ -113,7 +101,7 @@ class ChatRoomCubit extends Cubit { ); }); - privateChatChannel.bind('client-typing').listen((event) { + wsManager.clientTypingStream?.listen((event) { emit( ChatRoomState.loaded( chatRoom: (state as ChatRoomStateLoaded).chatRoom, @@ -152,7 +140,7 @@ class ChatRoomCubit extends Cubit { @override Future close() async { - privateChatChannel.unsubscribe(); + wsManager.unsubscribeDirectChatChannel(); super.close(); } } diff --git a/lib/features/chat/presentation/pages/chat_screen.dart b/lib/features/chat/presentation/pages/chat_screen.dart index 0a6d1b5a..3b76cb7c 100644 --- a/lib/features/chat/presentation/pages/chat_screen.dart +++ b/lib/features/chat/presentation/pages/chat_screen.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'dart:math'; import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart'; @@ -78,6 +79,7 @@ class _ChatScreenState extends State { resizeToAvoidBottomInset: true, appBar: AppBar( titleSpacing: 0, + backgroundColor: appTheme.gray100, centerTitle: false, title: Row( children: [ @@ -136,7 +138,7 @@ class _ChatScreenState extends State { children: [ Expanded( child: Container( - color: Color(0xFFF0F2F5), + color: appTheme.whiteA700, width: double.maxFinite, child: CustomScrollView( physics: BouncingScrollPhysics(), @@ -149,20 +151,16 @@ class _ChatScreenState extends State { delegate: SliverChildBuilderDelegate( (context, index) { if (index < state.chatMessages.length) { - return Column( - children: [ - if ((index + 1 != state.chatMessages.length && - DateTime.parse(state.chatMessages[index].updatedAt).day != - DateTime.parse(state.chatMessages[index + 1].updatedAt).day) || - index + 1 == state.chatMessages.length) - _messageDate(state.chatMessages[index].updatedAt), - ChatBubbleWidget( - sendTime: state.chatMessages[index].updatedAt, - text: state.chatMessages[index].data.trim(), - isCurrentUser: state.chatRoom.users['to_chat_user']!.id != state.chatMessages[index].authorId, - mediaData: state.chatMessages[index].mediaData ?? [], - ), - ], + if ((index + 1 != state.chatMessages.length && + DateTime.parse(state.chatMessages[index].updatedAt).day != DateTime.parse(state.chatMessages[index + 1].updatedAt).day) || + index + 1 == state.chatMessages.length) { + return _messageDate(state.chatMessages[index].updatedAt); + } + return ChatBubbleWidget( + sendTime: state.chatMessages[index].updatedAt, + 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( @@ -181,13 +179,6 @@ class _ChatScreenState extends State { ), Container( padding: EdgeInsets.symmetric(vertical: 8), - decoration: BoxDecoration( - border: Border( - top: BorderSide( - color: Colors.black.withValues(alpha: 0.1), - ), - ), - ), child: Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ @@ -231,57 +222,60 @@ class _ChatScreenState extends State { }, customBorder: CircleBorder(), child: Ink( - height: 32, - width: 32, + height: 40, + width: 40, child: Transform.rotate( - angle: 3.8, + angle: pi / 4, child: Icon( Icons.attach_file_rounded, - size: 24, + size: 30, color: Color(0xFF858E99), ), ), ), ), SizedBox(width: 8), - SizedBox( - width: MediaQuery.of(context).size.width - 96, - child: TextFormField( - controller: textEditingController, - maxLines: 5, - minLines: 1, - keyboardType: TextInputType.multiline, - style: TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1), - decoration: InputDecoration( - isDense: true, - contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), - hintText: 'Сообщение', - hintStyle: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w400, - height: 1, - color: Color(0xFFAEAEB2), - ), - filled: true, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(16), - borderSide: BorderSide( - color: Color(0xFFD1D1D6), - width: 1, + Expanded( + child: SizedBox( + height: 40, + child: TextFormField( + expands: true, + controller: textEditingController, + maxLines: null, + minLines: null, + keyboardType: TextInputType.multiline, + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1), + decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 10), + hintText: 'Сообщение', + hintStyle: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + height: 1, + color: Color(0xFFAEAEB2), ), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(16), - borderSide: BorderSide( - color: Color(0xFFD1D1D6), - width: 1, + filled: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(100), + borderSide: BorderSide( + color: Color(0xFFD1D1D6), + width: 1, + ), ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(16), - borderSide: BorderSide( - color: Color(0xFFD1D1D6), - width: 1, + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(100), + borderSide: BorderSide( + color: Color(0xFFD1D1D6), + width: 1, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(100), + borderSide: BorderSide( + color: Color(0xFFD1D1D6), + width: 1, + ), ), ), ), @@ -306,11 +300,11 @@ class _ChatScreenState extends State { color: appTheme.blueA400, shape: BoxShape.circle, ), - height: 32, - width: 32, + height: 40, + width: 40, child: Icon( Icons.arrow_upward_rounded, - size: 20, + size: 24, color: Colors.white, ), ), @@ -326,16 +320,18 @@ class _ChatScreenState extends State { } _messageDate(String date) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Text( - DateFormat.MMMMd('RU').format( - DateTime.parse(date), - ), - style: const TextStyle( - height: 1, - color: Color(0xFF8393A1), - fontWeight: FontWeight.w500, + return Center( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Text( + DateFormat.yMMMMd('RU').format( + DateTime.parse(date), + ), + style: const TextStyle( + height: 1, + color: Color(0xFF8393A1), + fontWeight: FontWeight.w500, + ), ), ), ); diff --git a/lib/features/chat/presentation/widgets/chat_bubble_widget.dart b/lib/features/chat/presentation/widgets/chat_bubble_widget.dart index 8ef43e8b..ca94dbf9 100644 --- a/lib/features/chat/presentation/widgets/chat_bubble_widget.dart +++ b/lib/features/chat/presentation/widgets/chat_bubble_widget.dart @@ -1,6 +1,8 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:dismissible_page/dismissible_page.dart'; import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/utils/date_time_utils.dart'; import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart'; import 'package:flutter/material.dart'; @@ -28,7 +30,7 @@ class ChatBubbleWidget extends StatelessWidget { alignment: isCurrentUser ? Alignment.centerRight : Alignment.centerLeft, child: DecoratedBox( decoration: BoxDecoration( - color: isCurrentUser ? Color(0xFF7F9CF5) : Colors.white, + color: isCurrentUser ? appTheme.gray50 : appTheme.gray60004, borderRadius: mediaData.length > 0 ? BorderRadius.circular(24) : BorderRadius.horizontal( @@ -108,15 +110,15 @@ class ChatBubbleWidget extends StatelessWidget { if (text.isNotEmpty) Text( text, - style: TextStyle(color: isCurrentUser ? Colors.white : Color(0xFF323F4B), height: 1), + style: TextStyles.textFS13FW400.copyWith(color: appTheme.black), ), - SizedBox(height: 4), + SizedBox(height: 8), Padding( padding: mediaData.length > 0 ? const EdgeInsets.only(right: 8, top: 4, left: 8) : EdgeInsets.zero, child: Text( DateTime.parse(sendTime).format('hh:mm'), style: TextStyle( - color: isCurrentUser ? Colors.white.withValues(alpha: 0.4) : Color(0xFF323F4B).withValues(alpha: 0.4), + color: Color(0xFF7B8794), height: 1, fontSize: 10, ), diff --git a/lib/features/chat/repositories/chat_repository.dart b/lib/features/chat/repositories/chat_repository.dart index 228709f6..67b49b73 100644 --- a/lib/features/chat/repositories/chat_repository.dart +++ b/lib/features/chat/repositories/chat_repository.dart @@ -4,7 +4,6 @@ import 'package:dio/dio.dart'; 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/chat_room_preview_model.dart'; -import 'package:eatvio_app/features/chat/models/websocket_info_model.dart'; import 'package:eatvio_app/features/chat/services/chat_service.dart'; import 'package:http_parser/http_parser.dart'; @@ -67,15 +66,7 @@ class ChatRepository { } catch (e) { rethrow; } - } - - Future getWebsocket() async { - try { - return (await chatService.getWebsocket()); - } catch (e) { - rethrow; - } - } + } Future getChatRoomIdByUserSlug({required String slug}) async { try { diff --git a/lib/features/chat/services/chat_service.dart b/lib/features/chat/services/chat_service.dart index f572b117..499f2584 100644 --- a/lib/features/chat/services/chat_service.dart +++ b/lib/features/chat/services/chat_service.dart @@ -2,7 +2,6 @@ import 'package:dio/dio.dart'; 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/chat_room_preview_model.dart'; -import 'package:eatvio_app/features/chat/models/websocket_info_model.dart'; import 'package:retrofit/retrofit.dart'; part 'chat_service.g.dart'; @@ -35,9 +34,6 @@ abstract class ChatService { @Path() required int page, }); - @GET('/api/profile/ws/params') - Future getWebsocket(); - @GET('/api/chat/get_by_user/{user}') Future getChatRoomIdByUserSlug({ @Path() required String user, diff --git a/lib/features/chat/services/chat_service.g.dart b/lib/features/chat/services/chat_service.g.dart index 978a0e43..5034c597 100644 --- a/lib/features/chat/services/chat_service.g.dart +++ b/lib/features/chat/services/chat_service.g.dart @@ -165,39 +165,6 @@ class _ChatService implements ChatService { return _value; } - @override - Future getWebsocket() async { - final _extra = {}; - final queryParameters = {}; - final _headers = {}; - const Map? _data = null; - final _options = _setStreamType(Options( - method: 'GET', - headers: _headers, - extra: _extra, - ) - .compose( - _dio.options, - '/api/profile/ws/params', - queryParameters: queryParameters, - data: _data, - ) - .copyWith( - baseUrl: _combineBaseUrls( - _dio.options.baseUrl, - baseUrl, - ))); - final _result = await _dio.fetch>(_options); - late WebsocketInfoModel _value; - try { - _value = WebsocketInfoModel.fromJson(_result.data!); - } on Object catch (e, s) { - errorLogger?.logError(e, s, _options); - rethrow; - } - return _value; - } - @override Future getChatRoomIdByUserSlug({required String user}) async { final _extra = {}; diff --git a/lib/features/diary/presentation/pages/diary_screen.dart b/lib/features/diary/presentation/pages/diary_screen.dart index c6c8e291..d0ef5bcb 100644 --- a/lib/features/diary/presentation/pages/diary_screen.dart +++ b/lib/features/diary/presentation/pages/diary_screen.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart'; import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart'; @@ -22,6 +24,9 @@ class _DiaryScreenState extends State { create: (BuildContext context) => sl()..fetchDiary(true), child: BlocBuilder( builder: (BuildContext context, DiaryState state) { + if (state is DiaryStateLoaded) { + log(state.diary.id.toString()); + } return Scaffold( backgroundColor: Colors.grey.shade200, appBar: AppBar( diff --git a/lib/features/feed/models/plan_pagination_model.dart b/lib/features/feed/models/plan_pagination_model.dart index 083bb162..09c2129f 100644 --- a/lib/features/feed/models/plan_pagination_model.dart +++ b/lib/features/feed/models/plan_pagination_model.dart @@ -39,7 +39,7 @@ class PlanModel with _$PlanModel { @JsonKey(name: "is_favorite") required bool isFavorite, @JsonKey(name: "min_price_of_options") required double minPriceOfOptions, @JsonKey(name: "max_price_of_options") required double maxPriceOfOptions, - @Default(false) bool wasLikedByMe, + @JsonKey(name: "was_liked_by_me") required bool wasLikedByMe, }) = _PlanModel; factory PlanModel.fromJson(Map json) => _$PlanModelFromJson(json); diff --git a/lib/features/feed/models/plan_pagination_model.freezed.dart b/lib/features/feed/models/plan_pagination_model.freezed.dart index 0a338d10..6df539a9 100644 --- a/lib/features/feed/models/plan_pagination_model.freezed.dart +++ b/lib/features/feed/models/plan_pagination_model.freezed.dart @@ -298,6 +298,7 @@ mixin _$PlanModel { double get minPriceOfOptions => throw _privateConstructorUsedError; @JsonKey(name: "max_price_of_options") double get maxPriceOfOptions => throw _privateConstructorUsedError; + @JsonKey(name: "was_liked_by_me") bool get wasLikedByMe => throw _privateConstructorUsedError; /// Serializes this PlanModel to a JSON map. @@ -335,7 +336,7 @@ abstract class $PlanModelCopyWith<$Res> { @JsonKey(name: "is_favorite") bool isFavorite, @JsonKey(name: "min_price_of_options") double minPriceOfOptions, @JsonKey(name: "max_price_of_options") double maxPriceOfOptions, - bool wasLikedByMe}); + @JsonKey(name: "was_liked_by_me") bool wasLikedByMe}); $PlanTypeCopyWith<$Res> get type; $PacketOptionsCopyWith<$Res>? get packet1Options; @@ -555,7 +556,7 @@ abstract class _$$PlanModelImplCopyWith<$Res> @JsonKey(name: "is_favorite") bool isFavorite, @JsonKey(name: "min_price_of_options") double minPriceOfOptions, @JsonKey(name: "max_price_of_options") double maxPriceOfOptions, - bool wasLikedByMe}); + @JsonKey(name: "was_liked_by_me") bool wasLikedByMe}); @override $PlanTypeCopyWith<$Res> get type; @@ -711,7 +712,7 @@ class _$PlanModelImpl implements _PlanModel { @JsonKey(name: "is_favorite") required this.isFavorite, @JsonKey(name: "min_price_of_options") required this.minPriceOfOptions, @JsonKey(name: "max_price_of_options") required this.maxPriceOfOptions, - this.wasLikedByMe = false}); + @JsonKey(name: "was_liked_by_me") required this.wasLikedByMe}); factory _$PlanModelImpl.fromJson(Map json) => _$$PlanModelImplFromJson(json); @@ -774,7 +775,7 @@ class _$PlanModelImpl implements _PlanModel { @JsonKey(name: "max_price_of_options") final double maxPriceOfOptions; @override - @JsonKey() + @JsonKey(name: "was_liked_by_me") final bool wasLikedByMe; @override @@ -865,29 +866,30 @@ class _$PlanModelImpl implements _PlanModel { abstract class _PlanModel implements PlanModel { const factory _PlanModel( - {@JsonKey(name: "id") required final int id, - @JsonKey(name: "slug") required final String slug, - @JsonKey(name: "name") required final String name, - @JsonKey(name: "image") required final String image, - @JsonKey(name: "duration") required final int duration, - @JsonKey(name: "state") required final String state, - @JsonKey(name: "type", fromJson: planTypeFromJson) - required final PlanType type, - @JsonKey(name: "details") required final String details, - @JsonKey(name: "packets_count") required final int packetsCount, - @JsonKey(name: "packet1_options") final PacketOptions? packet1Options, - @JsonKey(name: "packet2_options") final PacketOptions? packet2Options, - @JsonKey(name: "packet3_options") final PacketOptions? packet3Options, - @JsonKey(name: "author") required final Author author, - @JsonKey(name: "total_likes") required final int totalLikes, - @JsonKey(name: "total_views") required final int totalViews, - @JsonKey(name: "total_comments") required final int totalComments, - @JsonKey(name: "is_favorite") required final bool isFavorite, - @JsonKey(name: "min_price_of_options") - required final double minPriceOfOptions, - @JsonKey(name: "max_price_of_options") - required final double maxPriceOfOptions, - final bool wasLikedByMe}) = _$PlanModelImpl; + {@JsonKey(name: "id") required final int id, + @JsonKey(name: "slug") required final String slug, + @JsonKey(name: "name") required final String name, + @JsonKey(name: "image") required final String image, + @JsonKey(name: "duration") required final int duration, + @JsonKey(name: "state") required final String state, + @JsonKey(name: "type", fromJson: planTypeFromJson) + required final PlanType type, + @JsonKey(name: "details") required final String details, + @JsonKey(name: "packets_count") required final int packetsCount, + @JsonKey(name: "packet1_options") final PacketOptions? packet1Options, + @JsonKey(name: "packet2_options") final PacketOptions? packet2Options, + @JsonKey(name: "packet3_options") final PacketOptions? packet3Options, + @JsonKey(name: "author") required final Author author, + @JsonKey(name: "total_likes") required final int totalLikes, + @JsonKey(name: "total_views") required final int totalViews, + @JsonKey(name: "total_comments") required final int totalComments, + @JsonKey(name: "is_favorite") required final bool isFavorite, + @JsonKey(name: "min_price_of_options") + required final double minPriceOfOptions, + @JsonKey(name: "max_price_of_options") + required final double maxPriceOfOptions, + @JsonKey(name: "was_liked_by_me") required final bool wasLikedByMe}) = + _$PlanModelImpl; factory _PlanModel.fromJson(Map json) = _$PlanModelImpl.fromJson; @@ -950,6 +952,7 @@ abstract class _PlanModel implements PlanModel { @JsonKey(name: "max_price_of_options") double get maxPriceOfOptions; @override + @JsonKey(name: "was_liked_by_me") bool get wasLikedByMe; /// Create a copy of PlanModel diff --git a/lib/features/feed/models/plan_pagination_model.g.dart b/lib/features/feed/models/plan_pagination_model.g.dart index abfe3930..0db54936 100644 --- a/lib/features/feed/models/plan_pagination_model.g.dart +++ b/lib/features/feed/models/plan_pagination_model.g.dart @@ -54,7 +54,7 @@ _$PlanModelImpl _$$PlanModelImplFromJson(Map json) => isFavorite: json['is_favorite'] as bool, minPriceOfOptions: (json['min_price_of_options'] as num).toDouble(), maxPriceOfOptions: (json['max_price_of_options'] as num).toDouble(), - wasLikedByMe: json['wasLikedByMe'] as bool? ?? false, + wasLikedByMe: json['was_liked_by_me'] as bool, ); Map _$$PlanModelImplToJson(_$PlanModelImpl instance) => @@ -78,7 +78,7 @@ Map _$$PlanModelImplToJson(_$PlanModelImpl instance) => 'is_favorite': instance.isFavorite, 'min_price_of_options': instance.minPriceOfOptions, 'max_price_of_options': instance.maxPriceOfOptions, - 'wasLikedByMe': instance.wasLikedByMe, + 'was_liked_by_me': instance.wasLikedByMe, }; _$PacketOptionsImpl _$$PacketOptionsImplFromJson(Map json) => diff --git a/lib/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart b/lib/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart index 87cdd95d..87e8b342 100644 --- a/lib/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart +++ b/lib/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart @@ -9,7 +9,7 @@ import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_sta import 'package:eatvio_app/features/feed/repositories/feed_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -class DietsCubit extends Cubit{ +class DietsCubit extends Cubit { final FeedRepository feedRepository; final DietRepository dietsRepository; int currentPage = 1; @@ -52,7 +52,6 @@ class DietsCubit extends Cubit{ super.close(); } - void changeTypeFilter(int index) { emit(state.copyWith(filterState: state.filterState.copyWith(typeIndex: index), errorMessage: null)); EasyDebounce.debounce( @@ -64,7 +63,6 @@ class DietsCubit extends Cubit{ ); } - void changeSortingType(int index) { emit(state.copyWith(filterState: state.filterState.copyWith(sortingIndex: index), errorMessage: null)); EasyDebounce.debounce( @@ -76,7 +74,6 @@ class DietsCubit extends Cubit{ ); } - void changeDurationFilter(int index) { emit(state.copyWith(filterState: state.filterState.copyWith(durationIndex: index), errorMessage: null)); EasyDebounce.debounce( @@ -153,6 +150,7 @@ class DietsCubit extends Cubit{ Future likeDiet(int id, int index) async { final currentDiets = state.dietsList; + if (currentDiets.isEmpty) return; final dietsForModification = [...currentDiets]; final currentDiet = currentDiets[index]; if (currentDiet.wasLikedByMe) { diff --git a/lib/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart b/lib/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart index 10f5b8ab..5c2f28fd 100644 --- a/lib/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart +++ b/lib/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart @@ -284,4 +284,58 @@ class PlansCubit extends Cubit { }, ); } + + Future likePlan(String slug, int index) async { + final currentPlans = state.plans; + final plansForModification = [...currentPlans]; + final currentPlan = currentPlans[index]; + if (currentPlan.wasLikedByMe) { + plansForModification[index] = currentPlan.copyWith( + totalLikes: currentPlan.totalLikes - 1, + wasLikedByMe: false, + ); + emit( + state.copyWith( + plans: [ + ...plansForModification, + ], + isLoading: isLoading, + errorMessage: null, + ), + ); + } else { + plansForModification[index] = currentPlan.copyWith( + totalLikes: currentPlan.totalLikes + 1, + wasLikedByMe: true, + ); + emit( + state.copyWith( + plans: [ + ...plansForModification, + ], + isLoading: isLoading, + errorMessage: null, + ), + ); + } + EasyDebounce.debounce( + 'like_plan', + Duration(milliseconds: 200), + () async { + try { + await planRepository.likePlan(slug); + } catch (e) { + log(e.toString()); + emit( + state.copyWith( + plans: [ + ...currentPlans, + ], + isLoading: isLoading, + ), + ); + } + }, + ); + } } diff --git a/lib/features/feed/presentation/widgets/plan_widget.dart b/lib/features/feed/presentation/widgets/plan_widget.dart index f51dac71..56d31f9c 100644 --- a/lib/features/feed/presentation/widgets/plan_widget.dart +++ b/lib/features/feed/presentation/widgets/plan_widget.dart @@ -5,7 +5,6 @@ import 'package:eatvio_app/core/widgets/info_row.dart'; import 'package:eatvio_app/core/widgets/feed_icon_button.dart'; import 'package:eatvio_app/features/diets/presentation/widgets/info_bouble.dart'; import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; -import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart'; @@ -183,7 +182,7 @@ class _PlanWidgetState extends State { iconColor: widget.plan.wasLikedByMe ? appTheme.blueA400 : null, backgroundColor: widget.plan.wasLikedByMe ? appTheme.transparentBlue : null, onTap: () async { - await context.read().likeDiet(widget.plan.id, widget.index); + await context.read().likePlan(widget.plan.slug, widget.index); }, ), SizedBox( diff --git a/lib/features/plans/presentation/pages/about_plan_screen.dart b/lib/features/plans/presentation/pages/about_plan_screen.dart index 3392e523..db415e15 100644 --- a/lib/features/plans/presentation/pages/about_plan_screen.dart +++ b/lib/features/plans/presentation/pages/about_plan_screen.dart @@ -363,13 +363,11 @@ class _AboutPlanScreenState extends State { 'assets/icons/heart.svg', width: 14, height: 12, - - /// TODO задача для бека на добавление wasLokedByMe? - colorFilter: ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn), + colorFilter: state.planModel?.wasLikedByMe == true ? ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn) : null, ), SizedBox(width: 8), Text( - widget.plan.totalLikes.toString(), + state.planModel?.totalLikes.toString() ?? '0', style: TextStyles.textXSSemibold.copyWith(color: appTheme.black), ), ], diff --git a/lib/features/profile/presentation/pages/profile_screen.dart b/lib/features/profile/presentation/pages/profile_screen.dart index b838f707..6e73775e 100644 --- a/lib/features/profile/presentation/pages/profile_screen.dart +++ b/lib/features/profile/presentation/pages/profile_screen.dart @@ -59,7 +59,6 @@ class _ProfileScreenState extends State { late bool canSub; bool isFirstFetch = true; int _currentBullet = 0; - @override void initState() { super.initState(); @@ -338,7 +337,7 @@ class _ProfileScreenState extends State { padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: InkWell( onTap: () { - GoRouter.of(context).pushNamed('blogPostCreateScreen'); + GoRouter.of(context).pushNamed('blogPostCreateScreen'); }, borderRadius: BorderRadius.circular(8), child: Ink( @@ -911,15 +910,15 @@ class _ProfileScreenState extends State { child: Text('Посмотреть'), ), if (widget.author == null) - PopupMenuItem( - value: 'replace', - child: Text('Заменить'), - ), + PopupMenuItem( + value: 'replace', + child: Text('Заменить'), + ), if (widget.author == null) - PopupMenuItem( - value: 'delete', - child: Text('Удалить'), - ), + PopupMenuItem( + value: 'delete', + child: Text('Удалить'), + ), ], child: Hero( tag: state.profile.profilePhotoUrl, @@ -1102,7 +1101,7 @@ class _ProfileScreenState extends State { ); } - _charts() { + _charts() { return Material( color: Colors.white, borderRadius: BorderRadius.circular(16), diff --git a/lib/main.dart b/lib/main.dart index 8672683a..dfbb0b9a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,4 @@ import 'package:eatvio_app/core/app_router.dart'; -import 'package:eatvio_app/core/initialization_cubit/initialization_cubit.dart'; import 'package:eatvio_app/core/locator_service.dart' as di; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; @@ -26,8 +25,7 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MultiBlocProvider( - providers: [ - BlocProvider(create: (context) => sl()..runInitialization()), + providers: [ BlocProvider(create: (context) => sl()), BlocProvider(create: (context) => sl()), BlocProvider(create: (context) => sl()..loadFeedBlogPostList()), diff --git a/test/features/profile/services/profile_service_test.mocks.dart b/test/features/profile/services/profile_service_test.mocks.dart index c853a94e..bb0c633a 100644 --- a/test/features/profile/services/profile_service_test.mocks.dart +++ b/test/features/profile/services/profile_service_test.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.5 from annotations +// Mocks generated by Mockito 5.4.4 from annotations // in eatvio_app/test/features/profile/services/profile_service_test.dart. // Do not manually edit this file. @@ -25,7 +25,6 @@ import 'package:mockito/mockito.dart' as _i1; // ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member -// ignore_for_file: must_be_immutable // ignore_for_file: prefer_const_constructors // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types