fix(router): rename duplicate notifications route name
This commit is contained in:
@@ -169,7 +169,7 @@ void _handleNotificationNavigation(Map<String, dynamic> data) {
|
||||
|
||||
switch (command) {
|
||||
case 'ALERT':
|
||||
appRouter.go(AppRoutes.deviceAlertsNotifications);
|
||||
appRouter.go(AppRoutes.deviceNotifications);
|
||||
default:
|
||||
debugPrint('[Notification] unhandled command: $command');
|
||||
}
|
||||
|
||||
@@ -97,9 +97,9 @@ void configureAppRouter() {
|
||||
pageBuilder: CustomerServiceBuilder().buildPage,
|
||||
),
|
||||
GoRoute(
|
||||
path: 'device_alerts_notifications',
|
||||
name: 'device_alerts_notifications',
|
||||
pageBuilder: const DeviceAlertsBuilder().buildPage,
|
||||
path: 'notifications',
|
||||
name: 'legacy_notifications',
|
||||
pageBuilder: const DeviceNotificationsBuilder().buildPage,
|
||||
),
|
||||
GoRoute(
|
||||
path: 'account_settings',
|
||||
@@ -305,11 +305,6 @@ void configureAppRouter() {
|
||||
name: 'language',
|
||||
pageBuilder: LanguageBuilder().buildPage,
|
||||
),
|
||||
GoRoute(
|
||||
path: 'legacy_notifications',
|
||||
name: 'legacy_notifications',
|
||||
pageBuilder: LegacyNotificationsBuilder().buildPage,
|
||||
),
|
||||
GoRoute(
|
||||
path: 'remote_on_off',
|
||||
name: 'remote_on_off',
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export 'src/features/alerts/alerts_builder.dart' show DeviceAlertsBuilder;
|
||||
export 'src/features/notifications/notifications_builder.dart'
|
||||
show DeviceNotificationsBuilder;
|
||||
export 'src/features/control_panel/control_panel_builder.dart';
|
||||
export 'src/shared/widgets/device_map.dart';
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:sf_infrastructure/sf_infrastructure.dart';
|
||||
|
||||
import '../../../../core/domain/entities/alert_entity.dart';
|
||||
import '../../../../core/domain/repositories/alerts_repository.dart';
|
||||
import '../../../../core/providers/alerts_repository_provider.dart';
|
||||
import 'alerts_view_state.dart';
|
||||
|
||||
final alertsViewModelProvider =
|
||||
NotifierProvider.autoDispose<AlertsViewModel, AlertsViewState>(
|
||||
AlertsViewModel.new,
|
||||
);
|
||||
|
||||
class AlertsViewModel extends Notifier<AlertsViewState> {
|
||||
late final AlertsRepository _repository;
|
||||
late final WebSocketService _webSocket;
|
||||
StreamSubscription<WebSocketEvent>? _webSocketSubscription;
|
||||
|
||||
static const int _pageSize = 20;
|
||||
|
||||
@override
|
||||
AlertsViewState build() {
|
||||
_repository = ref.read(alertsRepositoryProvider);
|
||||
_webSocket = GetIt.I<WebSocketService>();
|
||||
|
||||
_webSocketSubscription = _webSocket.events.listen(_onWebSocketEvent);
|
||||
ref.onDispose(() => _webSocketSubscription?.cancel());
|
||||
|
||||
Future.microtask(_load);
|
||||
return const AlertsViewState();
|
||||
}
|
||||
|
||||
void _onWebSocketEvent(WebSocketEvent event) {
|
||||
if (event is! AlertEvent) return;
|
||||
|
||||
final newAlert = AlertEntity(
|
||||
id: '',
|
||||
deviceIdentificator: event.deviceIdentificator,
|
||||
deviceName: '',
|
||||
type: event.type,
|
||||
createdAt: DateTime.now().millisecondsSinceEpoch,
|
||||
);
|
||||
|
||||
if (state.selectedType != null && state.selectedType != event.type) return;
|
||||
|
||||
state = state.copyWith(alerts: [newAlert, ...state.alerts]);
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
try {
|
||||
final (alerts, totalPages) = await _repository.getAlerts(
|
||||
page: 1,
|
||||
pageSize: _pageSize,
|
||||
type: state.selectedType,
|
||||
);
|
||||
if (!ref.mounted) return;
|
||||
|
||||
state = state.copyWith(
|
||||
alerts: alerts,
|
||||
isLoading: false,
|
||||
currentPage: 1,
|
||||
hasMore: totalPages > 1,
|
||||
error: null,
|
||||
);
|
||||
} catch (_) {
|
||||
if (!ref.mounted) return;
|
||||
state = state.copyWith(
|
||||
isLoading: false,
|
||||
error: AlertsError.loadFailed,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadMore() async {
|
||||
if (state.isLoadingMore || !state.hasMore) return;
|
||||
|
||||
state = state.copyWith(isLoadingMore: true);
|
||||
|
||||
try {
|
||||
final nextPage = state.currentPage + 1;
|
||||
final (alerts, totalPages) = await _repository.getAlerts(
|
||||
page: nextPage,
|
||||
pageSize: _pageSize,
|
||||
type: state.selectedType,
|
||||
);
|
||||
if (!ref.mounted) return;
|
||||
|
||||
state = state.copyWith(
|
||||
alerts: [...state.alerts, ...alerts],
|
||||
isLoadingMore: false,
|
||||
currentPage: nextPage,
|
||||
hasMore: nextPage < totalPages,
|
||||
);
|
||||
} catch (_) {
|
||||
if (!ref.mounted) return;
|
||||
state = state.copyWith(isLoadingMore: false);
|
||||
}
|
||||
}
|
||||
|
||||
void filterByType(String? type) {
|
||||
state = state.copyWith(
|
||||
selectedType: type,
|
||||
isLoading: true,
|
||||
alerts: [],
|
||||
currentPage: 1,
|
||||
hasMore: false,
|
||||
);
|
||||
_load();
|
||||
}
|
||||
|
||||
void clearError() {
|
||||
if (state.error != null) state = state.copyWith(error: null);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import '../../../../core/domain/entities/alert_entity.dart';
|
||||
|
||||
part 'alerts_view_state.freezed.dart';
|
||||
|
||||
enum AlertsError { loadFailed }
|
||||
|
||||
@freezed
|
||||
abstract class AlertsViewState with _$AlertsViewState {
|
||||
const factory AlertsViewState({
|
||||
@Default([]) List<AlertEntity> alerts,
|
||||
@Default(true) bool isLoading,
|
||||
@Default(false) bool isLoadingMore,
|
||||
@Default(1) int currentPage,
|
||||
@Default(false) bool hasMore,
|
||||
String? selectedType,
|
||||
AlertsError? error,
|
||||
}) = _AlertsViewState;
|
||||
}
|
||||
@@ -1,295 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// 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 'alerts_view_state.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$AlertsViewState {
|
||||
|
||||
List<AlertEntity> get alerts; bool get isLoading; bool get isLoadingMore; int get currentPage; bool get hasMore; String? get selectedType; AlertsError? get error;
|
||||
/// Create a copy of AlertsViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$AlertsViewStateCopyWith<AlertsViewState> get copyWith => _$AlertsViewStateCopyWithImpl<AlertsViewState>(this as AlertsViewState, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is AlertsViewState&&const DeepCollectionEquality().equals(other.alerts, alerts)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isLoadingMore, isLoadingMore) || other.isLoadingMore == isLoadingMore)&&(identical(other.currentPage, currentPage) || other.currentPage == currentPage)&&(identical(other.hasMore, hasMore) || other.hasMore == hasMore)&&(identical(other.selectedType, selectedType) || other.selectedType == selectedType)&&(identical(other.error, error) || other.error == error));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(alerts),isLoading,isLoadingMore,currentPage,hasMore,selectedType,error);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AlertsViewState(alerts: $alerts, isLoading: $isLoading, isLoadingMore: $isLoadingMore, currentPage: $currentPage, hasMore: $hasMore, selectedType: $selectedType, error: $error)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $AlertsViewStateCopyWith<$Res> {
|
||||
factory $AlertsViewStateCopyWith(AlertsViewState value, $Res Function(AlertsViewState) _then) = _$AlertsViewStateCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
List<AlertEntity> alerts, bool isLoading, bool isLoadingMore, int currentPage, bool hasMore, String? selectedType, AlertsError? error
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$AlertsViewStateCopyWithImpl<$Res>
|
||||
implements $AlertsViewStateCopyWith<$Res> {
|
||||
_$AlertsViewStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final AlertsViewState _self;
|
||||
final $Res Function(AlertsViewState) _then;
|
||||
|
||||
/// Create a copy of AlertsViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? alerts = null,Object? isLoading = null,Object? isLoadingMore = null,Object? currentPage = null,Object? hasMore = null,Object? selectedType = freezed,Object? error = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
alerts: null == alerts ? _self.alerts : alerts // ignore: cast_nullable_to_non_nullable
|
||||
as List<AlertEntity>,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
|
||||
as bool,isLoadingMore: null == isLoadingMore ? _self.isLoadingMore : isLoadingMore // ignore: cast_nullable_to_non_nullable
|
||||
as bool,currentPage: null == currentPage ? _self.currentPage : currentPage // ignore: cast_nullable_to_non_nullable
|
||||
as int,hasMore: null == hasMore ? _self.hasMore : hasMore // ignore: cast_nullable_to_non_nullable
|
||||
as bool,selectedType: freezed == selectedType ? _self.selectedType : selectedType // ignore: cast_nullable_to_non_nullable
|
||||
as String?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
|
||||
as AlertsError?,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [AlertsViewState].
|
||||
extension AlertsViewStatePatterns on AlertsViewState {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _AlertsViewState value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _AlertsViewState() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _AlertsViewState value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _AlertsViewState():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _AlertsViewState value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _AlertsViewState() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( List<AlertEntity> alerts, bool isLoading, bool isLoadingMore, int currentPage, bool hasMore, String? selectedType, AlertsError? error)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _AlertsViewState() when $default != null:
|
||||
return $default(_that.alerts,_that.isLoading,_that.isLoadingMore,_that.currentPage,_that.hasMore,_that.selectedType,_that.error);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( List<AlertEntity> alerts, bool isLoading, bool isLoadingMore, int currentPage, bool hasMore, String? selectedType, AlertsError? error) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _AlertsViewState():
|
||||
return $default(_that.alerts,_that.isLoading,_that.isLoadingMore,_that.currentPage,_that.hasMore,_that.selectedType,_that.error);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( List<AlertEntity> alerts, bool isLoading, bool isLoadingMore, int currentPage, bool hasMore, String? selectedType, AlertsError? error)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _AlertsViewState() when $default != null:
|
||||
return $default(_that.alerts,_that.isLoading,_that.isLoadingMore,_that.currentPage,_that.hasMore,_that.selectedType,_that.error);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class _AlertsViewState implements AlertsViewState {
|
||||
const _AlertsViewState({final List<AlertEntity> alerts = const [], this.isLoading = true, this.isLoadingMore = false, this.currentPage = 1, this.hasMore = false, this.selectedType, this.error}): _alerts = alerts;
|
||||
|
||||
|
||||
final List<AlertEntity> _alerts;
|
||||
@override@JsonKey() List<AlertEntity> get alerts {
|
||||
if (_alerts is EqualUnmodifiableListView) return _alerts;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_alerts);
|
||||
}
|
||||
|
||||
@override@JsonKey() final bool isLoading;
|
||||
@override@JsonKey() final bool isLoadingMore;
|
||||
@override@JsonKey() final int currentPage;
|
||||
@override@JsonKey() final bool hasMore;
|
||||
@override final String? selectedType;
|
||||
@override final AlertsError? error;
|
||||
|
||||
/// Create a copy of AlertsViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$AlertsViewStateCopyWith<_AlertsViewState> get copyWith => __$AlertsViewStateCopyWithImpl<_AlertsViewState>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AlertsViewState&&const DeepCollectionEquality().equals(other._alerts, _alerts)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isLoadingMore, isLoadingMore) || other.isLoadingMore == isLoadingMore)&&(identical(other.currentPage, currentPage) || other.currentPage == currentPage)&&(identical(other.hasMore, hasMore) || other.hasMore == hasMore)&&(identical(other.selectedType, selectedType) || other.selectedType == selectedType)&&(identical(other.error, error) || other.error == error));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_alerts),isLoading,isLoadingMore,currentPage,hasMore,selectedType,error);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AlertsViewState(alerts: $alerts, isLoading: $isLoading, isLoadingMore: $isLoadingMore, currentPage: $currentPage, hasMore: $hasMore, selectedType: $selectedType, error: $error)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$AlertsViewStateCopyWith<$Res> implements $AlertsViewStateCopyWith<$Res> {
|
||||
factory _$AlertsViewStateCopyWith(_AlertsViewState value, $Res Function(_AlertsViewState) _then) = __$AlertsViewStateCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
List<AlertEntity> alerts, bool isLoading, bool isLoadingMore, int currentPage, bool hasMore, String? selectedType, AlertsError? error
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$AlertsViewStateCopyWithImpl<$Res>
|
||||
implements _$AlertsViewStateCopyWith<$Res> {
|
||||
__$AlertsViewStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _AlertsViewState _self;
|
||||
final $Res Function(_AlertsViewState) _then;
|
||||
|
||||
/// Create a copy of AlertsViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? alerts = null,Object? isLoading = null,Object? isLoadingMore = null,Object? currentPage = null,Object? hasMore = null,Object? selectedType = freezed,Object? error = freezed,}) {
|
||||
return _then(_AlertsViewState(
|
||||
alerts: null == alerts ? _self._alerts : alerts // ignore: cast_nullable_to_non_nullable
|
||||
as List<AlertEntity>,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
|
||||
as bool,isLoadingMore: null == isLoadingMore ? _self.isLoadingMore : isLoadingMore // ignore: cast_nullable_to_non_nullable
|
||||
as bool,currentPage: null == currentPage ? _self.currentPage : currentPage // ignore: cast_nullable_to_non_nullable
|
||||
as int,hasMore: null == hasMore ? _self.hasMore : hasMore // ignore: cast_nullable_to_non_nullable
|
||||
as bool,selectedType: freezed == selectedType ? _self.selectedType : selectedType // ignore: cast_nullable_to_non_nullable
|
||||
as String?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
|
||||
as AlertsError?,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
@@ -132,17 +132,6 @@ class _Header extends ConsumerWidget {
|
||||
'assets/shared/images/logo_sf.svg',
|
||||
height: SizeUtils.getByScreen(small: 36, big: 36),
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: SizeUtils.getByScreen(small: 10, big: 10),
|
||||
child: IconButton(
|
||||
onPressed: () => navigationContract.pushTo(AppRoutes.deviceAlertsNotifications),
|
||||
icon: Icon(
|
||||
Icons.notifications_outlined,
|
||||
size: SizeUtils.getByScreen(small: 26, big: 24),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,17 +3,17 @@ import 'package:get_it/get_it.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:navigation/navigation.dart';
|
||||
|
||||
import 'presentation/alerts_screen.dart';
|
||||
import 'presentation/notifications_screen.dart';
|
||||
|
||||
class DeviceAlertsBuilder {
|
||||
const DeviceAlertsBuilder();
|
||||
class DeviceNotificationsBuilder {
|
||||
const DeviceNotificationsBuilder();
|
||||
|
||||
Page<void> buildPage(BuildContext context, GoRouterState state) {
|
||||
final NavigationContract navigationContract = GetIt.I<NavigationContract>();
|
||||
|
||||
return MaterialPage<void>(
|
||||
key: state.pageKey,
|
||||
child: AlertsScreen(navigationContract: navigationContract),
|
||||
child: NotificationsScreen(navigationContract: navigationContract),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,35 +1,30 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:legacy_theme/legacy_theme.dart';
|
||||
import 'package:control_panel/src/core/domain/entities/alert_entity.dart';
|
||||
import 'package:control_panel/src/features/notifications/presentation/providers/notifications_feed_provider.dart';
|
||||
import 'package:control_panel/src/features/notifications/presentation/providers/notifications_filter_provider.dart';
|
||||
import 'package:control_panel/src/features/notifications/presentation/widgets/notification_card.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:legacy_theme/legacy_theme.dart';
|
||||
import 'package:navigation/navigation.dart';
|
||||
import 'package:sf_localizations/sf_localizations.dart';
|
||||
import 'package:sf_shared/sf_shared.dart';
|
||||
import 'package:utils/utils.dart';
|
||||
|
||||
import 'state/alerts_view_model.dart';
|
||||
import 'widgets/alert_card.dart';
|
||||
|
||||
class AlertsScreen extends ConsumerWidget {
|
||||
class NotificationsScreen extends ConsumerWidget {
|
||||
final NavigationContract navigationContract;
|
||||
|
||||
const AlertsScreen({super.key, required this.navigationContract});
|
||||
const NotificationsScreen({super.key, required this.navigationContract});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final primaryColor = context.sfColors.legacyPrimary;
|
||||
final vm = ref.read(alertsViewModelProvider.notifier);
|
||||
final (isLoading, selectedType) = ref.watch(
|
||||
alertsViewModelProvider.select((s) => (s.isLoading, s.selectedType)),
|
||||
);
|
||||
final selectedType = ref.watch(notificationsFilterProvider);
|
||||
final feedAsync = ref.watch(notificationsFeedProvider);
|
||||
|
||||
ref.listen(alertsViewModelProvider.select((s) => s.error), (_, error) {
|
||||
if (error == null) return;
|
||||
showTopSnackbar(
|
||||
context,
|
||||
message: context.translate(I18n.alertsLoadError),
|
||||
type: MessageType.error,
|
||||
);
|
||||
vm.clearError();
|
||||
ref.listen(notificationsFeedProvider, (prev, next) async {
|
||||
if (next.hasError && prev?.hasError != true) {
|
||||
await showErrorDialog(context, I18n.alertsLoadError);
|
||||
}
|
||||
});
|
||||
|
||||
return Scaffold(
|
||||
@@ -49,7 +44,7 @@ class AlertsScreen extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
context.translate(I18n.alertsTitle).toUpperCase(),
|
||||
context.translate(I18n.notificationsLegacyTitle).toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 20, big: 19),
|
||||
fontWeight: FontWeight.w500,
|
||||
@@ -62,12 +57,19 @@ class AlertsScreen extends ConsumerWidget {
|
||||
children: [
|
||||
_FilterChips(
|
||||
selectedType: selectedType,
|
||||
onSelected: vm.filterByType,
|
||||
onSelected: ref.read(notificationsFilterProvider.notifier).select,
|
||||
),
|
||||
Expanded(
|
||||
child: isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: const _AlertsList(),
|
||||
child: feedAsync.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (_, __) => Center(
|
||||
child: Text(context.translate(I18n.alertsLoadError)),
|
||||
),
|
||||
data: (feed) => _NotificationsList(
|
||||
notifications: feed.notifications,
|
||||
isLoadingMore: feed.isLoadingMore,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -75,14 +77,14 @@ class AlertsScreen extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _FilterChips extends ConsumerWidget {
|
||||
class _FilterChips extends StatelessWidget {
|
||||
final String? selectedType;
|
||||
final ValueChanged<String?> onSelected;
|
||||
|
||||
const _FilterChips({required this.selectedType, required this.onSelected});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
Widget build(BuildContext context) {
|
||||
final primaryColor = context.sfColors.legacyPrimary;
|
||||
|
||||
final filters = [
|
||||
@@ -131,14 +133,21 @@ class _FilterChips extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _AlertsList extends ConsumerStatefulWidget {
|
||||
const _AlertsList();
|
||||
class _NotificationsList extends ConsumerStatefulWidget {
|
||||
final List<AlertEntity> notifications;
|
||||
final bool isLoadingMore;
|
||||
|
||||
const _NotificationsList({
|
||||
required this.notifications,
|
||||
required this.isLoadingMore,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<_AlertsList> createState() => _AlertsListState();
|
||||
ConsumerState<_NotificationsList> createState() =>
|
||||
_NotificationsListState();
|
||||
}
|
||||
|
||||
class _AlertsListState extends ConsumerState<_AlertsList> {
|
||||
class _NotificationsListState extends ConsumerState<_NotificationsList> {
|
||||
final _scrollController = ScrollController();
|
||||
|
||||
@override
|
||||
@@ -156,17 +165,15 @@ class _AlertsListState extends ConsumerState<_AlertsList> {
|
||||
void _onScroll() {
|
||||
if (_scrollController.position.pixels >=
|
||||
_scrollController.position.maxScrollExtent - 200) {
|
||||
ref.read(alertsViewModelProvider.notifier).loadMore();
|
||||
ref.read(notificationsFeedProvider.notifier).loadMore();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final (alerts, isLoadingMore) = ref.watch(
|
||||
alertsViewModelProvider.select((s) => (s.alerts, s.isLoadingMore)),
|
||||
);
|
||||
final notifications = widget.notifications;
|
||||
|
||||
if (alerts.isEmpty) {
|
||||
if (notifications.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -195,15 +202,15 @@ class _AlertsListState extends ConsumerState<_AlertsList> {
|
||||
horizontal: SizeUtils.getByScreen(small: 16, big: 14),
|
||||
vertical: SizeUtils.getByScreen(small: 8, big: 6),
|
||||
),
|
||||
itemCount: alerts.length + (isLoadingMore ? 1 : 0),
|
||||
itemCount: notifications.length + (widget.isLoadingMore ? 1 : 0),
|
||||
itemBuilder: (context, index) {
|
||||
if (index == alerts.length) {
|
||||
if (index == notifications.length) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
return AlertCard(alert: alerts[index]);
|
||||
return NotificationCard(notification: notifications[index]);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:control_panel/src/core/domain/entities/alert_entity.dart';
|
||||
import 'package:control_panel/src/core/providers/alerts_repository_provider.dart';
|
||||
import 'package:control_panel/src/features/notifications/presentation/providers/notifications_filter_provider.dart';
|
||||
import 'package:legacy_device_state/legacy_device_state.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:sf_infrastructure/sf_infrastructure.dart';
|
||||
|
||||
part 'notifications_feed_provider.g.dart';
|
||||
|
||||
const int _pageSize = 20;
|
||||
|
||||
class NotificationsFeedState {
|
||||
const NotificationsFeedState({
|
||||
this.notifications = const [],
|
||||
this.currentPage = 1,
|
||||
this.hasMore = false,
|
||||
this.isLoadingMore = false,
|
||||
});
|
||||
|
||||
final List<AlertEntity> notifications;
|
||||
final int currentPage;
|
||||
final bool hasMore;
|
||||
final bool isLoadingMore;
|
||||
|
||||
NotificationsFeedState copyWith({
|
||||
List<AlertEntity>? notifications,
|
||||
int? currentPage,
|
||||
bool? hasMore,
|
||||
bool? isLoadingMore,
|
||||
}) {
|
||||
return NotificationsFeedState(
|
||||
notifications: notifications ?? this.notifications,
|
||||
currentPage: currentPage ?? this.currentPage,
|
||||
hasMore: hasMore ?? this.hasMore,
|
||||
isLoadingMore: isLoadingMore ?? this.isLoadingMore,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
class NotificationsFeed extends _$NotificationsFeed {
|
||||
StreamSubscription<WebSocketEvent>? _subscription;
|
||||
|
||||
@override
|
||||
Future<NotificationsFeedState> build() async {
|
||||
final filter = ref.watch(notificationsFilterProvider);
|
||||
|
||||
_subscription?.cancel();
|
||||
_subscription =
|
||||
ref.read(webSocketServiceProvider).events.listen(_onEvent);
|
||||
ref.onDispose(() => _subscription?.cancel());
|
||||
|
||||
final (notifications, totalPages) = await ref
|
||||
.read(alertsRepositoryProvider)
|
||||
.getAlerts(page: 1, pageSize: _pageSize, type: filter);
|
||||
|
||||
return NotificationsFeedState(
|
||||
notifications: notifications,
|
||||
currentPage: 1,
|
||||
hasMore: totalPages > 1,
|
||||
);
|
||||
}
|
||||
|
||||
void _onEvent(WebSocketEvent event) {
|
||||
if (event is! AlertEvent) return;
|
||||
|
||||
final current = state.value;
|
||||
if (current == null) return;
|
||||
|
||||
final filter = ref.read(notificationsFilterProvider);
|
||||
if (filter != null && filter != event.type) return;
|
||||
|
||||
final newNotification = AlertEntity(
|
||||
id: '',
|
||||
deviceIdentificator: event.deviceIdentificator,
|
||||
deviceName: '',
|
||||
type: event.type,
|
||||
createdAt: DateTime.now().millisecondsSinceEpoch,
|
||||
);
|
||||
|
||||
state = AsyncData(
|
||||
current.copyWith(
|
||||
notifications: [newNotification, ...current.notifications],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> loadMore() async {
|
||||
final current = state.value;
|
||||
if (current == null) return;
|
||||
if (current.isLoadingMore || !current.hasMore) return;
|
||||
|
||||
state = AsyncData(current.copyWith(isLoadingMore: true));
|
||||
|
||||
try {
|
||||
final filter = ref.read(notificationsFilterProvider);
|
||||
final nextPage = current.currentPage + 1;
|
||||
final (notifications, totalPages) = await ref
|
||||
.read(alertsRepositoryProvider)
|
||||
.getAlerts(page: nextPage, pageSize: _pageSize, type: filter);
|
||||
|
||||
state = AsyncData(
|
||||
current.copyWith(
|
||||
notifications: [...current.notifications, ...notifications],
|
||||
currentPage: nextPage,
|
||||
hasMore: nextPage < totalPages,
|
||||
isLoadingMore: false,
|
||||
),
|
||||
);
|
||||
} catch (_) {
|
||||
state = AsyncData(current.copyWith(isLoadingMore: false));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'notifications_feed_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(NotificationsFeed)
|
||||
const notificationsFeedProvider = NotificationsFeedProvider._();
|
||||
|
||||
final class NotificationsFeedProvider
|
||||
extends $AsyncNotifierProvider<NotificationsFeed, NotificationsFeedState> {
|
||||
const NotificationsFeedProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'notificationsFeedProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$notificationsFeedHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
NotificationsFeed create() => NotificationsFeed();
|
||||
}
|
||||
|
||||
String _$notificationsFeedHash() => r'bb7878198548338c17db4b9ac51e3f88a24115d6';
|
||||
|
||||
abstract class _$NotificationsFeed
|
||||
extends $AsyncNotifier<NotificationsFeedState> {
|
||||
FutureOr<NotificationsFeedState> build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref =
|
||||
this.ref
|
||||
as $Ref<AsyncValue<NotificationsFeedState>, NotificationsFeedState>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<
|
||||
AsyncValue<NotificationsFeedState>,
|
||||
NotificationsFeedState
|
||||
>,
|
||||
AsyncValue<NotificationsFeedState>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'notifications_filter_provider.g.dart';
|
||||
|
||||
@riverpod
|
||||
class NotificationsFilter extends _$NotificationsFilter {
|
||||
@override
|
||||
String? build() => null;
|
||||
|
||||
void select(String? type) {
|
||||
if (type == state) return;
|
||||
state = type;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'notifications_filter_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(NotificationsFilter)
|
||||
const notificationsFilterProvider = NotificationsFilterProvider._();
|
||||
|
||||
final class NotificationsFilterProvider
|
||||
extends $NotifierProvider<NotificationsFilter, String?> {
|
||||
const NotificationsFilterProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'notificationsFilterProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$notificationsFilterHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
NotificationsFilter create() => NotificationsFilter();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(String? value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<String?>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$notificationsFilterHash() =>
|
||||
r'3da0e65affd7f2c5121e19ab1625b5cfc14d3b3a';
|
||||
|
||||
abstract class _$NotificationsFilter extends $Notifier<String?> {
|
||||
String? build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref = this.ref as $Ref<String?, String?>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<String?, String?>,
|
||||
String?,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,10 @@ import 'package:utils/utils.dart';
|
||||
|
||||
import '../../../../core/domain/entities/alert_entity.dart';
|
||||
|
||||
class AlertCard extends ConsumerWidget {
|
||||
final AlertEntity alert;
|
||||
class NotificationCard extends ConsumerWidget {
|
||||
final AlertEntity notification;
|
||||
|
||||
const AlertCard({super.key, required this.alert});
|
||||
const NotificationCard({super.key, required this.notification});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@@ -36,12 +36,13 @@ class AlertCard extends ConsumerWidget {
|
||||
width: SizeUtils.getByScreen(small: 40, big: 36),
|
||||
height: SizeUtils.getByScreen(small: 40, big: 36),
|
||||
decoration: BoxDecoration(
|
||||
color: _alertColor(context, alert.type).withValues(alpha: 0.15),
|
||||
color: _notificationColor(context, notification.type)
|
||||
.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
_alertIcon(alert.type),
|
||||
color: _alertColor(context, alert.type),
|
||||
_notificationIcon(notification.type),
|
||||
color: _notificationColor(context, notification.type),
|
||||
size: SizeUtils.getByScreen(small: 22, big: 20),
|
||||
),
|
||||
),
|
||||
@@ -51,7 +52,7 @@ class AlertCard extends ConsumerWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_alertTypeLabel(context, alert.type),
|
||||
_notificationTypeLabel(context, notification.type),
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 14, big: 13),
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -60,19 +61,19 @@ class AlertCard extends ConsumerWidget {
|
||||
),
|
||||
SizedBox(height: 2),
|
||||
Text(
|
||||
alert.deviceName.isNotEmpty
|
||||
? alert.deviceName
|
||||
: alert.deviceIdentificator,
|
||||
notification.deviceName.isNotEmpty
|
||||
? notification.deviceName
|
||||
: notification.deviceIdentificator,
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 12, big: 11),
|
||||
color: Theme.of(context).colorScheme.onSurface
|
||||
.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
if (alert.geofenceAlert != null) ...[
|
||||
if (notification.geofenceAlert != null) ...[
|
||||
SizedBox(height: 2),
|
||||
Text(
|
||||
'${context.translate(I18n.alertGeofenceDetail)}: ${alert.geofenceAlert!.name}',
|
||||
'${context.translate(I18n.alertGeofenceDetail)}: ${notification.geofenceAlert!.name}',
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 11, big: 10),
|
||||
color: primaryColor,
|
||||
@@ -83,7 +84,7 @@ class AlertCard extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_timeAgo(context, alert.createdAt),
|
||||
_timeAgo(context, notification.createdAt),
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 11, big: 10),
|
||||
color: Theme.of(context).colorScheme.onSurface
|
||||
@@ -96,7 +97,7 @@ class AlertCard extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
IconData _alertIcon(String type) {
|
||||
IconData _notificationIcon(String type) {
|
||||
return switch (type) {
|
||||
'sos' => Icons.warning_amber_rounded,
|
||||
'falldown' => Icons.person_off,
|
||||
@@ -113,7 +114,7 @@ class AlertCard extends ConsumerWidget {
|
||||
};
|
||||
}
|
||||
|
||||
Color _alertColor(BuildContext context, String type) {
|
||||
Color _notificationColor(BuildContext context, String type) {
|
||||
return switch (type) {
|
||||
'sos' => Theme.of(context).colorScheme.error,
|
||||
'falldown' => Theme.of(context).colorScheme.error,
|
||||
@@ -130,7 +131,7 @@ class AlertCard extends ConsumerWidget {
|
||||
};
|
||||
}
|
||||
|
||||
String _alertTypeLabel(BuildContext context, String type) {
|
||||
String _notificationTypeLabel(BuildContext context, String type) {
|
||||
return switch (type) {
|
||||
'sos' => context.translate(I18n.alertTypeSos),
|
||||
'falldown' => context.translate(I18n.alertTypeFalldown),
|
||||
@@ -61,6 +61,7 @@ dependencies:
|
||||
get_it: ^9.0.5
|
||||
go_router: ^17.0.0
|
||||
flutter_riverpod: ^3.0.3
|
||||
riverpod_annotation: ^3.0.3
|
||||
freezed_annotation: ^3.1.0
|
||||
freezed: ^3.2.3
|
||||
dio: ^5.9.2
|
||||
@@ -80,6 +81,7 @@ dev_dependencies:
|
||||
riverpod_generator: ^3.0.3
|
||||
build_runner: ^2.7.1
|
||||
riverpod_lint: ^3.0.3
|
||||
mocktail: ^1.0.4
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:control_panel/src/core/domain/entities/alert_entity.dart';
|
||||
import 'package:control_panel/src/core/domain/repositories/alerts_repository.dart';
|
||||
import 'package:control_panel/src/core/providers/alerts_repository_provider.dart';
|
||||
import 'package:control_panel/src/features/notifications/presentation/providers/notifications_feed_provider.dart';
|
||||
import 'package:control_panel/src/features/notifications/presentation/providers/notifications_filter_provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:legacy_device_state/legacy_device_state.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:sf_infrastructure/sf_infrastructure.dart';
|
||||
import 'package:sf_shared/testing.dart';
|
||||
|
||||
class MockAlertsRepository extends Mock implements AlertsRepository {}
|
||||
|
||||
class FakeWebSocketService implements WebSocketService {
|
||||
final _controller = StreamController<WebSocketEvent>.broadcast();
|
||||
@override
|
||||
Stream<WebSocketEvent> get events => _controller.stream;
|
||||
|
||||
void emit(WebSocketEvent event) => _controller.add(event);
|
||||
void close() => _controller.close();
|
||||
|
||||
@override
|
||||
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||
}
|
||||
|
||||
final _alert1 = AlertEntity(
|
||||
id: '1',
|
||||
deviceIdentificator: 'd1',
|
||||
deviceName: 'Watch',
|
||||
type: 'sos',
|
||||
createdAt: 1,
|
||||
);
|
||||
final _alert2 = AlertEntity(
|
||||
id: '2',
|
||||
deviceIdentificator: 'd1',
|
||||
deviceName: 'Watch',
|
||||
type: 'falldown',
|
||||
createdAt: 2,
|
||||
);
|
||||
|
||||
void main() {
|
||||
ProviderContainer buildContainer({
|
||||
required AlertsRepository repo,
|
||||
required FakeWebSocketService ws,
|
||||
}) {
|
||||
return makeContainer(
|
||||
overrides: [
|
||||
alertsRepositoryProvider.overrideWithValue(repo),
|
||||
webSocketServiceProvider.overrideWithValue(ws),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
group('NotificationsFeed.build', () {
|
||||
test('loads first page with no filter', () async {
|
||||
final repo = MockAlertsRepository();
|
||||
final ws = FakeWebSocketService();
|
||||
when(
|
||||
() => repo.getAlerts(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
type: any(named: 'type'),
|
||||
),
|
||||
).thenAnswer((_) async => ([_alert1, _alert2], 2));
|
||||
|
||||
final container = buildContainer(repo: repo, ws: ws);
|
||||
addTearDown(container.dispose);
|
||||
addTearDown(ws.close);
|
||||
|
||||
final state =
|
||||
await container.read(notificationsFeedProvider.future);
|
||||
|
||||
expect(state.notifications, [_alert1, _alert2]);
|
||||
expect(state.currentPage, 1);
|
||||
expect(state.hasMore, isTrue);
|
||||
verify(() => repo.getAlerts(page: 1, pageSize: 20, type: null))
|
||||
.called(1);
|
||||
});
|
||||
|
||||
test('refetches when filter changes', () async {
|
||||
final repo = MockAlertsRepository();
|
||||
final ws = FakeWebSocketService();
|
||||
when(
|
||||
() => repo.getAlerts(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
type: any(named: 'type'),
|
||||
),
|
||||
).thenAnswer((_) async => ([_alert1], 1));
|
||||
|
||||
final container = buildContainer(repo: repo, ws: ws);
|
||||
addTearDown(container.dispose);
|
||||
addTearDown(ws.close);
|
||||
|
||||
await container.read(notificationsFeedProvider.future);
|
||||
|
||||
container.read(notificationsFilterProvider.notifier).select('sos');
|
||||
await container.read(notificationsFeedProvider.future);
|
||||
|
||||
verify(() => repo.getAlerts(page: 1, pageSize: 20, type: 'sos'))
|
||||
.called(1);
|
||||
});
|
||||
});
|
||||
|
||||
group('NotificationsFeed.loadMore', () {
|
||||
test('appends next page', () async {
|
||||
final repo = MockAlertsRepository();
|
||||
final ws = FakeWebSocketService();
|
||||
when(
|
||||
() => repo.getAlerts(
|
||||
page: 1,
|
||||
pageSize: any(named: 'pageSize'),
|
||||
type: any(named: 'type'),
|
||||
),
|
||||
).thenAnswer((_) async => ([_alert1], 2));
|
||||
when(
|
||||
() => repo.getAlerts(
|
||||
page: 2,
|
||||
pageSize: any(named: 'pageSize'),
|
||||
type: any(named: 'type'),
|
||||
),
|
||||
).thenAnswer((_) async => ([_alert2], 2));
|
||||
|
||||
final container = buildContainer(repo: repo, ws: ws);
|
||||
addTearDown(container.dispose);
|
||||
addTearDown(ws.close);
|
||||
|
||||
await container.read(notificationsFeedProvider.future);
|
||||
await container.read(notificationsFeedProvider.notifier).loadMore();
|
||||
|
||||
final state = container.read(notificationsFeedProvider).value!;
|
||||
expect(state.notifications, [_alert1, _alert2]);
|
||||
expect(state.currentPage, 2);
|
||||
expect(state.hasMore, isFalse);
|
||||
});
|
||||
|
||||
test('no-op when hasMore is false', () async {
|
||||
final repo = MockAlertsRepository();
|
||||
final ws = FakeWebSocketService();
|
||||
when(
|
||||
() => repo.getAlerts(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
type: any(named: 'type'),
|
||||
),
|
||||
).thenAnswer((_) async => ([_alert1], 1));
|
||||
|
||||
final container = buildContainer(repo: repo, ws: ws);
|
||||
addTearDown(container.dispose);
|
||||
addTearDown(ws.close);
|
||||
|
||||
await container.read(notificationsFeedProvider.future);
|
||||
await container.read(notificationsFeedProvider.notifier).loadMore();
|
||||
|
||||
verify(() => repo.getAlerts(page: 1, pageSize: 20, type: null))
|
||||
.called(1);
|
||||
verifyNever(() => repo.getAlerts(page: 2, pageSize: 20, type: null));
|
||||
});
|
||||
});
|
||||
|
||||
group('NotificationsFeed WebSocket integration', () {
|
||||
test('prepends new alert from WS when matching filter', () async {
|
||||
final repo = MockAlertsRepository();
|
||||
final ws = FakeWebSocketService();
|
||||
when(
|
||||
() => repo.getAlerts(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
type: any(named: 'type'),
|
||||
),
|
||||
).thenAnswer((_) async => ([_alert1], 1));
|
||||
|
||||
final container = buildContainer(repo: repo, ws: ws);
|
||||
addTearDown(container.dispose);
|
||||
addTearDown(ws.close);
|
||||
container.listen(notificationsFeedProvider, (_, __) {});
|
||||
|
||||
await container.read(notificationsFeedProvider.future);
|
||||
ws.emit(AlertEvent(
|
||||
type: 'sos',
|
||||
deviceIdentificator: 'd1',
|
||||
timestamp: '2026-04-22T00:00:00Z',
|
||||
));
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
final state = container.read(notificationsFeedProvider).value!;
|
||||
expect(state.notifications.length, 2);
|
||||
expect(state.notifications.first.type, 'sos');
|
||||
});
|
||||
|
||||
test('drops new alert when filter excludes it', () async {
|
||||
final repo = MockAlertsRepository();
|
||||
final ws = FakeWebSocketService();
|
||||
when(
|
||||
() => repo.getAlerts(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
type: any(named: 'type'),
|
||||
),
|
||||
).thenAnswer((_) async => ([_alert1], 1));
|
||||
|
||||
final container = buildContainer(repo: repo, ws: ws);
|
||||
addTearDown(container.dispose);
|
||||
addTearDown(ws.close);
|
||||
container.listen(notificationsFeedProvider, (_, __) {});
|
||||
|
||||
container.read(notificationsFilterProvider.notifier).select('sos');
|
||||
await container.read(notificationsFeedProvider.future);
|
||||
|
||||
ws.emit(AlertEvent(
|
||||
type: 'falldown',
|
||||
deviceIdentificator: 'd1',
|
||||
timestamp: '2026-04-22T00:00:00Z',
|
||||
));
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
final state = container.read(notificationsFeedProvider).value!;
|
||||
expect(state.notifications.length, 1);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -6,7 +6,6 @@ export 'src/features/battery/battery_builder.dart';
|
||||
export 'src/features/block_phone/block_phone_builder.dart';
|
||||
export 'src/features/disable_functions/disable_functions_builder.dart';
|
||||
export 'src/features/language/language_builder.dart';
|
||||
export 'src/features/legacy_notifications/legacy_notifications_builder.dart';
|
||||
export 'src/features/remote_management/remote_management_builder.dart';
|
||||
export 'src/features/remote_on_off/remote_on_off_builder.dart';
|
||||
export 'src/features/alerts/alerts_builder.dart';
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'presentation/legacy_notifications_screen.dart';
|
||||
|
||||
class LegacyNotificationsBuilder {
|
||||
const LegacyNotificationsBuilder();
|
||||
|
||||
Page<void> buildPage(BuildContext context, GoRouterState state) {
|
||||
return MaterialPage<void>(
|
||||
key: state.pageKey,
|
||||
child: const LegacyNotificationsScreen(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:legacy_ui/legacy_ui.dart';
|
||||
import 'package:sf_localizations/sf_localizations.dart';
|
||||
|
||||
class LegacyNotificationsScreen extends ConsumerWidget {
|
||||
const LegacyNotificationsScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
|
||||
return LegacyPageLayout(
|
||||
title: context.translate(I18n.notificationsLegacyTitle),
|
||||
body: const Center(child: Text('Coming soon')),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -93,8 +93,8 @@ class SettingsScreen extends ConsumerWidget {
|
||||
_item(
|
||||
context,
|
||||
onPressed: () =>
|
||||
navigationContract.pushTo(AppRoutes.legacyNotifications),
|
||||
icon: Icons.message_outlined,
|
||||
navigationContract.pushTo(AppRoutes.deviceNotifications),
|
||||
icon: Icons.notifications_outlined,
|
||||
text: I18n.notificationsLegacyTitle,
|
||||
color: color,
|
||||
),
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'dart:async';
|
||||
import 'package:legacy_device_state/legacy_device_state.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:settings/src/features/wifi_settings/domain/entities/wifi_network_entity.dart';
|
||||
import 'package:settings/src/features/wifi_settings/presentation/providers/web_socket_service_provider.dart';
|
||||
import 'package:sf_infrastructure/sf_infrastructure.dart';
|
||||
|
||||
part 'wifi_current_network_provider.g.dart';
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'dart:async';
|
||||
import 'package:legacy_device_state/legacy_device_state.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:settings/src/features/wifi_settings/domain/entities/scanned_wifi_network.dart';
|
||||
import 'package:settings/src/features/wifi_settings/presentation/providers/web_socket_service_provider.dart';
|
||||
import 'package:sf_infrastructure/sf_infrastructure.dart';
|
||||
|
||||
part 'wifi_scan_provider.g.dart';
|
||||
|
||||
@@ -7,7 +7,6 @@ import 'package:mocktail/mocktail.dart';
|
||||
import 'package:settings/src/core/domain/repositories/wifi_repository.dart';
|
||||
import 'package:settings/src/core/providers/wifi_repository_provider.dart';
|
||||
import 'package:settings/src/features/wifi_settings/domain/entities/wifi_network_entity.dart';
|
||||
import 'package:settings/src/features/wifi_settings/presentation/providers/web_socket_service_provider.dart';
|
||||
import 'package:settings/src/features/wifi_settings/presentation/providers/wifi_controller.dart';
|
||||
import 'package:sf_infrastructure/sf_infrastructure.dart';
|
||||
import 'package:sf_shared/testing.dart';
|
||||
|
||||
@@ -15,6 +15,7 @@ export 'src/data/repositories/commands_repository_impl.dart';
|
||||
export 'src/domain/repositories/command_repository.dart';
|
||||
export 'src/providers/commands_remote_datasource_provider.dart';
|
||||
export 'src/providers/commands_repository_provider.dart';
|
||||
export 'src/providers/web_socket_service_provider.dart';
|
||||
|
||||
// Device settings update
|
||||
export 'src/data/datasources/device_settings_update_datasource.dart';
|
||||
|
||||
@@ -26,6 +26,7 @@ dependencies:
|
||||
utils:
|
||||
path: ../../../../packages/utils
|
||||
flutter_riverpod: ^3.0.3
|
||||
riverpod_annotation: ^3.0.3
|
||||
freezed_annotation: ^3.1.0
|
||||
json_annotation: ^4.9.0
|
||||
dio: ^5.9.2
|
||||
@@ -38,6 +39,7 @@ dev_dependencies:
|
||||
build_runner: ^2.7.1
|
||||
freezed: ^3.2.3
|
||||
json_serializable: ^6.11.2
|
||||
riverpod_generator: ^3.0.3
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
|
||||
@@ -54,7 +54,7 @@ class AppRoutes {
|
||||
|
||||
static const controlPanel = '$legacyDashboard/control_panel';
|
||||
static const customerService = '$controlPanel/customer_service';
|
||||
static const deviceAlertsNotifications = '$controlPanel/device_alerts_notifications';
|
||||
static const deviceNotifications = '$controlPanel/notifications';
|
||||
|
||||
static const deviceManagement = '$legacyDashboard/device_management';
|
||||
static const legacyLocation = '$legacyDashboard/location';
|
||||
@@ -98,7 +98,6 @@ class AppRoutes {
|
||||
static const blockPhone = '$settings/block_phone';
|
||||
static const disableFunctions = '$settings/disable_functions';
|
||||
static const language = '$settings/language';
|
||||
static const legacyNotifications = '$settings/legacy_notifications';
|
||||
static const remoteManagement = '$settings/remote_management';
|
||||
static const remoteOnOff = '$settings/remote_on_off';
|
||||
static const alerts = '$settings/alerts';
|
||||
|
||||
Reference in New Issue
Block a user