diff --git a/lib/core/di/bloc_module.dart b/lib/core/di/bloc_module.dart index f05f7ab5..3276585c 100644 --- a/lib/core/di/bloc_module.dart +++ b/lib/core/di/bloc_module.dart @@ -147,4 +147,12 @@ void _registerBlocsModule() { _getIt(), _getIt(), )); -} \ No newline at end of file + + _registerFactoryWithParams( + (dao, _) => ProposalsHistoryBloc( + _getIt(), + _getIt(), + dao + ), + ); +} diff --git a/lib/core/di/di_setup.dart b/lib/core/di/di_setup.dart index f10641d8..b4dbca63 100644 --- a/lib/core/di/di_setup.dart +++ b/lib/core/di/di_setup.dart @@ -30,6 +30,7 @@ import 'package:hypha_wallet/core/network/api/services/token_service.dart'; import 'package:hypha_wallet/core/network/api/services/transaction_history_service.dart'; import 'package:hypha_wallet/core/network/api/services/user_account_service.dart'; import 'package:hypha_wallet/core/network/ipfs/ipfs_manager.dart'; +import 'package:hypha_wallet/core/network/models/dao_data_model.dart'; import 'package:hypha_wallet/core/network/models/network.dart'; import 'package:hypha_wallet/core/network/models/user_profile_data.dart'; import 'package:hypha_wallet/core/network/networking_manager.dart'; @@ -76,6 +77,7 @@ import 'package:hypha_wallet/ui/proposals/details/usecases/get_proposal_details_ import 'package:hypha_wallet/ui/proposals/filter/interactor/filter_proposals_bloc.dart'; import 'package:hypha_wallet/ui/proposals/filter/usecases/aggregate_dao_proposal_counts_use_case.dart'; import 'package:hypha_wallet/ui/proposals/filter/usecases/get_daos_from_proposal_counts_use_case.dart'; +import 'package:hypha_wallet/ui/proposals/history/interactor/proposals_history_bloc.dart'; import 'package:hypha_wallet/ui/proposals/list/interactor/proposals_bloc.dart'; import 'package:hypha_wallet/ui/proposals/list/usecases/get_proposals_use_case.dart'; import 'package:hypha_wallet/ui/search_user/interactor/search_user_bloc.dart'; diff --git a/lib/ui/proposals/components/proposals_list.dart b/lib/ui/proposals/components/proposals_list.dart new file mode 100644 index 00000000..d85ed627 --- /dev/null +++ b/lib/ui/proposals/components/proposals_list.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:hypha_wallet/core/network/models/proposal_model.dart'; +import 'package:hypha_wallet/ui/proposals/list/components/hypha_proposals_action_card.dart'; + +class ProposalsList extends StatelessWidget { + final List proposals; + + const ProposalsList(this.proposals, {super.key}); + + @override + Widget build(BuildContext context) { + return ListView.separated( + padding: const EdgeInsets.only(bottom: 22), + itemBuilder: (BuildContext context, int index) => HyphaProposalsActionCard(proposals[index]), + separatorBuilder: (BuildContext context, int index) { + return const SizedBox(height: 16); + }, + itemCount: proposals.length); + } +} diff --git a/lib/ui/proposals/history/components/proposals_history_view.dart b/lib/ui/proposals/history/components/proposals_history_view.dart new file mode 100644 index 00000000..c58a4038 --- /dev/null +++ b/lib/ui/proposals/history/components/proposals_history_view.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:get/get.dart'; +import 'package:hypha_wallet/design/hypha_colors.dart'; +import 'package:hypha_wallet/ui/proposals/components/proposals_list.dart'; +import 'package:hypha_wallet/ui/proposals/history/interactor/proposals_history_bloc.dart'; +import 'package:hypha_wallet/ui/shared/hypha_body_widget.dart'; + +class ProposalsHistoryView extends StatelessWidget { + const ProposalsHistoryView({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: context.isDarkMode ? HyphaColors.darkBlack : HyphaColors.offWhite, + appBar: AppBar( + title: const Text('Proposals History'), + ), + body: RefreshIndicator( + onRefresh: () async { + context.read().add(const ProposalsHistoryEvent.initial(refresh: true)); + }, + child: BlocBuilder( + builder: (context, state) { + return HyphaBodyWidget(pageState: state.pageState, success: (context) { + return Container( + padding: const EdgeInsets.all(20), + child: ProposalsList(state.proposals), + ); + }); + }), + ) + ); + } +} diff --git a/lib/ui/proposals/history/interactor/proposals_history_bloc.dart b/lib/ui/proposals/history/interactor/proposals_history_bloc.dart new file mode 100644 index 00000000..b0c85339 --- /dev/null +++ b/lib/ui/proposals/history/interactor/proposals_history_bloc.dart @@ -0,0 +1,39 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hypha_wallet/core/error_handler/error_handler_manager.dart'; +import 'package:hypha_wallet/core/error_handler/model/hypha_error.dart'; +import 'package:hypha_wallet/core/network/models/dao_data_model.dart'; +import 'package:hypha_wallet/core/network/models/proposal_model.dart'; +import 'package:hypha_wallet/ui/architecture/interactor/page_states.dart'; +import 'package:hypha_wallet/ui/architecture/result/result.dart'; +import 'package:hypha_wallet/ui/proposals/filter/interactor/filter_status.dart'; +import 'package:hypha_wallet/ui/proposals/list/usecases/get_proposals_use_case.dart'; + +part 'proposals_history_bloc.freezed.dart'; +part 'proposals_history_event.dart'; +part 'proposals_history_state.dart'; + +class ProposalsHistoryBloc extends Bloc { + final GetProposalsUseCase _getProposalsUseCase; + final ErrorHandlerManager _errorHandlerManager; + final DaoData _dao; + + ProposalsHistoryBloc(this._getProposalsUseCase, this._errorHandlerManager, this._dao) : super(const ProposalsHistoryState()) { + on<_Initial>(_initial); + } + + Future _initial(_Initial event, Emitter emit) async { + if (!event.refresh) { + emit(state.copyWith(pageState: PageState.loading)); + } + + final Result, HyphaError> proposalsResult = await _getProposalsUseCase.run([_dao], FilterStatus.past); + + if (proposalsResult.isValue) { + emit(state.copyWith(pageState: PageState.success, proposals: proposalsResult.asValue!.value)); + } else { + await _errorHandlerManager.handlerError(proposalsResult.asError!.error); + emit(state.copyWith(pageState: PageState.failure)); + } + } +} diff --git a/lib/ui/proposals/history/interactor/proposals_history_bloc.freezed.dart b/lib/ui/proposals/history/interactor/proposals_history_bloc.freezed.dart new file mode 100644 index 00000000..f7f69edc --- /dev/null +++ b/lib/ui/proposals/history/interactor/proposals_history_bloc.freezed.dart @@ -0,0 +1,396 @@ +// 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 'proposals_history_bloc.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 _$ProposalsHistoryEvent { + bool get refresh => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult when({ + required TResult Function(bool refresh) initial, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(bool refresh)? initial, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(bool refresh)? initial, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + + /// Create a copy of ProposalsHistoryEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ProposalsHistoryEventCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ProposalsHistoryEventCopyWith<$Res> { + factory $ProposalsHistoryEventCopyWith(ProposalsHistoryEvent value, + $Res Function(ProposalsHistoryEvent) then) = + _$ProposalsHistoryEventCopyWithImpl<$Res, ProposalsHistoryEvent>; + @useResult + $Res call({bool refresh}); +} + +/// @nodoc +class _$ProposalsHistoryEventCopyWithImpl<$Res, + $Val extends ProposalsHistoryEvent> + implements $ProposalsHistoryEventCopyWith<$Res> { + _$ProposalsHistoryEventCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ProposalsHistoryEvent + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? refresh = null, + }) { + return _then(_value.copyWith( + refresh: null == refresh + ? _value.refresh + : refresh // ignore: cast_nullable_to_non_nullable + as bool, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$InitialImplCopyWith<$Res> + implements $ProposalsHistoryEventCopyWith<$Res> { + factory _$$InitialImplCopyWith( + _$InitialImpl value, $Res Function(_$InitialImpl) then) = + __$$InitialImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({bool refresh}); +} + +/// @nodoc +class __$$InitialImplCopyWithImpl<$Res> + extends _$ProposalsHistoryEventCopyWithImpl<$Res, _$InitialImpl> + implements _$$InitialImplCopyWith<$Res> { + __$$InitialImplCopyWithImpl( + _$InitialImpl _value, $Res Function(_$InitialImpl) _then) + : super(_value, _then); + + /// Create a copy of ProposalsHistoryEvent + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? refresh = null, + }) { + return _then(_$InitialImpl( + refresh: null == refresh + ? _value.refresh + : refresh // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc + +class _$InitialImpl implements _Initial { + const _$InitialImpl({this.refresh = false}); + + @override + @JsonKey() + final bool refresh; + + @override + String toString() { + return 'ProposalsHistoryEvent.initial(refresh: $refresh)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$InitialImpl && + (identical(other.refresh, refresh) || other.refresh == refresh)); + } + + @override + int get hashCode => Object.hash(runtimeType, refresh); + + /// Create a copy of ProposalsHistoryEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$InitialImplCopyWith<_$InitialImpl> get copyWith => + __$$InitialImplCopyWithImpl<_$InitialImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(bool refresh) initial, + }) { + return initial(refresh); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(bool refresh)? initial, + }) { + return initial?.call(refresh); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(bool refresh)? initial, + required TResult orElse(), + }) { + if (initial != null) { + return initial(refresh); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + }) { + return initial(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + }) { + return initial?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + required TResult orElse(), + }) { + if (initial != null) { + return initial(this); + } + return orElse(); + } +} + +abstract class _Initial implements ProposalsHistoryEvent { + const factory _Initial({final bool refresh}) = _$InitialImpl; + + @override + bool get refresh; + + /// Create a copy of ProposalsHistoryEvent + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$InitialImplCopyWith<_$InitialImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$ProposalsHistoryState { + PageState get pageState => throw _privateConstructorUsedError; + List get proposals => throw _privateConstructorUsedError; + + /// Create a copy of ProposalsHistoryState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ProposalsHistoryStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ProposalsHistoryStateCopyWith<$Res> { + factory $ProposalsHistoryStateCopyWith(ProposalsHistoryState value, + $Res Function(ProposalsHistoryState) then) = + _$ProposalsHistoryStateCopyWithImpl<$Res, ProposalsHistoryState>; + @useResult + $Res call({PageState pageState, List proposals}); +} + +/// @nodoc +class _$ProposalsHistoryStateCopyWithImpl<$Res, + $Val extends ProposalsHistoryState> + implements $ProposalsHistoryStateCopyWith<$Res> { + _$ProposalsHistoryStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ProposalsHistoryState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? pageState = null, + Object? proposals = null, + }) { + return _then(_value.copyWith( + pageState: null == pageState + ? _value.pageState + : pageState // ignore: cast_nullable_to_non_nullable + as PageState, + proposals: null == proposals + ? _value.proposals + : proposals // ignore: cast_nullable_to_non_nullable + as List, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$ProposalsHistoryStateImplCopyWith<$Res> + implements $ProposalsHistoryStateCopyWith<$Res> { + factory _$$ProposalsHistoryStateImplCopyWith( + _$ProposalsHistoryStateImpl value, + $Res Function(_$ProposalsHistoryStateImpl) then) = + __$$ProposalsHistoryStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({PageState pageState, List proposals}); +} + +/// @nodoc +class __$$ProposalsHistoryStateImplCopyWithImpl<$Res> + extends _$ProposalsHistoryStateCopyWithImpl<$Res, + _$ProposalsHistoryStateImpl> + implements _$$ProposalsHistoryStateImplCopyWith<$Res> { + __$$ProposalsHistoryStateImplCopyWithImpl(_$ProposalsHistoryStateImpl _value, + $Res Function(_$ProposalsHistoryStateImpl) _then) + : super(_value, _then); + + /// Create a copy of ProposalsHistoryState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? pageState = null, + Object? proposals = null, + }) { + return _then(_$ProposalsHistoryStateImpl( + pageState: null == pageState + ? _value.pageState + : pageState // ignore: cast_nullable_to_non_nullable + as PageState, + proposals: null == proposals + ? _value._proposals + : proposals // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc + +class _$ProposalsHistoryStateImpl implements _ProposalsHistoryState { + const _$ProposalsHistoryStateImpl( + {this.pageState = PageState.initial, + final List proposals = const []}) + : _proposals = proposals; + + @override + @JsonKey() + final PageState pageState; + final List _proposals; + @override + @JsonKey() + List get proposals { + if (_proposals is EqualUnmodifiableListView) return _proposals; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_proposals); + } + + @override + String toString() { + return 'ProposalsHistoryState(pageState: $pageState, proposals: $proposals)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ProposalsHistoryStateImpl && + (identical(other.pageState, pageState) || + other.pageState == pageState) && + const DeepCollectionEquality() + .equals(other._proposals, _proposals)); + } + + @override + int get hashCode => Object.hash( + runtimeType, pageState, const DeepCollectionEquality().hash(_proposals)); + + /// Create a copy of ProposalsHistoryState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ProposalsHistoryStateImplCopyWith<_$ProposalsHistoryStateImpl> + get copyWith => __$$ProposalsHistoryStateImplCopyWithImpl< + _$ProposalsHistoryStateImpl>(this, _$identity); +} + +abstract class _ProposalsHistoryState implements ProposalsHistoryState { + const factory _ProposalsHistoryState( + {final PageState pageState, + final List proposals}) = _$ProposalsHistoryStateImpl; + + @override + PageState get pageState; + @override + List get proposals; + + /// Create a copy of ProposalsHistoryState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ProposalsHistoryStateImplCopyWith<_$ProposalsHistoryStateImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/ui/proposals/history/interactor/proposals_history_event.dart b/lib/ui/proposals/history/interactor/proposals_history_event.dart new file mode 100644 index 00000000..bd9ea4a1 --- /dev/null +++ b/lib/ui/proposals/history/interactor/proposals_history_event.dart @@ -0,0 +1,6 @@ +part of 'proposals_history_bloc.dart'; + +@freezed +class ProposalsHistoryEvent with _$ProposalsHistoryEvent { + const factory ProposalsHistoryEvent.initial({@Default(false) bool refresh}) = _Initial; +} diff --git a/lib/ui/proposals/history/interactor/proposals_history_state.dart b/lib/ui/proposals/history/interactor/proposals_history_state.dart new file mode 100644 index 00000000..71180a10 --- /dev/null +++ b/lib/ui/proposals/history/interactor/proposals_history_state.dart @@ -0,0 +1,9 @@ +part of 'proposals_history_bloc.dart'; + +@freezed +class ProposalsHistoryState with _$ProposalsHistoryState { + const factory ProposalsHistoryState({ + @Default(PageState.initial) PageState pageState, + @Default([]) List proposals, + }) = _ProposalsHistoryState; +} diff --git a/lib/ui/proposals/history/proposals_history_page.dart b/lib/ui/proposals/history/proposals_history_page.dart new file mode 100644 index 00000000..b0cb4a2f --- /dev/null +++ b/lib/ui/proposals/history/proposals_history_page.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:get_it/get_it.dart'; +import 'package:hypha_wallet/core/network/models/dao_data_model.dart'; +import 'package:hypha_wallet/ui/proposals/history/components/proposals_history_view.dart'; +import 'package:hypha_wallet/ui/proposals/history/interactor/proposals_history_bloc.dart'; + +class ProposalsHistoryPage extends StatelessWidget { + final DaoData _dao; + const ProposalsHistoryPage(this._dao, {super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => GetIt.I.get(param1: _dao)..add(const ProposalsHistoryEvent.initial()), + child: const ProposalsHistoryView(), + ); + } +} diff --git a/lib/ui/proposals/list/components/proposals_view.dart b/lib/ui/proposals/list/components/proposals_view.dart index 215fbc2b..3e186cc3 100644 --- a/lib/ui/proposals/list/components/proposals_view.dart +++ b/lib/ui/proposals/list/components/proposals_view.dart @@ -7,10 +7,9 @@ import 'package:hypha_wallet/design/hypha_colors.dart'; import 'package:hypha_wallet/design/themes/extensions/theme_extension_provider.dart'; import 'package:hypha_wallet/ui/blocs/authentication/authentication_bloc.dart'; import 'package:hypha_wallet/ui/profile/profile_page.dart'; +import 'package:hypha_wallet/ui/proposals/components/proposals_list.dart'; import 'package:hypha_wallet/ui/proposals/filter/filter_proposals_page.dart'; -import 'package:hypha_wallet/ui/proposals/filter/interactor/filter_proposals_bloc.dart'; import 'package:hypha_wallet/ui/proposals/filter/interactor/filter_status.dart'; -import 'package:hypha_wallet/ui/proposals/list/components/hypha_proposals_action_card.dart'; import 'package:hypha_wallet/ui/proposals/list/interactor/proposals_bloc.dart'; import 'package:hypha_wallet/ui/shared/hypha_body_widget.dart'; @@ -100,17 +99,7 @@ class ProposalsView extends StatelessWidget { const SizedBox( height: 20, ), - Expanded( - child: ListView.separated( - padding: const EdgeInsets.only(bottom: 22), - itemBuilder: (BuildContext context, - int index) => - HyphaProposalsActionCard(state.proposals[index]), - separatorBuilder: - (BuildContext context, int index) { - return const SizedBox(height: 16); - }, - itemCount: state.proposals.length)), + Expanded(child: ProposalsList(state.proposals)), ], ), ), diff --git a/lib/ui/proposals/list/interactor/proposals_bloc.dart b/lib/ui/proposals/list/interactor/proposals_bloc.dart index ba9d7d76..7ef16974 100644 --- a/lib/ui/proposals/list/interactor/proposals_bloc.dart +++ b/lib/ui/proposals/list/interactor/proposals_bloc.dart @@ -39,6 +39,7 @@ class ProposalsBloc extends Bloc { final Result profileResult = await _fetchProfileUseCase.run(); + // TODO(Zied): DAOs may be empty list due to crash or user has no DAO, so refactor if (profileResult.isValue && profileResult.asValue!.value.daos.isNotEmpty) { await _fetchAndEmitProposals(emit, profileResult.asValue!.value.daos, filterStatus); } else {