diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index d0b37842..679176df 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -390,6 +390,7 @@ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = 9CX5MA4DG5; ENABLE_BITCODE = NO; + FLUTTER_BUILD_NAME = 1.3.2; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -527,6 +528,7 @@ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = 9CX5MA4DG5; ENABLE_BITCODE = NO; + FLUTTER_BUILD_NAME = 1.3.2; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -556,6 +558,7 @@ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = 9CX5MA4DG5; ENABLE_BITCODE = NO; + FLUTTER_BUILD_NAME = 1.3.2; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", diff --git a/lib/core/network/models/dao_proposals_model.dart b/lib/core/network/models/dao_proposals_model.dart new file mode 100644 index 00000000..0fbe3ef6 --- /dev/null +++ b/lib/core/network/models/dao_proposals_model.dart @@ -0,0 +1,8 @@ +import 'package:hypha_wallet/core/network/models/dao_data_model.dart'; +import 'package:hypha_wallet/core/network/models/proposal_model.dart'; + +class DaoProposalsModel { + final DaoData dao; + final List proposals; + DaoProposalsModel({required this.dao, required this.proposals}); +} diff --git a/lib/core/network/repository/proposal_repository.dart b/lib/core/network/repository/proposal_repository.dart index 7519c55a..8a62c983 100644 --- a/lib/core/network/repository/proposal_repository.dart +++ b/lib/core/network/repository/proposal_repository.dart @@ -4,6 +4,7 @@ import 'package:hypha_wallet/core/logging/log_helper.dart'; import 'package:hypha_wallet/core/network/api/services/dao_service.dart'; import 'package:hypha_wallet/core/network/api/services/proposal_service.dart'; import 'package:hypha_wallet/core/network/models/dao_data_model.dart'; +import 'package:hypha_wallet/core/network/models/dao_proposals_model.dart'; import 'package:hypha_wallet/core/network/models/network.dart'; import 'package:hypha_wallet/core/network/models/proposal_details_model.dart'; import 'package:hypha_wallet/core/network/models/proposal_model.dart'; @@ -25,12 +26,17 @@ class ProposalRepository { this._daoService, this._proposalService, this._profileService); - Future, HyphaError>> getProposals(UserProfileData user, GetProposalsUseCaseInput input) async { - final List, HyphaError>>> futures = input.daos.map((DaoData dao) { - return input.filterStatus == FilterStatus.active ? _proposalService.getActiveProposals(user, dao.docId) : _proposalService.getPastProposals(user, dao.docId); + Future, HyphaError>> getProposals( + UserProfileData user, GetProposalsUseCaseInput input) async { + final List, HyphaError>>> futures = + input.daos.map((DaoData dao) { + return input.filterStatus == FilterStatus.active + ? _proposalService.getActiveProposals(user, dao.docId) + : _proposalService.getPastProposals(user, dao.docId); }).toList(); - final List, HyphaError>> futureResults = await Future.wait(futures); + final List, HyphaError>> futureResults = + await Future.wait(futures); final List allProposals = []; @@ -46,11 +52,15 @@ class ProposalRepository { } try { - final List proposals = await _parseProposalsFromResponse(response, input.daos[i], input.filterStatus); + final List proposals = + await _parseProposalsFromResponse( + response, input.daos[i], input.filterStatus); allProposals.addAll(proposals); } catch (e, stackTrace) { - LogHelper.e('Error parsing data into proposal model', error: e, stacktrace: stackTrace); - return Result.error(HyphaError.generic('Error parsing data into proposal model')); + LogHelper.e('Error parsing data into proposal model', + error: e, stacktrace: stackTrace); + return Result.error( + HyphaError.generic('Error parsing data into proposal model')); } } else { LogHelper.e('GraphQL query failed', error: result.asError!.error); @@ -62,13 +72,69 @@ class ProposalRepository { return Result.value(allProposals); } - Future> _parseProposalsFromResponse(Map response, DaoData daoData, FilterStatus filterStatus) async { + Future, HyphaError>> getHistoryProposalsPerDao( + UserProfileData user, GetProposalsUseCaseInput input) async { + // Fetch past proposals for all DAOs + final List, HyphaError>>> futures = + input.daos.map((DaoData dao) { + return _proposalService.getPastProposals( + user, dao.docId); // Fetch proposals using integer docId + }).toList(); + + final List, HyphaError>> futureResults = + await Future.wait(futures); + + final List daoProposalsList = []; + + for (int i = 0; i < futureResults.length; i++) { + final Result, HyphaError> result = futureResults[i]; + + if (result.isValue) { + final Map response = result.asValue!.value; + if (response['errors'] != null) { + LogHelper.e('GraphQL query failed', error: response['errors']); + return Result.error(HyphaError.api('GraphQL query failed')); + } + + try { + // Parse proposals for the current DAO + final List proposals = + await _parseProposalsFromResponse( + response, input.daos[i], input.filterStatus); + + // Create a DaoProposalsModel instance with the current DAO and its proposals + daoProposalsList + .add(DaoProposalsModel(dao: input.daos[i], proposals: proposals)); + } catch (e, stackTrace) { + LogHelper.e('Error parsing data into proposal model', + error: e, stacktrace: stackTrace); + return Result.error( + HyphaError.generic('Error parsing data into proposal model')); + } + } else { + LogHelper.e('GraphQL query failed', error: result.asError!.error); + return Result.error(result.asError!.error); + } + } + + // Return the list of DaoProposalsModel + return Result.value(daoProposalsList); + } + + Future> _parseProposalsFromResponse( + Map response, + DaoData daoData, + FilterStatus filterStatus) async { final List proposalsData = response['data']['queryDao']; - final List> proposalFutures = proposalsData.expand((dao) { - final List proposals = dao[filterStatus == FilterStatus.active ? 'proposal' : 'votable'] as List; + final List> proposalFutures = + proposalsData.expand((dao) { + final List proposals = + dao[filterStatus == FilterStatus.active ? 'proposal' : 'votable'] + as List; return proposals.map((dynamic proposal) async { - final Result creator = await _profileService.getProfile(proposal['creator']); + final Result creator = + await _profileService.getProfile(proposal['creator']); proposal['creator'] = null; final ProposalModel proposalModel = ProposalModel.fromJson(proposal); @@ -86,7 +152,8 @@ class ProposalRepository { void sortProposals(List proposals,) { proposals.sort((a, b) { - final int daoNameComparison = (a.dao?.settingsDaoTitle ?? '').compareTo(b.dao?.settingsDaoTitle ?? ''); + final int daoNameComparison = (a.dao?.settingsDaoTitle ?? '') + .compareTo(b.dao?.settingsDaoTitle ?? ''); if (daoNameComparison != 0) { return daoNameComparison; } @@ -103,10 +170,11 @@ class ProposalRepository { return a.expiration?.compareTo(b.expiration ?? DateTime.now()) ?? 0; }); } + Future> getProposalDetails( String proposalId, UserProfileData user) async { final Result, HyphaError> result = - await _proposalService.getProposalDetails(proposalId, user); + await _proposalService.getProposalDetails(proposalId, user); if (result.isValue) { if (result.asValue!.value['errors'] != null) { LogHelper.e('GraphQL query failed', @@ -123,8 +191,8 @@ class ProposalRepository { if (proposalDetails.votes != null) { for (int i = 0; i < proposalDetails.votes!.length; i++) { final Result voterData = - await _profileService - .getProfile(proposalDetails.votes![i].voter); + await _profileService + .getProfile(proposalDetails.votes![i].voter); if (voterData.isValue) { proposalDetails.votes![i].voterImageUrl = voterData.asValue!.value.avatarUrl; @@ -145,4 +213,4 @@ class ProposalRepository { return Result.error(result.asError!.error); } } -} \ No newline at end of file +} diff --git a/lib/ui/proposals/history/proposals_history_page.dart b/lib/ui/proposals/history/proposals_history_page.dart index b0cb4a2f..ab80a5d2 100644 --- a/lib/ui/proposals/history/proposals_history_page.dart +++ b/lib/ui/proposals/history/proposals_history_page.dart @@ -7,12 +7,15 @@ import 'package:hypha_wallet/ui/proposals/history/interactor/proposals_history_b class ProposalsHistoryPage extends StatelessWidget { final DaoData _dao; + const ProposalsHistoryPage(this._dao, {super.key}); + // TODO(Zied): Refactor the logic to use the list of models already fetched from the proposal screen to avoid redundant fetching. @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => GetIt.I.get(param1: _dao)..add(const ProposalsHistoryEvent.initial()), + 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 12f79dcd..09c8fb03 100644 --- a/lib/ui/proposals/list/components/proposals_view.dart +++ b/lib/ui/proposals/list/components/proposals_view.dart @@ -3,7 +3,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:get/get.dart' as GetX; import 'package:get_it/get_it.dart'; import 'package:hypha_wallet/core/extension/proposals_filter_extension.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/design/avatar_image/hypha_avatar_image.dart'; import 'package:hypha_wallet/design/background/hypha_page_background.dart'; @@ -24,6 +23,8 @@ class ProposalsView extends StatelessWidget { @override Widget build(BuildContext context) { + final FilterStatus filterStatus = + context.watch().filterStatus; return BlocBuilder( builder: (context, state) { return HyphaPageBackground( @@ -104,7 +105,7 @@ class ProposalsView extends StatelessWidget { height: 22, ), Text( - '${proposals.length} ${context.read().filterStatus.string} Proposal${proposals.length == 1 ? '' : 's'}', + '${proposals.length} ${filterStatus.string} Proposal${proposals.length == 1 ? '' : 's'}', style: context.hyphaTextTheme.ralMediumBody .copyWith(color: HyphaColors.midGrey), ), @@ -121,41 +122,45 @@ class ProposalsView extends StatelessWidget { proposals, isScrollable: false, ), - const SizedBox( - height: 30, + SizedBox( + height: + filterStatus == FilterStatus.active + ? 30 + : 90, ), - Text( - 'See Proposals History', - style: context - .hyphaTextTheme.ralMediumBody - .copyWith(color: HyphaColors.midGrey), - ), - ...List.generate( - 2, - (index) { - return Container( - margin: const EdgeInsets.symmetric( - vertical: 10), - child: - const HyphaProposalHistoryCard( - dao: DaoData( - docId: 67176, - detailsDaoName: - 't4rcvben2fe4', - settingsDaoTitle: - 'Think-it Collective', - logoIPFSHash: - 'QmVB8q28U1bjfi51reQMaU7XwP4FThWj39DrU5G8MriMS9', - logoType: 'png', - settingsDaoUrl: - 'think-it-collective'), - subTitle: '1,234 Past Proposals', - )); - }, - ), - const SizedBox( - height: 100, - ) + if (filterStatus == FilterStatus.active) + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + 'See Proposals History', + style: context + .hyphaTextTheme.ralMediumBody + .copyWith( + color: HyphaColors.midGrey), + ), + ...List.generate( + state.historyProposalsPerDao + .length, (index) { + return Container( + margin: + const EdgeInsets.symmetric( + vertical: 10), + child: HyphaProposalHistoryCard( + dao: state + .historyProposalsPerDao[ + index] + .dao, + subTitle: + '${state.historyProposalsPerDao[index].proposals.length} Past Proposals', + )); + }), + const SizedBox( + height: 100, + ) + ], + ), ], ), ), diff --git a/lib/ui/proposals/list/interactor/proposals_bloc.dart b/lib/ui/proposals/list/interactor/proposals_bloc.dart index 0c86697b..bc2eed9d 100644 --- a/lib/ui/proposals/list/interactor/proposals_bloc.dart +++ b/lib/ui/proposals/list/interactor/proposals_bloc.dart @@ -3,6 +3,7 @@ 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/dao_proposals_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'; @@ -13,7 +14,9 @@ import 'package:hypha_wallet/ui/proposals/list/interactor/get_proposals_use_case import 'package:hypha_wallet/ui/proposals/list/usecases/get_proposals_use_case.dart'; part 'proposals_bloc.freezed.dart'; + part 'proposals_event.dart'; + part 'proposals_state.dart'; class ProposalsBloc extends Bloc { @@ -21,11 +24,12 @@ class ProposalsBloc extends Bloc { final FetchProfileUseCase _fetchProfileUseCase; final ErrorHandlerManager _errorHandlerManager; - ProposalsBloc(this._getProposalsUseCase, this._fetchProfileUseCase, this._errorHandlerManager) : super(const ProposalsState()) { + ProposalsBloc(this._getProposalsUseCase, this._fetchProfileUseCase, + this._errorHandlerManager) + : super(const ProposalsState()) { on<_Initial>(_initial); } - List? _daos; FilterStatus filterStatus = FilterStatus.active; Future _initial(_Initial event, Emitter emit) async { @@ -34,29 +38,43 @@ class ProposalsBloc extends Bloc { filterStatus = event.filterStatus; } - if (_daos == null) { - final Result profileResult = await _fetchProfileUseCase.run(); + await _fetchAndEmitProposals(emit, filterStatus); + } + + Future _fetchAndEmitProposals( + Emitter emit, FilterStatus filterStatus) async { + // Fetch DAOs + final Result profileResult = + await _fetchProfileUseCase.run(); + + if (profileResult.isValue && profileResult.asValue!.value.daos.isNotEmpty) { + final List daos = profileResult.asValue!.value.daos; + + // Fetch Proposals using the fetched DAOs + final Result, HyphaError> proposalsResult = + await _getProposalsUseCase + .run(GetProposalsUseCaseInput(daos, filterStatus)); - if (profileResult.isValue && profileResult.asValue!.value.daos.isNotEmpty) { - _daos = profileResult.asValue!.value.daos; - await _fetchAndEmitProposals(emit, _daos!, filterStatus); + final Result, HyphaError> + historyProposalsPerDaoResult = await _getProposalsUseCase + .run1(GetProposalsUseCaseInput(daos, FilterStatus.past)); + + if (proposalsResult.isValue && historyProposalsPerDaoResult.isValue) { + // Emit both daos and proposals in one state + emit(state.copyWith( + pageState: PageState.success, + historyProposalsPerDao: historyProposalsPerDaoResult.asValue!.value, + proposals: proposalsResult.asValue!.value, + )); } else { - final HyphaError error = profileResult.isError ? profileResult.asError!.error : HyphaError.api('Failed to retrieve DAOs'); - await _errorHandlerManager.handlerError(error); + await _errorHandlerManager.handlerError(proposalsResult.asError!.error); emit(state.copyWith(pageState: PageState.failure)); } } else { - await _fetchAndEmitProposals(emit, _daos!, filterStatus); - } - } - - Future _fetchAndEmitProposals(Emitter emit, List daos, FilterStatus filterStatus) async { - final Result, HyphaError> proposalsResult = await _getProposalsUseCase.run(GetProposalsUseCaseInput(daos, filterStatus)); - - if (proposalsResult.isValue) { - emit(state.copyWith(pageState: PageState.success, proposals: proposalsResult.asValue!.value)); - } else { - await _errorHandlerManager.handlerError(proposalsResult.asError!.error); + final HyphaError error = profileResult.isError + ? profileResult.asError!.error + : HyphaError.api('Failed to retrieve DAOs'); + await _errorHandlerManager.handlerError(error); emit(state.copyWith(pageState: PageState.failure)); } } diff --git a/lib/ui/proposals/list/interactor/proposals_bloc.freezed.dart b/lib/ui/proposals/list/interactor/proposals_bloc.freezed.dart index 79658a00..4e51ca68 100644 --- a/lib/ui/proposals/list/interactor/proposals_bloc.freezed.dart +++ b/lib/ui/proposals/list/interactor/proposals_bloc.freezed.dart @@ -255,6 +255,8 @@ abstract class _Initial implements ProposalsEvent { mixin _$ProposalsState { PageState get pageState => throw _privateConstructorUsedError; List get proposals => throw _privateConstructorUsedError; + List get historyProposalsPerDao => + throw _privateConstructorUsedError; /// Create a copy of ProposalsState /// with the given fields replaced by the non-null parameter values. @@ -269,7 +271,10 @@ abstract class $ProposalsStateCopyWith<$Res> { ProposalsState value, $Res Function(ProposalsState) then) = _$ProposalsStateCopyWithImpl<$Res, ProposalsState>; @useResult - $Res call({PageState pageState, List proposals}); + $Res call( + {PageState pageState, + List proposals, + List historyProposalsPerDao}); } /// @nodoc @@ -289,6 +294,7 @@ class _$ProposalsStateCopyWithImpl<$Res, $Val extends ProposalsState> $Res call({ Object? pageState = null, Object? proposals = null, + Object? historyProposalsPerDao = null, }) { return _then(_value.copyWith( pageState: null == pageState @@ -299,6 +305,10 @@ class _$ProposalsStateCopyWithImpl<$Res, $Val extends ProposalsState> ? _value.proposals : proposals // ignore: cast_nullable_to_non_nullable as List, + historyProposalsPerDao: null == historyProposalsPerDao + ? _value.historyProposalsPerDao + : historyProposalsPerDao // ignore: cast_nullable_to_non_nullable + as List, ) as $Val); } } @@ -311,7 +321,10 @@ abstract class _$$ProposalsStateImplCopyWith<$Res> __$$ProposalsStateImplCopyWithImpl<$Res>; @override @useResult - $Res call({PageState pageState, List proposals}); + $Res call( + {PageState pageState, + List proposals, + List historyProposalsPerDao}); } /// @nodoc @@ -329,6 +342,7 @@ class __$$ProposalsStateImplCopyWithImpl<$Res> $Res call({ Object? pageState = null, Object? proposals = null, + Object? historyProposalsPerDao = null, }) { return _then(_$ProposalsStateImpl( pageState: null == pageState @@ -339,6 +353,10 @@ class __$$ProposalsStateImplCopyWithImpl<$Res> ? _value._proposals : proposals // ignore: cast_nullable_to_non_nullable as List, + historyProposalsPerDao: null == historyProposalsPerDao + ? _value._historyProposalsPerDao + : historyProposalsPerDao // ignore: cast_nullable_to_non_nullable + as List, )); } } @@ -348,8 +366,10 @@ class __$$ProposalsStateImplCopyWithImpl<$Res> class _$ProposalsStateImpl implements _ProposalsState { const _$ProposalsStateImpl( {this.pageState = PageState.initial, - final List proposals = const []}) - : _proposals = proposals; + final List proposals = const [], + final List historyProposalsPerDao = const []}) + : _proposals = proposals, + _historyProposalsPerDao = historyProposalsPerDao; @override @JsonKey() @@ -363,9 +383,19 @@ class _$ProposalsStateImpl implements _ProposalsState { return EqualUnmodifiableListView(_proposals); } + final List _historyProposalsPerDao; + @override + @JsonKey() + List get historyProposalsPerDao { + if (_historyProposalsPerDao is EqualUnmodifiableListView) + return _historyProposalsPerDao; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_historyProposalsPerDao); + } + @override String toString() { - return 'ProposalsState(pageState: $pageState, proposals: $proposals)'; + return 'ProposalsState(pageState: $pageState, proposals: $proposals, historyProposalsPerDao: $historyProposalsPerDao)'; } @override @@ -376,12 +406,17 @@ class _$ProposalsStateImpl implements _ProposalsState { (identical(other.pageState, pageState) || other.pageState == pageState) && const DeepCollectionEquality() - .equals(other._proposals, _proposals)); + .equals(other._proposals, _proposals) && + const DeepCollectionEquality().equals( + other._historyProposalsPerDao, _historyProposalsPerDao)); } @override int get hashCode => Object.hash( - runtimeType, pageState, const DeepCollectionEquality().hash(_proposals)); + runtimeType, + pageState, + const DeepCollectionEquality().hash(_proposals), + const DeepCollectionEquality().hash(_historyProposalsPerDao)); /// Create a copy of ProposalsState /// with the given fields replaced by the non-null parameter values. @@ -395,13 +430,17 @@ class _$ProposalsStateImpl implements _ProposalsState { abstract class _ProposalsState implements ProposalsState { const factory _ProposalsState( - {final PageState pageState, - final List proposals}) = _$ProposalsStateImpl; + {final PageState pageState, + final List proposals, + final List historyProposalsPerDao}) = + _$ProposalsStateImpl; @override PageState get pageState; @override List get proposals; + @override + List get historyProposalsPerDao; /// Create a copy of ProposalsState /// with the given fields replaced by the non-null parameter values. diff --git a/lib/ui/proposals/list/interactor/proposals_state.dart b/lib/ui/proposals/list/interactor/proposals_state.dart index ffa79c03..79bea556 100644 --- a/lib/ui/proposals/list/interactor/proposals_state.dart +++ b/lib/ui/proposals/list/interactor/proposals_state.dart @@ -5,5 +5,6 @@ class ProposalsState with _$ProposalsState { const factory ProposalsState({ @Default(PageState.initial) PageState pageState, @Default([]) List proposals, + @Default([]) List historyProposalsPerDao, }) = _ProposalsState; } diff --git a/lib/ui/proposals/list/usecases/get_proposals_use_case.dart b/lib/ui/proposals/list/usecases/get_proposals_use_case.dart index 67f7e40b..07f59fb8 100644 --- a/lib/ui/proposals/list/usecases/get_proposals_use_case.dart +++ b/lib/ui/proposals/list/usecases/get_proposals_use_case.dart @@ -6,6 +6,8 @@ import 'package:hypha_wallet/ui/architecture/interactor/base_usecase.dart'; import 'package:hypha_wallet/ui/architecture/result/result.dart'; import 'package:hypha_wallet/ui/proposals/list/interactor/get_proposals_use_case_input.dart'; +import '../../../../core/network/models/dao_proposals_model.dart'; + class GetProposalsUseCase extends InputUseCase, HyphaError>, GetProposalsUseCaseInput> { final AuthRepository _authRepository; final ProposalRepository _proposalRepository; @@ -14,4 +16,5 @@ class GetProposalsUseCase extends InputUseCase, Hypha @override Future, HyphaError>> run(GetProposalsUseCaseInput input) async => _proposalRepository.getProposals(_authRepository.authDataOrCrash.userProfileData, input); + Future, HyphaError>> run1(GetProposalsUseCaseInput input) async => _proposalRepository.getHistoryProposalsPerDao(_authRepository.authDataOrCrash.userProfileData, input); }