diff --git a/catalyst_voices/apps/voices/lib/dependency/dependencies.dart b/catalyst_voices/apps/voices/lib/dependency/dependencies.dart index 81fbdf8d99e0..59a4acdac3f9 100644 --- a/catalyst_voices/apps/voices/lib/dependency/dependencies.dart +++ b/catalyst_voices/apps/voices/lib/dependency/dependencies.dart @@ -469,14 +469,23 @@ final class Dependencies extends DependencyProvider { }, dispose: (storage) async => storage.dispose(), ); + registerLazySingleton( + () { + return AppMetaStorageLocalStorage( + sharedPreferences: get(), + ); + }, + ); } void _registerUtils() { registerLazySingleton( () { return SyncManager( + get(), get(), get(), + get(), ); }, dispose: (manager) async => manager.dispose(), diff --git a/catalyst_voices/apps/voices/lib/pages/proposal_builder/appbar/proposal_builder_status_action.dart b/catalyst_voices/apps/voices/lib/pages/proposal_builder/appbar/proposal_builder_status_action.dart index c39b1aec001a..662a7505a520 100644 --- a/catalyst_voices/apps/voices/lib/pages/proposal_builder/appbar/proposal_builder_status_action.dart +++ b/catalyst_voices/apps/voices/lib/pages/proposal_builder/appbar/proposal_builder_status_action.dart @@ -29,6 +29,7 @@ class ProposalBuilderStatusAction extends StatelessWidget { offstage: state.isLoading || state.error != null, items: ProposalMenuItemAction.proposalBuilderAvailableOptions( state.metadata.publish, + fromActiveCampaign: state.metadata.fromActiveCampaign, ), ); }, diff --git a/catalyst_voices/apps/voices/lib/pages/workspace/proposal_menu_action_button.dart b/catalyst_voices/apps/voices/lib/pages/workspace/proposal_menu_action_button.dart index eeaeff583fde..3b7595784531 100644 --- a/catalyst_voices/apps/voices/lib/pages/workspace/proposal_menu_action_button.dart +++ b/catalyst_voices/apps/voices/lib/pages/workspace/proposal_menu_action_button.dart @@ -23,6 +23,7 @@ class ProposalMenuActionButton extends StatefulWidget { final int version; final String title; final bool hasNewerLocalIteration; + final bool fromActiveCampaign; const ProposalMenuActionButton({ super.key, @@ -31,6 +32,7 @@ class ProposalMenuActionButton extends StatefulWidget { required this.version, required this.title, required this.hasNewerLocalIteration, + required this.fromActiveCampaign, }); @override @@ -65,6 +67,7 @@ class _ProposalMenuActionButtonState extends State { List get _items => ProposalMenuItemAction.workspaceAvailableOptions( widget.proposalPublish, + fromActiveCampaign: widget.fromActiveCampaign, ); @override diff --git a/catalyst_voices/apps/voices/lib/pages/workspace/user_proposals/user_proposal_section.dart b/catalyst_voices/apps/voices/lib/pages/workspace/user_proposals/user_proposal_section.dart index e0c918488f2b..bb03114c2d1f 100644 --- a/catalyst_voices/apps/voices/lib/pages/workspace/user_proposals/user_proposal_section.dart +++ b/catalyst_voices/apps/voices/lib/pages/workspace/user_proposals/user_proposal_section.dart @@ -8,7 +8,7 @@ class UserProposalSection extends StatefulWidget { final List items; final String title; final String info; - final String learnMoreUrl; + final String? learnMoreUrl; final String emptyTextMessage; const UserProposalSection({ @@ -17,7 +17,7 @@ class UserProposalSection extends StatefulWidget { required this.emptyTextMessage, required this.title, required this.info, - required this.learnMoreUrl, + this.learnMoreUrl, }); @override diff --git a/catalyst_voices/apps/voices/lib/pages/workspace/user_proposals/user_proposals.dart b/catalyst_voices/apps/voices/lib/pages/workspace/user_proposals/user_proposals.dart index 276ab4585709..53518f093c00 100644 --- a/catalyst_voices/apps/voices/lib/pages/workspace/user_proposals/user_proposals.dart +++ b/catalyst_voices/apps/voices/lib/pages/workspace/user_proposals/user_proposals.dart @@ -29,10 +29,16 @@ class _Header extends StatelessWidget { } class _UserProposalsState extends State { - List get _draft => widget.items.where((e) => e.publish.isDraft).toList(); - List get _local => widget.items.where((e) => e.publish.isLocal).toList(); + Iterable get _active => widget.items.where((e) => e.fromActiveCampaign); + + List get _draft => _active.where((e) => e.publish.isDraft).toList(); + + Iterable get _inactive => widget.items.where((e) => !e.fromActiveCampaign); + + List get _local => _active.where((e) => e.publish.isLocal).toList(); + List get _submitted => - widget.items.where((e) => e.publish.isPublished).toList(); + _active.where((e) => e.publish.isPublished).toList(); @override Widget build(BuildContext context) { @@ -74,6 +80,13 @@ class _UserProposalsState extends State { info: context.l10n.notPublishedInfoMarkdown, learnMoreUrl: VoicesConstants.proposalPublishingDocsUrl, ), + if (_inactive.isNotEmpty) + UserProposalSection( + items: _inactive.toList(), + emptyTextMessage: '', + title: context.l10n.notActiveCampaign, + info: context.l10n.notActiveCampaignInfoMarkdown, + ), ], ), ); diff --git a/catalyst_voices/apps/voices/lib/widgets/cards/proposal/workspace_proposal_card.dart b/catalyst_voices/apps/voices/lib/widgets/cards/proposal/workspace_proposal_card.dart index d768e8398c56..fcd67dff2a90 100644 --- a/catalyst_voices/apps/voices/lib/widgets/cards/proposal/workspace_proposal_card.dart +++ b/catalyst_voices/apps/voices/lib/widgets/cards/proposal/workspace_proposal_card.dart @@ -122,6 +122,7 @@ class _Body extends StatelessWidget { title: proposal.title, version: proposal.iteration, hasNewerLocalIteration: proposal.hasNewerLocalIteration, + fromActiveCampaign: proposal.fromActiveCampaign, ), ], ); diff --git a/catalyst_voices/apps/voices/lib/widgets/headers/section_learn_more_header.dart b/catalyst_voices/apps/voices/lib/widgets/headers/section_learn_more_header.dart index d6be75f2725b..97054d1929a9 100644 --- a/catalyst_voices/apps/voices/lib/widgets/headers/section_learn_more_header.dart +++ b/catalyst_voices/apps/voices/lib/widgets/headers/section_learn_more_header.dart @@ -8,7 +8,7 @@ import 'package:flutter/material.dart'; class SectionLearnMoreHeader extends StatelessWidget with LaunchUrlMixin { final String title; final String info; - final String learnMoreUrl; + final String? learnMoreUrl; final bool isExpanded; final ValueChanged? onExpandedChanged; @@ -16,7 +16,7 @@ class SectionLearnMoreHeader extends StatelessWidget with LaunchUrlMixin { super.key, required this.title, required this.info, - required this.learnMoreUrl, + this.learnMoreUrl, this.isExpanded = false, this.onExpandedChanged, }); @@ -50,7 +50,7 @@ class SectionLearnMoreHeader extends StatelessWidget with LaunchUrlMixin { ), ), const Spacer(), - VoicesLearnMoreTextButton.url(url: learnMoreUrl), + if (learnMoreUrl case final value?) VoicesLearnMoreTextButton.url(url: value), ], ), ); diff --git a/catalyst_voices/apps/voices/test/widgets/cards/small_proposal_card_test.dart b/catalyst_voices/apps/voices/test/widgets/cards/small_proposal_card_test.dart index 999f36c2cf0a..6c20099eaa85 100644 --- a/catalyst_voices/apps/voices/test/widgets/cards/small_proposal_card_test.dart +++ b/catalyst_voices/apps/voices/test/widgets/cards/small_proposal_card_test.dart @@ -67,6 +67,7 @@ void main() { commentsCount: 0, category: 'Cardano Use Cases: Concept', categoryId: SignedDocumentRef.generateFirstRef(), + fromActiveCampaign: true, ); }); diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/discovery/discovery_cubit.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/discovery/discovery_cubit.dart index 4f18406d825b..7edcf659ec31 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/discovery/discovery_cubit.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/discovery/discovery_cubit.dart @@ -153,7 +153,7 @@ class DiscoveryCubit extends Cubit with BlocErrorEmitterMixin { return _proposalService .watchProposalsPage( request: const PageRequest(page: 0, size: _maxRecentProposalsCount), - filters: const ProposalsFilters(), + filters: ProposalsFilters.forActiveCampaign(), order: const UpdateDate(isAscending: false), ) .map((event) => event.items) diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposal_builder/new_proposal/new_proposal_cubit.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposal_builder/new_proposal/new_proposal_cubit.dart index 6005ec79ba91..fc8db7cd088b 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposal_builder/new_proposal/new_proposal_cubit.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposal_builder/new_proposal/new_proposal_cubit.dart @@ -79,27 +79,28 @@ class NewProposalCubit extends Cubit if (campaign == null) { throw StateError('Cannot load proposal, active campaign not found'); } - final templateRef = await _proposalService.getProposalTemplate( - // TODO(LynxLynxx): when we have separate proposal template for generic questions use it here - // right now user can start creating proposal without selecting category. - // Right now every category have the same requirements for title so we can do a fallback for - // first category from the list. - ref: campaign.categories - .firstWhere( - (e) => e.selfRef == categoryRef, - orElse: () => campaign.categories.first, - ) - .proposalTemplateRef, - ); - final titlePropertySchema = - templateRef.schema.getPropertySchema(ProposalDocument.titleNodeId)! - as DocumentStringSchema; - final titleRange = titlePropertySchema.strLengthRange; + // TODO(LynxLynxx): when we have separate proposal template for generic questions use it here + // right now user can start creating proposal without selecting category. + // Right now every category have the same requirements for title so we can do a fallback for + // first category from the list. + final templateRef = campaign.categories + .cast() + .firstWhere( + (e) => e?.selfRef == categoryRef, + orElse: () => campaign.categories.firstOrNull, + ) + ?.proposalTemplateRef; + + final template = templateRef != null + ? await _proposalService.getProposalTemplate(ref: templateRef) + : null; + final titleRange = template?.title?.strLengthRange; final categories = campaign.categories .map(CampaignCategoryDetailsViewModel.fromModel) .toList(); + final newState = state.copyWith( isLoading: false, step: step, diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposal_builder/proposal_builder_bloc.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposal_builder/proposal_builder_bloc.dart index 086582f33657..7e6b1ff56332 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposal_builder/proposal_builder_bloc.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposal_builder/proposal_builder_bloc.dart @@ -538,6 +538,9 @@ final class ProposalBuilderBloc extends Bloc versions; + final bool fromActiveCampaign; const ProposalBuilderMetadata({ this.publish = ProposalPublish.localDraft, @@ -19,6 +20,7 @@ final class ProposalBuilderMetadata extends Equatable { this.templateRef, this.categoryId, this.versions = const [], + this.fromActiveCampaign = true, }); factory ProposalBuilderMetadata.newDraft({ @@ -43,6 +45,7 @@ final class ProposalBuilderMetadata extends Equatable { templateRef, categoryId, versions, + fromActiveCampaign, ]; ProposalBuilderMetadata copyWith({ @@ -52,6 +55,7 @@ final class ProposalBuilderMetadata extends Equatable { Optional? templateRef, Optional? categoryId, List? versions, + bool? fromActiveCampaign, }) { return ProposalBuilderMetadata( publish: publish ?? this.publish, @@ -60,6 +64,7 @@ final class ProposalBuilderMetadata extends Equatable { templateRef: templateRef.dataOr(this.templateRef), categoryId: categoryId.dataOr(this.categoryId), versions: versions ?? this.versions, + fromActiveCampaign: fromActiveCampaign ?? this.fromActiveCampaign, ); } } diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposals/proposals_cubit.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposals/proposals_cubit.dart index a8a5e30ada6c..5822f75e89a8 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposals/proposals_cubit.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposals/proposals_cubit.dart @@ -294,7 +294,7 @@ final class ProposalsCubit extends Cubit void _resetCache() { final activeAccount = _userService.user.activeAccount; - final filters = ProposalsFilters(author: activeAccount?.catalystId); + final filters = ProposalsFilters.forActiveCampaign(author: activeAccount?.catalystId); _cache = ProposalsCubitCache(filters: filters); } diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/voting/voting_cubit.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/voting/voting_cubit.dart index 626de0cb077e..bd928ae16fb7 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/voting/voting_cubit.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/voting/voting_cubit.dart @@ -360,7 +360,7 @@ final class VotingCubit extends Cubit void _resetCache() { final activeAccount = _userService.user.activeAccount; - final filters = ProposalsFilters(author: activeAccount?.catalystId); + final filters = ProposalsFilters.forActiveCampaign(author: activeAccount?.catalystId); _cache = VotingCubitCache( filters: filters, diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/workspace/workspace_bloc.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/workspace/workspace_bloc.dart index e8e8eadf264a..45bdcbda8b49 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/workspace/workspace_bloc.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/workspace/workspace_bloc.dart @@ -184,17 +184,28 @@ final class WorkspaceBloc extends Bloc Future> _mapProposalToViewModel( List proposals, - int fundNumber, ) async { final futures = proposals.map((proposal) async { _cachedCampaign ??= await _campaignService.getActiveCampaign(); - final category = _cachedCampaign?.categories.firstWhere( - (e) => e.selfRef.id == proposal.categoryRef.id, - ); + + // TODO(damian-molinski): proposal should have ref to campaign + final campaigns = Campaign.all; + + final categories = campaigns.expand((element) => element.categories); + final category = categories.firstWhereOrNull((e) => e.selfRef.id == proposal.categoryRef.id); + + // TODO(damian-molinski): refactor it + final fundNumber = category != null + ? campaigns.firstWhere((campaign) => campaign.hasCategory(category.selfRef.id)).fundNumber + : 0; + + final fromActiveCampaign = fundNumber == _cachedCampaign?.fundNumber; + return UsersProposalOverview.fromProposal( proposal, fundNumber, category?.formattedCategoryName ?? '', + fromActiveCampaign: fromActiveCampaign, ); }).toList(); @@ -212,7 +223,7 @@ final class WorkspaceBloc extends Bloc (proposals) async { if (isClosed) return; _logger.info('Stream received ${proposals.length} proposals'); - final mappedProposals = await _mapProposalToViewModel(proposals, state.fundNumber); + final mappedProposals = await _mapProposalToViewModel(proposals); add(LoadProposalsEvent(mappedProposals)); }, onError: (Object error, StackTrace stackTrace) { diff --git a/catalyst_voices/packages/internal/catalyst_voices_localization/lib/l10n/intl_en.arb b/catalyst_voices/packages/internal/catalyst_voices_localization/lib/l10n/intl_en.arb index 395debe5365f..ffde740fb9c7 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_localization/lib/l10n/intl_en.arb +++ b/catalyst_voices/packages/internal/catalyst_voices_localization/lib/l10n/intl_en.arb @@ -2035,6 +2035,14 @@ "@notPublishedInfoMarkdown": { "description": "Info message shown in tooltip info in workspace page" }, + "notActiveCampaign": "Not active campaign", + "@notActiveCampaign": { + "description": "Label for proposals from not active campaign" + }, + "notActiveCampaignInfoMarkdown": "**Read only**", + "@notActiveCampaignInfoMarkdown": { + "description": "Info message shown in tooltip info in workspace page" + }, "notPublishedProposals": "Not published proposals", "@notPublishedProposals": { "description": "Title for section to show not published proposals" diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/app/app_meta.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/app/app_meta.dart new file mode 100644 index 000000000000..296b021b6556 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/app/app_meta.dart @@ -0,0 +1,23 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:equatable/equatable.dart'; + +final class AppMeta extends Equatable { + final DocumentRef? activeCampaign; + + const AppMeta({ + this.activeCampaign, + }); + + @override + List get props => [ + activeCampaign, + ]; + + AppMeta copyWith({ + Optional? activeCampaign, + }) { + return AppMeta( + activeCampaign: activeCampaign.dataOr(this.activeCampaign), + ); + } +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/campaign.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/campaign.dart index 2c6eacc92fba..2d8a9384be5f 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/campaign.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/campaign.dart @@ -1,9 +1,22 @@ import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_models/src/campaign/constant/f14_static_campaign_categories.dart'; +import 'package:catalyst_voices_models/src/campaign/constant/f14_static_campaign_timeline.dart'; +import 'package:catalyst_voices_models/src/campaign/constant/f15_static_campaign_categories.dart'; +import 'package:catalyst_voices_models/src/campaign/constant/f15_static_campaign_timeline.dart'; import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; +import 'package:collection/collection.dart'; import 'package:equatable/equatable.dart'; final class Campaign extends Equatable { - static final f14Ref = SignedDocumentRef.generateFirstRef(); + // Frontend only references. F14 and F15 do not mean anything for backend. + // They're only used to difference between campaigns. + static const f14Ref = SignedDocumentRef.first('01997695-e26f-70db-b9d4-92574a806bcd'); + static const f15Ref = SignedDocumentRef.first('01997696-2024-7438-9178-f7d29b2c1ddb'); + + static final all = [ + Campaign.f14(), + Campaign.f15(), + ]; // Using DocumentRef instead of SignedDocumentRef because in Campaign Treasury user can create // 'draft' version of campaign like Proposal @@ -35,21 +48,26 @@ final class Campaign extends Equatable { name: 'Catalyst Fund14', description: ''' Project Catalyst turns economic power into innovation power by using the Cardano Treasury to incentivize and fund community-approved ideas.''', - allFunds: MultiCurrencyAmount.single( - Money.fromMajorUnits( - currency: Currencies.ada, - majorUnits: BigInt.from(20000000), - ), - ), - totalAsk: MultiCurrencyAmount.single( - Money.zero( - currency: Currencies.ada, - ), - ), + allFunds: MultiCurrencyAmount.single(Currencies.ada.amount(20000000)), + totalAsk: MultiCurrencyAmount.single(Money.zero(currency: Currencies.ada)), fundNumber: 14, - timeline: CampaignTimeline(phases: CampaignPhaseX.f14StaticContent), + timeline: f14StaticCampaignTimeline, + publish: CampaignPublish.published, + categories: f14StaticCampaignCategories, + ); + } + + factory Campaign.f15() { + return Campaign( + selfRef: f15Ref, + name: 'Catalyst Fund15', + description: '''TODO''', + allFunds: MultiCurrencyAmount.single(Currencies.ada.amount(20000000)), + totalAsk: MultiCurrencyAmount.single(Money.zero(currency: Currencies.ada)), + fundNumber: 15, + timeline: f15StaticCampaignTimeline, publish: CampaignPublish.published, - categories: staticCampaignCategories, + categories: f15StaticCampaignCategories, ); } @@ -160,6 +178,10 @@ Project Catalyst turns economic power into innovation power by using the Cardano ); } + bool hasCategory(String id) { + return categories.any((element) => element.selfRef.id == id); + } + /// Returns the state of the campaign for a specific phase. /// It's a shortcut for [state] when you are only interested in a specific phase. And want to know /// the status of that phase. diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/campaign_category.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/campaign_category.dart index 35775a3f5308..db06e00451c9 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/campaign_category.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/campaign_category.dart @@ -2,360 +2,6 @@ import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; import 'package:equatable/equatable.dart'; -/// List of static [CampaignCategory] definitions. -/// -/// Categories will come from documents later. -/// -/// See [CampaignCategory]. -final staticCampaignCategories = [ - CampaignCategory( - selfRef: constantDocumentsRefs[0].category, - proposalTemplateRef: constantDocumentsRefs[0].proposal, - campaignRef: Campaign.f14Ref, - categoryName: 'Cardano Use Case:', - categorySubname: 'Partners & Products', - description: - '''Cardano Use Cases: Partners & Products empowers exceptional applications and enterprise collaborations to enhance products and services with capabilities that drive high-volume transactions and accelerates mainstream adoption.''', - shortDescription: - 'For Tier-1 collaborations and real-world pilots that scale Cardano adoption through high-impact use cases.', - proposalsCount: 0, - availableFunds: MultiCurrencyAmount.single(_adaMajorUnits(8500000)), - imageUrl: '', - totalAsk: MultiCurrencyAmount.single(_adaMajorUnits(0)), - range: ComparableRange( - min: _adaMajorUnits(250000), - max: _adaMajorUnits(1000000), - ), - currency: Currencies.ada, - descriptions: const [ - CategoryDescription( - title: 'Overview', - description: ''' -Cardano Partners & Products accelerates Cardano’s mainstream adoption by supporting mature, mainnet-deployed products and strategic enterprise collaborations that deliver real-world utility and high-value transactions.\n\nUnlike Cardano Use Cases: Concepts, which funds novel, early-stage ideas like prototypes or MVPs, this funding category is for established teams or enterprises with proven products or collaborations driving measurable adoption.''', - ), - CategoryDescription( - title: 'Who should apply', - description: ''' -Cardano Partners & Products funds enterprise R&D collaborations between Cardano-based businesses and teams with Tier-1 industry leaders to integrate Cardano solutions into real world use cases.\n\nThis category is for established enterprises or startups with mainnet-deployed Cardano-based products or industry leading collaborations.\n\nIf your project is an early-stage concept, prototype, or lacks mainnet deployment, apply to _Cardano Use Cases: Concepts_ instead. -''', - ), - CategoryDescription( - title: 'Areas of Interest', - description: ''' -Proposals should focus on mature R&D for products with Tier-1 collaborations, such as: - -- Enterprise integrations and demonstrator pilots with Tier-1 industry leaders _e.g. embedding Cardano wallets in automotive systems._ -- Stablecoin use-cases: Partner-led pilots that show Cardano stablecoins in action supporting real transactions, liquidity, utility, or payments on Cardano. -- Maturing use cases using Cardano scaling solutions like Hydra _e.g. a high-throughput payment system into household-name ecommerce marketplace providers_ -- Applications leveraging exponential technologies like AI _e.g. an AI-enhanced supply chain solution with a large manufacturer_ -- All projects should have measurable adoption outcomes _e.g. a tokenized asset platform with validated transaction growth._ -''', - ), - CategoryDescription( - title: 'Proposal Guidance', - description: ''' -- Proposals must demonstrate measurable adoption outcomes and a clear link to Cardano’s blockchain _e.g. transaction volume, user growth, liquidity metrics_ -- Projects must be led by or partnered with industry leading businesses that have 2+ years business track record -- Projects must provide evidence of a working product -- Projects should be prepared to demonstrate an established business collaboration as well as relevant experience in the area to at least the Catalyst Team as the Fund Operator, if commercial details cannot be made public -- Clearly state funding commitments to show their commitment to the project -- Projects may be proprietary or open source but must drive high-volume/value transactions and co-marketing opportunities. -- Co-marketing plans and community engagement to amplify Cardano’s partnership visibility. -- Early-stage concepts or prototypes should apply to _Cardano Use Cases: Concepts._ -- Projects that aren’t primarily focused on R&D and demonstrating the utility of enabling applications -''', - ), - CategoryDescription( - title: 'Eligibility Criteria', - description: ''' -The following will **not** be funded: - -- Early-stage concepts or prototypes _e.g. a new DeFi protocol MVP proposal belongs in Cardano Use Cases: Concepts._ -- Foundational R&D or maintenance for critical infrastructure. These belong to other Cardano funding streams such as via a members organization or direct governance action for treasury withdrawal and administrations _e.g. Cardano protocol, Layer 2 solutions_ -- Proposals lacking evidence of a mainnet-deployed product or collaboration with a Tier-1 industry leader. -- Developer tools, middleware, APIs, or integrated development environments belong in Cardano Open: Developers. -- Projects lacking measurable adoption outcomes or Cardano relevance. -- Proposals to use funds primarily for liquidity, loans, NFTs, or buying token-based products without technical integration. -- Informational content, websites, or blogs _e.g. a Cardano education site_. -''', - ), - ], - dos: const [ - '**Provide proof** of established collaborations', - '**Provide proof** of key performance metrics', - '**Clarify** funding commitments', - ], - donts: const [ - '**No** prototype R&D', - '**No** Cardano infrastructure', - '**No** way to prove your impact', - ], - submissionCloseDate: DateTimeExt.now(), - ), - CampaignCategory( - selfRef: constantDocumentsRefs[1].category, - proposalTemplateRef: constantDocumentsRefs[1].proposal, - campaignRef: Campaign.f14Ref, - categoryName: 'Cardano Use Case:', - categorySubname: 'Concept', - description: - '''Cardano Use Cases: Concepts funds novel, early-stage Cardano-based concepts developing proof of concept prototypes through deploying minimum viable products (MVP) to validate innovative products, services, or business models driving Cardano adoption.''', - shortDescription: - 'For early-stage ideas to create, test, and validate Cardano-based prototypes to MVP innovations.', - proposalsCount: 0, - availableFunds: MultiCurrencyAmount.single(_adaMajorUnits(4000000)), - imageUrl: '', - totalAsk: MultiCurrencyAmount.single(_adaMajorUnits(0)), - range: ComparableRange( - min: _adaMajorUnits(15000), - max: _adaMajorUnits(100000), - ), - currency: Currencies.ada, - descriptions: const [ - CategoryDescription( - title: 'Overview', - description: ''' -Cardano Use Cases: Concepts category fuels disruptive, untested Cardano-based use cases to experiment with novel utility and on-chain transactions. - -The funding category supports early-stage ideas - spanning proof of concept, design research, basic prototyping, and minimum viable products (MVPs) - to lay the foundation for innovative products, services, or business models. - -Unlike _Cardano Use Cases: Partners & Products_, which funds mature, deployed products or enterprise collaborations with proven adoption, this category is for newer Catalyst entrants or innovators validating novel concepts and product market fit with no prior development or funding. -''', - ), - CategoryDescription( - title: 'Who should apply?', - description: ''' -This category is for entrepreneurs, designers, and innovators with original, untested Cardano-based concepts, such as a DeFi startup researching a novel lending protocol, a Web3 developer prototyping a zero-knowledge identity solution, or a researcher exploring AI-blockchain integration. If your project involves a mature product, enterprise collaboration, or incremental feature enhancements, apply to _Cardano Use Cases: Partners & Products_ instead. -''', - ), - CategoryDescription( - title: 'Areas of Interest', - description: ''' -- Disruptive industrial innovations using Cardano’s blockchain and smart contracts -- DeFi solutions with unique approaches _e.g. a peer-to-peer yield and novel bond curve protocol MVP._ -- Privacy, oracles, or zero-knowledge proof applications. -- Projects porting or bridging from other ecosystems _e.g. a wallet prototyping Cardano Native Token support_. -- Prototyping on Cardano scaling solutions like Hydra _e.g. a high-throughput payment prototype_. -- Technical blockchain research with exponential technologies _e.g. a feasibility study for AI-driven oracles_. -''', - ), - CategoryDescription( - title: 'Proposal Guidance', - description: ''' -- Proposals adding features to existing early-stage prototypes must provide evidence of the prototype’s novelty and early status _e.g. URL, demo, developer repository_ -- Proposals must describe the concept’s novelty, explaining how it differs from existing solutions and confirming no prior Catalyst funding or development. -- Demonstrate how the project could drive Cardano adoption through increased on-chain transactions and you intend to validate potential. -- Projects may be proprietary or open source but must clearly benefit the Cardano ecosystem. -- New concepts must be original and not previously developed. -- Mature products with enterprise partnerships, and enhancements for deployed solutions should apply to _Cardano Use Cases: Partners & Products._ -- Proposals must have a delivery timeline of no more than 12 months. -''', - ), - CategoryDescription( - title: 'Eligibility Criteria', - description: ''' -The following will **not** be funded: - -- Proposals for mature products, incremental improvements, or enterprise partnerships _e.g. scaling an existing DeFi app or adding features to a deployed wallet belong in Cardano Use Cases: Partners & Products_. -- Proposals focused on informational content only, websites, or blogs _e.g. a Cardano education site belong instead in Cardano Open: Ecosystem._ -- Infrastructure or tooling projects not tied to specific Cardano use cases belong in either Cardano Open: Developers or requesting funds from another Cardano funding stream _e.g. members organization or DAO_. -- Proposals lacking a clear link to Cardano or potential to generate transactions on chain. -''', - ), - ], - dos: const [ - '**Propose** fresh ideas that haven’t been funded before', - '**Produce** a working on-chain prototype or MVP', - '**Provide** realistic blockchain usage projections', - ], - donts: const [ - '**No** existing Cardano products', - '**No** info-only websites and content', - '**No** moon metrics!', - ], - submissionCloseDate: DateTimeExt.now(), - ), - CampaignCategory( - selfRef: constantDocumentsRefs[2].category, - proposalTemplateRef: constantDocumentsRefs[2].proposal, - campaignRef: Campaign.f14Ref, - categoryName: 'Cardano Open:', - categorySubname: 'Developers', - description: ''' -Funds open source tools and environments to enhance the Cardano developer experience. Apply to create impactful, community-driven open source solutions! -''', - shortDescription: - 'For developers to build open-source tools that enhance the Cardano developer experience.', - proposalsCount: 0, - availableFunds: MultiCurrencyAmount.single(_adaMajorUnits(3100000)), - imageUrl: '', - totalAsk: MultiCurrencyAmount.single(_adaMajorUnits(0)), - range: ComparableRange( - min: _adaMajorUnits(15000), - max: _adaMajorUnits(100000), - ), - currency: Currencies.ada, - descriptions: const [ - CategoryDescription( - title: 'Overview', - description: ''' -Cardano Open: Developers will fund devs, programmers, and engineers creating or contributing to open source technologies that strengthen the Cardano developer ecosystem. We believe in open source software, hardware, and data solutions driving transparency, security, and collaboration for the good of the network. - -The goal is to streamline integrated development environments, boost coding efficiency, and simplify the developer experience on Cardano. - -Learn more about open source at [opensource.org](https://opensource.org/). -''', - ), - CategoryDescription( - title: 'Who should apply?', - description: ''' -This category is for software developers, blockchain engineers, data architects, and open source contributors eager to build O/S tools, libraries, or environments that improve the Cardano developer ecosystem. - -The scope is wide open for creating smart contract debugging tools, crafting SDKs support new languages, or DevOps specialists designing interoperability CLIs. If your proposal focuses on non-technical initiatives like education or marketing, apply to _Cardano Open: Ecosystem_ instead. -''', - ), - CategoryDescription( - title: 'Areas of interest', - description: ''' -Proposals may focus on open source technical solutions, for example: - -- Standardizing, developing, or supporting full-stack solutions and integrated development environments _e.g. new language and code extension support for Plutus debugging_. -- Creating libraries, SDKs, APIs, toolchains, or frameworks _e.g., new SDKs for Cardano privacy preserving smart contracts_. -- Developing governance and blockchain interoperability tooling _e.g. a CLI tool for cross-chain transaction validation_ -''', - ), - CategoryDescription( - title: 'Proposal Guidance', - description: ''' -- Proposals must be source-available from day one with a declared open source repository e.g. GitHub, GitLab. -- Select an OSI-approved open source license e.g. MIT, Apache 2.0, GPLv3) that permits the community to freely copy, inspect, and modify the project’s output. Visit [choosealicense.com](https://choosealicense.com/) for guidance. -- Ensure thorough documentation is available from day one and updated regularly. -- Describe how the project will benefit the Cardano developer community and foster collaboration e.g. through tutorials, webinars, or forum posts. -- Proposals must have a delivery timeline of no more than 12 months. -''', - ), - CategoryDescription( - title: 'Eligibility Criteria', - description: ''' -The following will **not** be funded: - -- Proposals not primarily developing new technology or contributing to existing open source technology _e.g. learning guides for Plutus or Aiken, these would belong in the Cardano Open: Ecosystem category_. -- Proposals producing proprietary software/hardware or open sourcing only a portion of the codebase. -- Proposals lacking an OSI-approved open source license, public repository, or high-quality documentation from day one. -- Proposals not directly enhancing the Cardano developer experience _e.g. general blockchain tools without Cardano-specific integration._ -''', - ), - ], - dos: const [ - '**Open source** licensing of project outputs is required', - '**Clear statement** of your open source license', - '**Open source outputs** from project start', - '**Provide** high quality documentation', - ], - donts: const [ - '**No** proprietary materials (including software or hardware)', - '**No** projects that are non-technical', - '**No** info-only websites and content', - '**Forget to be open**, public, and available on Day 1', - ], - submissionCloseDate: DateTimeExt.now(), - ), - CampaignCategory( - selfRef: constantDocumentsRefs[3].category, - proposalTemplateRef: constantDocumentsRefs[3].proposal, - campaignRef: Campaign.f14Ref, - categoryName: 'Cardano Open:', - categorySubname: 'Ecosystem', - description: ''' -Funds non-technical initiatives like marketing, education, and community building to grow Cardano’s ecosystem and onboard new users globally. -''', - shortDescription: - 'For non-tech projects like marketing, education, or community growth to expand Cardano’s reach.', - proposalsCount: 0, - availableFunds: MultiCurrencyAmount.single(_adaMajorUnits(3000000)), - imageUrl: '', - totalAsk: MultiCurrencyAmount.single(_adaMajorUnits(0)), - range: ComparableRange( - min: _adaMajorUnits(15000), - max: _adaMajorUnits(60000), - ), - currency: Currencies.ada, - descriptions: const [ - CategoryDescription( - title: 'Overview', - description: ''' -Cardano Open: Ecosystem will fund non-technical initiatives that drive grassroots ecosystem growth, education, and community engagement to broaden Cardano’s reach and onboard new users. - -Cardano’s passionate global community is a foundation for growth, yet more can be done to expand adoption. This category focuses on building the creative capacity to deliver local campaigns and beyond that bring awareness and attention to Cardano from the hearts and minds of the grassroots community collective. - -Learn more at [](https://cardano.org/)[cardano.org](http://cardano.org). -''', - ), - CategoryDescription( - title: 'Who should apply?', - description: ''' -This category is for educators, marketers, content creators, and community leaders passionate about expanding Cardano’s global reach through non-technical initiatives, such as marketing experts launching social media campaigns, educators developing blockchain courses, or community organizers hosting hackathons. If your proposal focuses on software or hardware development, apply to Cardano Use Cases or Cardano Open: Developers instead. -''', - ), - CategoryDescription( - title: 'Areas of interest', - description: ''' -Proposals may focus on non-technical initiatives, such as: - -- Cardano Community Hubs that create or maintain a local Cardano presence in cities or regions worldwide -- Growth-focused marketing campaigns and public outreach _e.g. a social media campaign to promote Cardano adoption._ -- Community hackathons or builder outreach events _e.g. a virtual workshop for new developers._ -- Content creation or curation _e.g. educational videos on Cardano’s features_. -- Mentorship, incubation, or collaboration programs _e.g. a startup accelerator for Cardano-based projects._ -- Educational advancements _e.g. a university course on blockchain with Cardano case studies._ -- Local policy, standards, or regulation advocacy _e.g. a policy paper on Cardano’s role in regional finance and pathways to present to local authorities_ -''', - ), - CategoryDescription( - title: 'Proposal Guidance', - description: ''' -- Proposals must focus on non-technical initiatives like marketing, education, or community building, not software/hardware development. -- Clearly articulate how the initiative will expand Cardano’s global footprint or onboard new users, using measurable Key Performance Indicators (KPIs) and relevant metrics _e.g. event attendance, content reach, new user registrations_. -- Ensure activities are inclusive and accessible to both existing and new Cardano communities. -- Proposals must have a delivery timeline of no more than 12 months. -- Publicly accessible outputs where applicable _e.g. educational content, event reports, or marketing materials._ -- A timeline with clear milestones _e.g. campaign launch, event dates, content release._ -- _Optional_: Plans for sustained community engagement, such as follow-up events, social media campaigns, or mentorship programs. -''', - ), - CategoryDescription( - title: 'Eligibility Criteria', - description: ''' -The following will **not** be funded: - -- Proposals not primarily focused on grassroots ecosystem growth through marketing, education, or community building. -- Proposals centered on software or hardware development _e.g. a fan loyalty platform or marketing automation tool belong in Cardano Use Cases category_. -- Proposals lacking clear goals, measurable outcomes, KPIs, or relevance to Cardano’s ecosystem. -''', - ), - ], - dos: const [ - '**Deliver** Community engagement projects', - '**Deliver** grassroots growth, education projects', - '**Clearly outline** goals, activities, and measurable outcomes', - '**Design** for new Cardano communities', - ], - donts: const [ - '**No** software or hardware development', - '**No** vague or irrelevant value to Cardano', - '**No** clear targets or KPIs', - ], - submissionCloseDate: DateTimeExt.now(), - ), -]; - -Money _adaMajorUnits(int majorUnits) { - return Money.fromMajorUnits( - currency: Currencies.ada, - majorUnits: BigInt.from(majorUnits), - ); -} - /// Representation of single [Campaign] category. /// /// Should have factory constructor from document representation. diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/campaign_filters.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/campaign_filters.dart new file mode 100644 index 000000000000..9969674c7b5b --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/campaign_filters.dart @@ -0,0 +1,18 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:equatable/equatable.dart'; + +final class CampaignFilters extends Equatable { + final List categoriesIds; + + const CampaignFilters({ + required this.categoriesIds, + }); + + factory CampaignFilters.active() { + final categoriesIds = activeConstantDocumentRefs.map((e) => e.category.id).toList(); + return CampaignFilters(categoriesIds: categoriesIds); + } + + @override + List get props => [categoriesIds]; +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/campaign_phase.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/campaign_phase.dart index 95e74a2786cd..ba173c7de557 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/campaign_phase.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/campaign_phase.dart @@ -46,76 +46,3 @@ enum CampaignPhaseType { votingResults, projectOnboarding, } - -extension CampaignPhaseX on CampaignPhase { - static List f14StaticContent = [ - CampaignPhase( - title: 'Proposal Submission', - description: - '''Proposers submit initial ideas to solve challenges. Each proposal includes the problem, solution, requested ADA budget, and a clear implementation plan.''', - timeline: DateRange( - from: DateTime.utc(2025, 07, 25, 10), - to: DateTime.utc(2025, 12, 30, 18), - ), - type: CampaignPhaseType.proposalSubmission, - ), - CampaignPhase( - title: 'Voting Registration', - description: - 'During Voter registration, ADA holders register via supported wallet to participate in the Voting.', - timeline: DateRange( - from: DateTime.utc(2025, 07, 05, 18), - to: DateTime.utc(2025, 07, 12, 10), - ), - type: CampaignPhaseType.votingRegistration, - ), - CampaignPhase( - title: 'Community Review', - description: - '''Community members help improve proposals through two key steps: LV0 and LV1 reviewers assess the proposals, then LV2 moderators oversee the process to ensure quality and fairness.''', - timeline: DateRange( - from: DateTime.utc(2025, 07, 03, 8), - to: DateTime.utc(2025, 07, 08, 20), - ), - type: CampaignPhaseType.communityReview, - ), - CampaignPhase( - title: 'Reviewers and Moderators registration', - description: '', - timeline: DateRange( - from: DateTime.utc(2025, 07, 03, 8), - to: DateTime.utc(2025, 07, 04, 20), - ), - type: CampaignPhaseType.reviewRegistration, - ), - CampaignPhase( - title: 'Community Voting', - description: '''Community members cast their votes using the Catalyst Voting app.''', - timeline: DateRange( - from: DateTime.utc(2025, 8, 22, 12), - to: DateTime.utc(2025, 12, 30, 18), - ), - type: CampaignPhaseType.communityVoting, - ), - CampaignPhase( - title: 'Voting Results', - description: - '''Votes are tallied and the results are announced. Rewards are distributed to both voters and community reviewers.''', - timeline: DateRange( - from: DateTime.utc(2025, 07, 18, 9), - to: DateTime.utc(2025, 07, 21, 2), - ), - type: CampaignPhaseType.votingResults, - ), - CampaignPhase( - title: 'Project Onboarding', - description: - '''This phase involves finalizing the key milestones submitted in the Catalyst App during the proposal submission type within the Catalyst Milestone Module. It also includes conducting formal due diligence, and fulfilling all required onboarding steps to become eligible for funding.''', - timeline: DateRange( - from: DateTime.utc(2025, 07, 25, 06), - to: DateTime.utc(2025, 07, 30, 06), - ), - type: CampaignPhaseType.projectOnboarding, - ), - ]; -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/constant/f14_static_campaign_categories.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/constant/f14_static_campaign_categories.dart new file mode 100644 index 000000000000..b4cba67ddfa3 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/constant/f14_static_campaign_categories.dart @@ -0,0 +1,349 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; + +/// List of static [CampaignCategory] definitions. +/// +/// Categories will come from documents later. +/// +/// See [CampaignCategory]. +final f14StaticCampaignCategories = [ + CampaignCategory( + selfRef: f14ConstDocumentsRefs[0].category, + proposalTemplateRef: f14ConstDocumentsRefs[0].proposal, + campaignRef: Campaign.f14Ref, + categoryName: 'Cardano Use Case:', + categorySubname: 'Partners & Products', + description: + '''Cardano Use Cases: Partners & Products empowers exceptional applications and enterprise collaborations to enhance products and services with capabilities that drive high-volume transactions and accelerates mainstream adoption.''', + shortDescription: + 'For Tier-1 collaborations and real-world pilots that scale Cardano adoption through high-impact use cases.', + proposalsCount: 0, + availableFunds: MultiCurrencyAmount.single(Currencies.ada.amount(8500000)), + imageUrl: '', + totalAsk: MultiCurrencyAmount.single(Currencies.ada.amount(0)), + range: ComparableRange( + min: Currencies.ada.amount(250000), + max: Currencies.ada.amount(1000000), + ), + currency: Currencies.ada, + descriptions: const [ + CategoryDescription( + title: 'Overview', + description: ''' +Cardano Partners & Products accelerates Cardano’s mainstream adoption by supporting mature, mainnet-deployed products and strategic enterprise collaborations that deliver real-world utility and high-value transactions.\n\nUnlike Cardano Use Cases: Concepts, which funds novel, early-stage ideas like prototypes or MVPs, this funding category is for established teams or enterprises with proven products or collaborations driving measurable adoption.''', + ), + CategoryDescription( + title: 'Who should apply', + description: ''' +Cardano Partners & Products funds enterprise R&D collaborations between Cardano-based businesses and teams with Tier-1 industry leaders to integrate Cardano solutions into real world use cases.\n\nThis category is for established enterprises or startups with mainnet-deployed Cardano-based products or industry leading collaborations.\n\nIf your project is an early-stage concept, prototype, or lacks mainnet deployment, apply to _Cardano Use Cases: Concepts_ instead. +''', + ), + CategoryDescription( + title: 'Areas of Interest', + description: ''' +Proposals should focus on mature R&D for products with Tier-1 collaborations, such as: + +- Enterprise integrations and demonstrator pilots with Tier-1 industry leaders _e.g. embedding Cardano wallets in automotive systems._ +- Stablecoin use-cases: Partner-led pilots that show Cardano stablecoins in action supporting real transactions, liquidity, utility, or payments on Cardano. +- Maturing use cases using Cardano scaling solutions like Hydra _e.g. a high-throughput payment system into household-name ecommerce marketplace providers_ +- Applications leveraging exponential technologies like AI _e.g. an AI-enhanced supply chain solution with a large manufacturer_ +- All projects should have measurable adoption outcomes _e.g. a tokenized asset platform with validated transaction growth._ +''', + ), + CategoryDescription( + title: 'Proposal Guidance', + description: ''' +- Proposals must demonstrate measurable adoption outcomes and a clear link to Cardano’s blockchain _e.g. transaction volume, user growth, liquidity metrics_ +- Projects must be led by or partnered with industry leading businesses that have 2+ years business track record +- Projects must provide evidence of a working product +- Projects should be prepared to demonstrate an established business collaboration as well as relevant experience in the area to at least the Catalyst Team as the Fund Operator, if commercial details cannot be made public +- Clearly state funding commitments to show their commitment to the project +- Projects may be proprietary or open source but must drive high-volume/value transactions and co-marketing opportunities. +- Co-marketing plans and community engagement to amplify Cardano’s partnership visibility. +- Early-stage concepts or prototypes should apply to _Cardano Use Cases: Concepts._ +- Projects that aren’t primarily focused on R&D and demonstrating the utility of enabling applications +''', + ), + CategoryDescription( + title: 'Eligibility Criteria', + description: ''' +The following will **not** be funded: + +- Early-stage concepts or prototypes _e.g. a new DeFi protocol MVP proposal belongs in Cardano Use Cases: Concepts._ +- Foundational R&D or maintenance for critical infrastructure. These belong to other Cardano funding streams such as via a members organization or direct governance action for treasury withdrawal and administrations _e.g. Cardano protocol, Layer 2 solutions_ +- Proposals lacking evidence of a mainnet-deployed product or collaboration with a Tier-1 industry leader. +- Developer tools, middleware, APIs, or integrated development environments belong in Cardano Open: Developers. +- Projects lacking measurable adoption outcomes or Cardano relevance. +- Proposals to use funds primarily for liquidity, loans, NFTs, or buying token-based products without technical integration. +- Informational content, websites, or blogs _e.g. a Cardano education site_. +''', + ), + ], + dos: const [ + '**Provide proof** of established collaborations', + '**Provide proof** of key performance metrics', + '**Clarify** funding commitments', + ], + donts: const [ + '**No** prototype R&D', + '**No** Cardano infrastructure', + '**No** way to prove your impact', + ], + submissionCloseDate: DateTimeExt.now(), + ), + CampaignCategory( + selfRef: f14ConstDocumentsRefs[1].category, + proposalTemplateRef: f14ConstDocumentsRefs[1].proposal, + campaignRef: Campaign.f14Ref, + categoryName: 'Cardano Use Case:', + categorySubname: 'Concept', + description: + '''Cardano Use Cases: Concepts funds novel, early-stage Cardano-based concepts developing proof of concept prototypes through deploying minimum viable products (MVP) to validate innovative products, services, or business models driving Cardano adoption.''', + shortDescription: + 'For early-stage ideas to create, test, and validate Cardano-based prototypes to MVP innovations.', + proposalsCount: 0, + availableFunds: MultiCurrencyAmount.single(Currencies.ada.amount(4000000)), + imageUrl: '', + totalAsk: MultiCurrencyAmount.single(Currencies.ada.amount(0)), + range: ComparableRange( + min: Currencies.ada.amount(15000), + max: Currencies.ada.amount(100000), + ), + currency: Currencies.ada, + descriptions: const [ + CategoryDescription( + title: 'Overview', + description: ''' +Cardano Use Cases: Concepts category fuels disruptive, untested Cardano-based use cases to experiment with novel utility and on-chain transactions. + +The funding category supports early-stage ideas - spanning proof of concept, design research, basic prototyping, and minimum viable products (MVPs) - to lay the foundation for innovative products, services, or business models. + +Unlike _Cardano Use Cases: Partners & Products_, which funds mature, deployed products or enterprise collaborations with proven adoption, this category is for newer Catalyst entrants or innovators validating novel concepts and product market fit with no prior development or funding. +''', + ), + CategoryDescription( + title: 'Who should apply?', + description: ''' +This category is for entrepreneurs, designers, and innovators with original, untested Cardano-based concepts, such as a DeFi startup researching a novel lending protocol, a Web3 developer prototyping a zero-knowledge identity solution, or a researcher exploring AI-blockchain integration. If your project involves a mature product, enterprise collaboration, or incremental feature enhancements, apply to _Cardano Use Cases: Partners & Products_ instead. +''', + ), + CategoryDescription( + title: 'Areas of Interest', + description: ''' +- Disruptive industrial innovations using Cardano’s blockchain and smart contracts +- DeFi solutions with unique approaches _e.g. a peer-to-peer yield and novel bond curve protocol MVP._ +- Privacy, oracles, or zero-knowledge proof applications. +- Projects porting or bridging from other ecosystems _e.g. a wallet prototyping Cardano Native Token support_. +- Prototyping on Cardano scaling solutions like Hydra _e.g. a high-throughput payment prototype_. +- Technical blockchain research with exponential technologies _e.g. a feasibility study for AI-driven oracles_. +''', + ), + CategoryDescription( + title: 'Proposal Guidance', + description: ''' +- Proposals adding features to existing early-stage prototypes must provide evidence of the prototype’s novelty and early status _e.g. URL, demo, developer repository_ +- Proposals must describe the concept’s novelty, explaining how it differs from existing solutions and confirming no prior Catalyst funding or development. +- Demonstrate how the project could drive Cardano adoption through increased on-chain transactions and you intend to validate potential. +- Projects may be proprietary or open source but must clearly benefit the Cardano ecosystem. +- New concepts must be original and not previously developed. +- Mature products with enterprise partnerships, and enhancements for deployed solutions should apply to _Cardano Use Cases: Partners & Products._ +- Proposals must have a delivery timeline of no more than 12 months. +''', + ), + CategoryDescription( + title: 'Eligibility Criteria', + description: ''' +The following will **not** be funded: + +- Proposals for mature products, incremental improvements, or enterprise partnerships _e.g. scaling an existing DeFi app or adding features to a deployed wallet belong in Cardano Use Cases: Partners & Products_. +- Proposals focused on informational content only, websites, or blogs _e.g. a Cardano education site belong instead in Cardano Open: Ecosystem._ +- Infrastructure or tooling projects not tied to specific Cardano use cases belong in either Cardano Open: Developers or requesting funds from another Cardano funding stream _e.g. members organization or DAO_. +- Proposals lacking a clear link to Cardano or potential to generate transactions on chain. +''', + ), + ], + dos: const [ + '**Propose** fresh ideas that haven’t been funded before', + '**Produce** a working on-chain prototype or MVP', + '**Provide** realistic blockchain usage projections', + ], + donts: const [ + '**No** existing Cardano products', + '**No** info-only websites and content', + '**No** moon metrics!', + ], + submissionCloseDate: DateTimeExt.now(), + ), + CampaignCategory( + selfRef: f14ConstDocumentsRefs[2].category, + proposalTemplateRef: f14ConstDocumentsRefs[2].proposal, + campaignRef: Campaign.f14Ref, + categoryName: 'Cardano Open:', + categorySubname: 'Developers', + description: ''' +Funds open source tools and environments to enhance the Cardano developer experience. Apply to create impactful, community-driven open source solutions! +''', + shortDescription: + 'For developers to build open-source tools that enhance the Cardano developer experience.', + proposalsCount: 0, + availableFunds: MultiCurrencyAmount.single(Currencies.ada.amount(3100000)), + imageUrl: '', + totalAsk: MultiCurrencyAmount.single(Currencies.ada.amount(0)), + range: ComparableRange( + min: Currencies.ada.amount(15000), + max: Currencies.ada.amount(100000), + ), + currency: Currencies.ada, + descriptions: const [ + CategoryDescription( + title: 'Overview', + description: ''' +Cardano Open: Developers will fund devs, programmers, and engineers creating or contributing to open source technologies that strengthen the Cardano developer ecosystem. We believe in open source software, hardware, and data solutions driving transparency, security, and collaboration for the good of the network. + +The goal is to streamline integrated development environments, boost coding efficiency, and simplify the developer experience on Cardano. + +Learn more about open source at [opensource.org](https://opensource.org/). +''', + ), + CategoryDescription( + title: 'Who should apply?', + description: ''' +This category is for software developers, blockchain engineers, data architects, and open source contributors eager to build O/S tools, libraries, or environments that improve the Cardano developer ecosystem. + +The scope is wide open for creating smart contract debugging tools, crafting SDKs support new languages, or DevOps specialists designing interoperability CLIs. If your proposal focuses on non-technical initiatives like education or marketing, apply to _Cardano Open: Ecosystem_ instead. +''', + ), + CategoryDescription( + title: 'Areas of interest', + description: ''' +Proposals may focus on open source technical solutions, for example: + +- Standardizing, developing, or supporting full-stack solutions and integrated development environments _e.g. new language and code extension support for Plutus debugging_. +- Creating libraries, SDKs, APIs, toolchains, or frameworks _e.g., new SDKs for Cardano privacy preserving smart contracts_. +- Developing governance and blockchain interoperability tooling _e.g. a CLI tool for cross-chain transaction validation_ +''', + ), + CategoryDescription( + title: 'Proposal Guidance', + description: ''' +- Proposals must be source-available from day one with a declared open source repository e.g. GitHub, GitLab. +- Select an OSI-approved open source license e.g. MIT, Apache 2.0, GPLv3) that permits the community to freely copy, inspect, and modify the project’s output. Visit [choosealicense.com](https://choosealicense.com/) for guidance. +- Ensure thorough documentation is available from day one and updated regularly. +- Describe how the project will benefit the Cardano developer community and foster collaboration e.g. through tutorials, webinars, or forum posts. +- Proposals must have a delivery timeline of no more than 12 months. +''', + ), + CategoryDescription( + title: 'Eligibility Criteria', + description: ''' +The following will **not** be funded: + +- Proposals not primarily developing new technology or contributing to existing open source technology _e.g. learning guides for Plutus or Aiken, these would belong in the Cardano Open: Ecosystem category_. +- Proposals producing proprietary software/hardware or open sourcing only a portion of the codebase. +- Proposals lacking an OSI-approved open source license, public repository, or high-quality documentation from day one. +- Proposals not directly enhancing the Cardano developer experience _e.g. general blockchain tools without Cardano-specific integration._ +''', + ), + ], + dos: const [ + '**Open source** licensing of project outputs is required', + '**Clear statement** of your open source license', + '**Open source outputs** from project start', + '**Provide** high quality documentation', + ], + donts: const [ + '**No** proprietary materials (including software or hardware)', + '**No** projects that are non-technical', + '**No** info-only websites and content', + '**Forget to be open**, public, and available on Day 1', + ], + submissionCloseDate: DateTimeExt.now(), + ), + CampaignCategory( + selfRef: f14ConstDocumentsRefs[3].category, + proposalTemplateRef: f14ConstDocumentsRefs[3].proposal, + campaignRef: Campaign.f14Ref, + categoryName: 'Cardano Open:', + categorySubname: 'Ecosystem', + description: ''' +Funds non-technical initiatives like marketing, education, and community building to grow Cardano’s ecosystem and onboard new users globally. +''', + shortDescription: + 'For non-tech projects like marketing, education, or community growth to expand Cardano’s reach.', + proposalsCount: 0, + availableFunds: MultiCurrencyAmount.single(Currencies.ada.amount(3000000)), + imageUrl: '', + totalAsk: MultiCurrencyAmount.single(Currencies.ada.amount(0)), + range: ComparableRange( + min: Currencies.ada.amount(15000), + max: Currencies.ada.amount(60000), + ), + currency: Currencies.ada, + descriptions: const [ + CategoryDescription( + title: 'Overview', + description: ''' +Cardano Open: Ecosystem will fund non-technical initiatives that drive grassroots ecosystem growth, education, and community engagement to broaden Cardano’s reach and onboard new users. + +Cardano’s passionate global community is a foundation for growth, yet more can be done to expand adoption. This category focuses on building the creative capacity to deliver local campaigns and beyond that bring awareness and attention to Cardano from the hearts and minds of the grassroots community collective. + +Learn more at [](https://cardano.org/)[cardano.org](http://cardano.org). +''', + ), + CategoryDescription( + title: 'Who should apply?', + description: ''' +This category is for educators, marketers, content creators, and community leaders passionate about expanding Cardano’s global reach through non-technical initiatives, such as marketing experts launching social media campaigns, educators developing blockchain courses, or community organizers hosting hackathons. If your proposal focuses on software or hardware development, apply to Cardano Use Cases or Cardano Open: Developers instead. +''', + ), + CategoryDescription( + title: 'Areas of interest', + description: ''' +Proposals may focus on non-technical initiatives, such as: + +- Cardano Community Hubs that create or maintain a local Cardano presence in cities or regions worldwide +- Growth-focused marketing campaigns and public outreach _e.g. a social media campaign to promote Cardano adoption._ +- Community hackathons or builder outreach events _e.g. a virtual workshop for new developers._ +- Content creation or curation _e.g. educational videos on Cardano’s features_. +- Mentorship, incubation, or collaboration programs _e.g. a startup accelerator for Cardano-based projects._ +- Educational advancements _e.g. a university course on blockchain with Cardano case studies._ +- Local policy, standards, or regulation advocacy _e.g. a policy paper on Cardano’s role in regional finance and pathways to present to local authorities_ +''', + ), + CategoryDescription( + title: 'Proposal Guidance', + description: ''' +- Proposals must focus on non-technical initiatives like marketing, education, or community building, not software/hardware development. +- Clearly articulate how the initiative will expand Cardano’s global footprint or onboard new users, using measurable Key Performance Indicators (KPIs) and relevant metrics _e.g. event attendance, content reach, new user registrations_. +- Ensure activities are inclusive and accessible to both existing and new Cardano communities. +- Proposals must have a delivery timeline of no more than 12 months. +- Publicly accessible outputs where applicable _e.g. educational content, event reports, or marketing materials._ +- A timeline with clear milestones _e.g. campaign launch, event dates, content release._ +- _Optional_: Plans for sustained community engagement, such as follow-up events, social media campaigns, or mentorship programs. +''', + ), + CategoryDescription( + title: 'Eligibility Criteria', + description: ''' +The following will **not** be funded: + +- Proposals not primarily focused on grassroots ecosystem growth through marketing, education, or community building. +- Proposals centered on software or hardware development _e.g. a fan loyalty platform or marketing automation tool belong in Cardano Use Cases category_. +- Proposals lacking clear goals, measurable outcomes, KPIs, or relevance to Cardano’s ecosystem. +''', + ), + ], + dos: const [ + '**Deliver** Community engagement projects', + '**Deliver** grassroots growth, education projects', + '**Clearly outline** goals, activities, and measurable outcomes', + '**Design** for new Cardano communities', + ], + donts: const [ + '**No** software or hardware development', + '**No** vague or irrelevant value to Cardano', + '**No** clear targets or KPIs', + ], + submissionCloseDate: DateTimeExt.now(), + ), +]; diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/constant/f14_static_campaign_timeline.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/constant/f14_static_campaign_timeline.dart new file mode 100644 index 000000000000..a66aa89942d0 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/constant/f14_static_campaign_timeline.dart @@ -0,0 +1,75 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; + +final f14StaticCampaignTimeline = CampaignTimeline( + phases: [ + CampaignPhase( + title: 'Proposal Submission', + description: + '''Proposers submit initial ideas to solve challenges. Each proposal includes the problem, solution, requested ADA budget, and a clear implementation plan.''', + timeline: DateRange( + from: DateTime.utc(2025, 07, 25, 10), + to: DateTime.utc(2025, 12, 30, 18), + ), + type: CampaignPhaseType.proposalSubmission, + ), + CampaignPhase( + title: 'Voting Registration', + description: + 'During Voter registration, ADA holders register via supported wallet to participate in the Voting.', + timeline: DateRange( + from: DateTime.utc(2025, 07, 05, 18), + to: DateTime.utc(2025, 07, 12, 10), + ), + type: CampaignPhaseType.votingRegistration, + ), + CampaignPhase( + title: 'Community Review', + description: + '''Community members help improve proposals through two key steps: LV0 and LV1 reviewers assess the proposals, then LV2 moderators oversee the process to ensure quality and fairness.''', + timeline: DateRange( + from: DateTime.utc(2025, 07, 03, 8), + to: DateTime.utc(2025, 07, 08, 20), + ), + type: CampaignPhaseType.communityReview, + ), + CampaignPhase( + title: 'Reviewers and Moderators registration', + description: '', + timeline: DateRange( + from: DateTime.utc(2025, 07, 03, 8), + to: DateTime.utc(2025, 07, 04, 20), + ), + type: CampaignPhaseType.reviewRegistration, + ), + CampaignPhase( + title: 'Community Voting', + description: '''Community members cast their votes using the Catalyst Voting app.''', + timeline: DateRange( + from: DateTime.utc(2025, 8, 22, 12), + to: DateTime.utc(2025, 12, 30, 18), + ), + type: CampaignPhaseType.communityVoting, + ), + CampaignPhase( + title: 'Voting Results', + description: + '''Votes are tallied and the results are announced. Rewards are distributed to both voters and community reviewers.''', + timeline: DateRange( + from: DateTime.utc(2025, 07, 18, 9), + to: DateTime.utc(2025, 07, 21, 2), + ), + type: CampaignPhaseType.votingResults, + ), + CampaignPhase( + title: 'Project Onboarding', + description: + '''This phase involves finalizing the key milestones submitted in the Catalyst App during the proposal submission type within the Catalyst Milestone Module. It also includes conducting formal due diligence, and fulfilling all required onboarding steps to become eligible for funding.''', + timeline: DateRange( + from: DateTime.utc(2025, 07, 25, 06), + to: DateTime.utc(2025, 07, 30, 06), + ), + type: CampaignPhaseType.projectOnboarding, + ), + ], +); diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/constant/f15_static_campaign_categories.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/constant/f15_static_campaign_categories.dart new file mode 100644 index 000000000000..210f89ea63ea --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/constant/f15_static_campaign_categories.dart @@ -0,0 +1,8 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; + +/// List of static [CampaignCategory] definitions. +/// +/// Categories will come from documents later. +/// +/// See [CampaignCategory]. +final f15StaticCampaignCategories = []; diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/constant/f15_static_campaign_timeline.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/constant/f15_static_campaign_timeline.dart new file mode 100644 index 000000000000..12d05dda8212 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/constant/f15_static_campaign_timeline.dart @@ -0,0 +1,75 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; + +final f15StaticCampaignTimeline = CampaignTimeline( + phases: [ + CampaignPhase( + title: 'Proposal Submission', + description: + '''Proposers submit initial ideas to solve challenges. Each proposal includes the problem, solution, requested ADA budget, and a clear implementation plan.''', + timeline: DateRange( + from: DateTime.utc(2025, 09, 23, 8), + to: DateTime.utc(2025, 12, 30, 18), + ), + type: CampaignPhaseType.proposalSubmission, + ), + CampaignPhase( + title: 'Voting Registration', + description: + 'During Voter registration, ADA holders register via supported wallet to participate in the Voting.', + timeline: DateRange( + from: DateTime.utc(2025, 07, 05, 18), + to: DateTime.utc(2025, 07, 12, 10), + ), + type: CampaignPhaseType.votingRegistration, + ), + CampaignPhase( + title: 'Community Review', + description: + '''Community members help improve proposals through two key steps: LV0 and LV1 reviewers assess the proposals, then LV2 moderators oversee the process to ensure quality and fairness.''', + timeline: DateRange( + from: DateTime.utc(2025, 07, 03, 8), + to: DateTime.utc(2025, 07, 08, 20), + ), + type: CampaignPhaseType.communityReview, + ), + CampaignPhase( + title: 'Reviewers and Moderators registration', + description: '', + timeline: DateRange( + from: DateTime.utc(2025, 07, 03, 8), + to: DateTime.utc(2025, 07, 04, 20), + ), + type: CampaignPhaseType.reviewRegistration, + ), + CampaignPhase( + title: 'Community Voting', + description: '''Community members cast their votes using the Catalyst Voting app.''', + timeline: DateRange( + from: DateTime.utc(2025, 8, 22, 12), + to: DateTime.utc(2025, 12, 30, 18), + ), + type: CampaignPhaseType.communityVoting, + ), + CampaignPhase( + title: 'Voting Results', + description: + '''Votes are tallied and the results are announced. Rewards are distributed to both voters and community reviewers.''', + timeline: DateRange( + from: DateTime.utc(2025, 07, 18, 9), + to: DateTime.utc(2025, 07, 21, 2), + ), + type: CampaignPhaseType.votingResults, + ), + CampaignPhase( + title: 'Project Onboarding', + description: + '''This phase involves finalizing the key milestones submitted in the Catalyst App during the proposal submission type within the Catalyst Milestone Module. It also includes conducting formal due diligence, and fulfilling all required onboarding steps to become eligible for funding.''', + timeline: DateRange( + from: DateTime.utc(2025, 07, 25, 06), + to: DateTime.utc(2025, 07, 30, 06), + ), + type: CampaignPhaseType.projectOnboarding, + ), + ], +); diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/catalyst_voices_models.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/catalyst_voices_models.dart index a77791cd3ece..54571ee58425 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/catalyst_voices_models.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/catalyst_voices_models.dart @@ -2,12 +2,14 @@ library catalyst_voices_models; export 'api/api_response_status_code.dart'; export 'api/exception/api_exception.dart'; +export 'app/app_meta.dart'; export 'auth/password_strength.dart'; export 'auth/rbac_token.dart'; export 'auth/seed_phrase.dart'; export 'blockchain/blockchain_slot_config.dart'; export 'campaign/campaign.dart'; export 'campaign/campaign_category.dart'; +export 'campaign/campaign_filters.dart'; export 'campaign/campaign_phase.dart'; export 'campaign/campaign_timeline.dart'; export 'common/hi_lo/hi_lo.dart'; @@ -30,7 +32,11 @@ export 'crypto/lock_factor.dart'; export 'dev_tools/dev_tools_config.dart'; export 'document/builder/document_builder.dart'; export 'document/builder/document_change.dart'; -export 'document/constant_documents_refs.dart'; +export 'document/constant/active_const_documents_refs.dart'; +export 'document/constant/all_const_documents_refs.dart'; +export 'document/constant/constant_documents_refs.dart'; +export 'document/constant/f14_const_documents_refs.dart'; +export 'document/constant/f15_const_documents_refs.dart'; export 'document/data/document_data.dart'; export 'document/data/document_data_content.dart'; export 'document/data/document_data_metadata.dart'; diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/constant/active_const_documents_refs.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/constant/active_const_documents_refs.dart new file mode 100644 index 000000000000..be641cfb2969 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/constant/active_const_documents_refs.dart @@ -0,0 +1,4 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; + +const activeCampaignRef = Campaign.f15Ref; +const List activeConstantDocumentRefs = f15ConstDocumentsRefs; diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/constant/all_const_documents_refs.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/constant/all_const_documents_refs.dart new file mode 100644 index 000000000000..7cda9f14b9d1 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/constant/all_const_documents_refs.dart @@ -0,0 +1,6 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; + +final List allConstantDocumentRefs = [ + ...f14ConstDocumentsRefs, + ...f15ConstDocumentsRefs, +]; diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/constant/constant_documents_refs.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/constant/constant_documents_refs.dart new file mode 100644 index 000000000000..ad8515de7dda --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/constant/constant_documents_refs.dart @@ -0,0 +1,31 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:equatable/equatable.dart'; + +/// Groups related [proposal] and [comment] templates to given [category]. +final class CategoryTemplatesRefs extends Equatable { + final SignedDocumentRef category; + final SignedDocumentRef proposal; + final SignedDocumentRef comment; + + const CategoryTemplatesRefs({ + required this.category, + required this.proposal, + required this.comment, + }); + + Iterable get all => [category, proposal, comment]; + + Iterable get allTyped { + return [ + TypedDocumentRef( + ref: category, + type: DocumentType.categoryParametersDocument, + ), + TypedDocumentRef(ref: proposal, type: DocumentType.proposalTemplate), + TypedDocumentRef(ref: comment, type: DocumentType.commentTemplate), + ]; + } + + @override + List get props => [category, proposal, comment]; +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/constant_documents_refs.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/constant/f14_const_documents_refs.dart similarity index 79% rename from catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/constant_documents_refs.dart rename to catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/constant/f14_const_documents_refs.dart index 90e621810be6..54b2d980fe3d 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/constant_documents_refs.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/constant/f14_const_documents_refs.dart @@ -1,12 +1,11 @@ import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:equatable/equatable.dart'; /// At the moment categories are hard coded. /// See list in link below /// https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/catalyst_docs/proposal/#fund-14-defined-category-ids /// https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/catalyst_docs/proposal/#fund-14-defined-templates-ids /// https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/catalyst_docs/comment/#fund-14-defined-templates-ids -const constantDocumentsRefs = [ +const f14ConstDocumentsRefs = [ CategoryTemplatesRefs( category: SignedDocumentRef.first('0194d490-30bf-7473-81c8-a0eaef369619'), proposal: SignedDocumentRef.first('0194d492-1daa-75b5-b4a4-5cf331cd8d1a'), @@ -68,32 +67,3 @@ const constantDocumentsRefs = [ comment: SignedDocumentRef.first('0194d494-4402-7aa5-9dbc-5fe886e60ebc'), ), ]; - -/// Groups related [proposal] and [comment] templates to given [category]. -final class CategoryTemplatesRefs extends Equatable { - final SignedDocumentRef category; - final SignedDocumentRef proposal; - final SignedDocumentRef comment; - - const CategoryTemplatesRefs({ - required this.category, - required this.proposal, - required this.comment, - }); - - Iterable get all => [category, proposal, comment]; - - Iterable get allTyped { - return [ - TypedDocumentRef( - ref: category, - type: DocumentType.categoryParametersDocument, - ), - TypedDocumentRef(ref: proposal, type: DocumentType.proposalTemplate), - TypedDocumentRef(ref: comment, type: DocumentType.commentTemplate), - ]; - } - - @override - List get props => [category, proposal, comment]; -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/constant/f15_const_documents_refs.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/constant/f15_const_documents_refs.dart new file mode 100644 index 000000000000..22ac231688bc --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/constant/f15_const_documents_refs.dart @@ -0,0 +1,3 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; + +const f15ConstDocumentsRefs = []; diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/specialized/proposal_template.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/specialized/proposal_template.dart index 4453a697d072..60bde983410f 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/specialized/proposal_template.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/specialized/proposal_template.dart @@ -11,6 +11,11 @@ final class ProposalTemplate extends Equatable { required this.schema, }); + DocumentStringSchema? get title { + final property = schema.getPropertySchema(ProposalDocument.titleNodeId); + return property is DocumentStringSchema ? property : null; + } + @override List get props => [ metadata, diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/money/currency.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/money/currency.dart index b46682a4e967..e8b8c51ac605 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/money/currency.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/money/currency.dart @@ -1,4 +1,4 @@ -import 'package:catalyst_voices_models/src/money/currencies.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:catalyst_voices_models/src/money/currency_code.dart'; import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; import 'package:collection/collection.dart'; @@ -58,6 +58,43 @@ final class Currency extends Equatable { decimalPattern, ]; + /// Syntax sugar for [amountBig]. + Money amount( + int value, { + MoneyUnits moneyUnits = MoneyUnits.majorUnits, + }) { + return amountBig( + BigInt.from(value), + moneyUnits: moneyUnits, + ); + } + + /// Creates [Money] instance with this [Currency]. + /// + /// Because [Money] stores values always in minor units + /// you have to tell if [value] is passed in minor or major + /// units. + /// + /// Units: + /// Ada == major unit + /// lovelaces == minor unit. + /// + /// Examples + /// Currencies.ada.amount(1) + /// Currencies.ada.amount(1000000, moneyUnits: MoneyUnits.minorUnits) + /// + /// Gives same amount of money. + Money amountBig( + BigInt value, { + MoneyUnits moneyUnits = MoneyUnits.majorUnits, + }) { + return Money.fromUnits( + currency: this, + amount: value, + moneyUnits: moneyUnits, + ); + } + /// Formats [minorUnits] into a string using [defaultPattern]. /// /// Example (USD): `12345` → `123.45` diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposals/proposals_count_filters.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposals/proposals_count_filters.dart index 758bb7c878a4..5e7bc2827696 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposals/proposals_count_filters.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposals/proposals_count_filters.dart @@ -7,6 +7,7 @@ final class ProposalsCountFilters extends Equatable { final SignedDocumentRef? category; final String? searchQuery; final Duration? maxAge; + final CampaignFilters? campaign; const ProposalsCountFilters({ this.author, @@ -14,6 +15,7 @@ final class ProposalsCountFilters extends Equatable { this.category, this.searchQuery, this.maxAge, + this.campaign, }); @override @@ -23,5 +25,6 @@ final class ProposalsCountFilters extends Equatable { category, searchQuery, maxAge, + campaign, ]; } diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposals/proposals_filters.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposals/proposals_filters.dart index d1f2c06b6f21..9eded976ca98 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposals/proposals_filters.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposals/proposals_filters.dart @@ -8,6 +8,7 @@ final class ProposalsFilters extends Equatable { final SignedDocumentRef? category; final String? searchQuery; final Duration? maxAge; + final CampaignFilters? campaign; const ProposalsFilters({ this.type = ProposalsFilterType.total, @@ -16,8 +17,18 @@ final class ProposalsFilters extends Equatable { this.category, this.searchQuery, this.maxAge, + this.campaign, }); + ProposalsFilters.forActiveCampaign({ + this.type = ProposalsFilterType.total, + this.author, + this.onlyAuthor, + this.category, + this.searchQuery, + this.maxAge, + }) : campaign = CampaignFilters.active(); + @override List get props => [ type, @@ -26,6 +37,7 @@ final class ProposalsFilters extends Equatable { category, searchQuery, maxAge, + campaign, ]; ProposalsFilters copyWith({ @@ -35,6 +47,7 @@ final class ProposalsFilters extends Equatable { Optional? category, Optional? searchQuery, Optional? maxAge, + Optional? campaign, }) { return ProposalsFilters( type: type ?? this.type, @@ -43,6 +56,7 @@ final class ProposalsFilters extends Equatable { category: category.dataOr(this.category), searchQuery: searchQuery.dataOr(this.searchQuery), maxAge: maxAge.dataOr(this.maxAge), + campaign: campaign.dataOr(this.campaign), ); } @@ -53,6 +67,7 @@ final class ProposalsFilters extends Equatable { category: category, searchQuery: searchQuery, maxAge: maxAge, + campaign: campaign, ); } @@ -64,7 +79,8 @@ final class ProposalsFilters extends Equatable { 'onlyAuthor[$onlyAuthor], ' 'category[$category], ' 'searchQuery[$searchQuery], ' - 'maxAge[$maxAge]' + 'maxAge[$maxAge], ' + 'campaign[$campaign]' ')'; } diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/test/money/currency_test.dart b/catalyst_voices/packages/internal/catalyst_voices_models/test/money/currency_test.dart new file mode 100644 index 000000000000..32e910b683d5 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_models/test/money/currency_test.dart @@ -0,0 +1,22 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:test/test.dart'; + +void main() { + group(Currency, () { + group('amount', () { + test('1 ada equals 1000000 lovelaces', () { + // Given + const ada = 1; + const lovelaces = 1000000; + const currency = Currencies.ada; + + // When + final adaMoney = currency.amount(ada); + final lovelaceMoney = currency.amount(lovelaces, moneyUnits: MoneyUnits.minorUnits); + + // Then + expect(adaMoney, lovelaceMoney); + }); + }); + }); +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/campaign/campaign_repository.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/campaign/campaign_repository.dart index f37994b18dac..540d26d46fa2 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/campaign/campaign_repository.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/campaign/campaign_repository.dart @@ -19,11 +19,19 @@ final class CampaignRepositoryImpl implements CampaignRepository { Future getCampaign({ required String id, }) async { - return Campaign.f14(); + if (id == Campaign.f15Ref.id) { + return Campaign.f15(); + } + if (id == Campaign.f14Ref.id) { + return Campaign.f14(); + } + throw NotFoundException(message: 'Campaign $id not found'); } @override Future getCategory(SignedDocumentRef ref) async { - return staticCampaignCategories.firstWhereOrNull((e) => e.selfRef.id == ref.id); + return Campaign.all + .expand((element) => element.categories) + .firstWhereOrNull((e) => e.selfRef.id == ref.id); } } diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/catalyst_voices_repositories.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/catalyst_voices_repositories.dart index aba900410c5d..9945b7306bd8 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/catalyst_voices_repositories.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/catalyst_voices_repositories.dart @@ -25,6 +25,7 @@ export 'proposal/proposal_repository.dart' show ProposalRepository; export 'signed_document/signed_document_manager.dart' show SignedDocumentManager; export 'signed_document/signed_document_manager_impl.dart' show SignedDocumentManagerImpl; export 'storage/vault/secure_storage_vault.dart'; +export 'sync/app_meta_storage.dart'; export 'sync/sync_stats_storage.dart'; export 'user/keychain/keychain_provider.dart' show KeychainProvider; export 'user/keychain/keychain_signer.dart' show KeychainSigner; diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/dao/documents_dao.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/dao/documents_dao.dart index bd37729b7181..2d936c72b3ca 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/dao/documents_dao.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/dao/documents_dao.dart @@ -5,6 +5,7 @@ import 'package:catalyst_voices_repositories/src/database/query/jsonb_expression import 'package:catalyst_voices_repositories/src/database/table/documents.dart'; import 'package:catalyst_voices_repositories/src/database/table/documents.drift.dart'; import 'package:catalyst_voices_repositories/src/database/table/documents_metadata.dart'; +import 'package:catalyst_voices_repositories/src/database/table/drafts.dart'; import 'package:catalyst_voices_repositories/src/database/typedefs.dart'; import 'package:collection/collection.dart'; import 'package:drift/drift.dart'; @@ -40,7 +41,11 @@ abstract interface class DocumentsDao { }); /// Deletes all documents. Cascades to metadata. - Future deleteAll(); + /// + /// If [keepTemplatesForLocalDrafts] is true keeps templates referred by local drafts. + Future deleteAll({ + bool keepTemplatesForLocalDrafts, + }); /// If version is specified in [ref] returns this version or null. /// Returns newest version with matching id or null of none found. @@ -110,6 +115,7 @@ abstract interface class DocumentsDao { tables: [ Documents, DocumentsMetadata, + Drafts, ], ) class DriftDocumentsDao extends DatabaseAccessor @@ -174,8 +180,28 @@ class DriftDocumentsDao extends DatabaseAccessor } @override - Future deleteAll() async { - final deletedRows = await delete(documents).go(); + Future deleteAll({ + bool keepTemplatesForLocalDrafts = false, + }) async { + final query = delete(documents); + + if (keepTemplatesForLocalDrafts) { + final templateId = drafts.metadata.jsonExtract(r'$.template.id'); + + query.where((documents) { + return notExistsQuery( + selectOnly(drafts, distinct: true) + ..addColumns([ + templateId, + ]) + ..where( + documents.metadata.jsonExtract(r'$.selfRef.id').equalsExp(templateId), + ), + ); + }); + } + + final deletedRows = await query.go(); if (kDebugMode) { debugPrint('DocumentsDao: Deleted[$deletedRows] rows'); diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/dao/proposals_dao.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/dao/proposals_dao.dart index 3cc0134ed6ad..64275ad1558b 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/dao/proposals_dao.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/dao/proposals_dao.dart @@ -16,8 +16,13 @@ import 'package:collection/collection.dart'; import 'package:drift/drift.dart'; import 'package:drift/extensions/json1.dart'; import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; import 'package:rxdart/rxdart.dart'; +/// Exposes only public operation on proposals, and related tables. +/// This is a wrapper around [DocumentsDao] and [DraftsDao] to provide a single interface for proposals. +/// Since proposals are composed of multiple documents (template, action, comments, etc.) we need to +/// join multiple tables to get all the information about a proposal, which make sense to create this specialized dao. @DriftAccessor( tables: [ Documents, @@ -25,10 +30,6 @@ import 'package:rxdart/rxdart.dart'; DocumentsFavorites, ], ) -/// Exposes only public operation on proposals, and related tables. -/// This is a wrapper around [DocumentsDao] and [DraftsDao] to provide a single interface for proposals. -/// Since proposals are composed of multiple documents (template, action, comments, etc.) we need to -/// join multiple tables to get all the information about a proposal, which make sense to create this specialized dao. class DriftProposalsDao extends DatabaseAccessor with $DriftProposalsDaoMixin implements ProposalsDao { @@ -36,11 +37,25 @@ class DriftProposalsDao extends DatabaseAccessor // TODO(dt-iohk): it seems that this method doesn't correctly filter by ProposalsFilterType.my // since it does not check for author, consider to use another type which doesn't have "my" case. + + // TODO(damian-molinski): filters is only used for campaign and type. @override Future> queryProposals({ SignedDocumentRef? categoryRef, - required ProposalsFilterType type, + required ProposalsFilters filters, }) async { + if ([ + filters.author, + filters.onlyAuthor, + filters.category, + filters.searchQuery, + filters.maxAge, + ].nonNulls.isNotEmpty) { + if (kDebugMode) { + print('queryProposals supports only campaign and type filters'); + } + } + final latestProposalRef = alias(documents, 'latestProposalRef'); final proposal = alias(documents, 'proposal'); @@ -72,7 +87,11 @@ class DriftProposalsDao extends DatabaseAccessor Expression.and([ proposal.type.equalsValue(DocumentType.proposalDocument), proposal.metadata.jsonExtract(r'$.template').isNotNull(), - proposal.metadata.jsonExtract(r'$.categoryId').isNotNull(), + proposal.metadata.jsonExtract(r'$.categoryId.id').isNotNull(), + if (filters.campaign != null) + proposal.metadata + .jsonExtract(r'$.categoryId.id') + .isIn(filters.campaign!.categoriesIds), ]), ) ..orderBy([OrderingTerm.asc(proposal.verHi)]); @@ -81,7 +100,7 @@ class DriftProposalsDao extends DatabaseAccessor mainQuery.where(proposal.metadata.isCategory(categoryRef)); } - final ids = await _getFilterTypeIds(type); + final ids = await _getFilterTypeIds(filters.type); final include = ids.include; if (include != null) { @@ -158,6 +177,10 @@ class DriftProposalsDao extends DatabaseAccessor // Safe check for invalid proposals proposal.metadata.jsonExtract(r'$.template').isNotNull(), proposal.metadata.jsonExtract(r'$.categoryId').isNotNull(), + if (filters.campaign != null) + proposal.metadata + .jsonExtract(r'$.categoryId.id') + .isIn(filters.campaign!.categoriesIds), ]), ) ..orderBy(order.terms(proposal)) @@ -514,6 +537,10 @@ class DriftProposalsDao extends DatabaseAccessor // Safe check for invalid proposals documents.metadata.jsonExtract(r'$.template').isNotNull(), documents.metadata.jsonExtract(r'$.categoryId').isNotNull(), + if (filters?.campaign != null) + documents.metadata + .jsonExtract(r'$.categoryId.id') + .isIn(filters!.campaign!.categoriesIds), ]), ) ..orderBy([OrderingTerm.desc(documents.verHi)]) @@ -714,7 +741,7 @@ class DriftProposalsDao extends DatabaseAccessor abstract interface class ProposalsDao { Future> queryProposals({ SignedDocumentRef? categoryRef, - required ProposalsFilterType type, + required ProposalsFilters filters, }); Future> queryProposalsPage({ diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/document_repository.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/document_repository.dart index e90789bdeced..018e491af6fe 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/document_repository.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/document_repository.dart @@ -54,7 +54,9 @@ abstract interface class DocumentRepository { /// Returns list of refs to all published and any refs it may hold. /// /// Its using documents index api. - Future> getAllDocumentsRefs(); + Future> getAllDocumentsRefs({ + required Campaign campaign, + }); /// Return list of all cached documents id for given [id]. /// It looks for documents in the local storage and draft storage. @@ -128,7 +130,11 @@ abstract interface class DocumentRepository { /// Removes all locally stored documents. /// /// Returns number of deleted rows. - Future removeAll(); + /// + /// if [keepLocalDrafts] is true local drafts will be kept and related templates. + Future removeAll({ + bool keepLocalDrafts, + }); /// Saves a pre-parsed and validated document to storage. /// @@ -263,9 +269,11 @@ final class DocumentRepositoryImpl implements DocumentRepository { } @override - Future> getAllDocumentsRefs() async { - final allRefs = await _remoteDocuments.index().then(_uniqueTypedRefs); - final allConstRefs = constantDocumentsRefs.expand((element) => element.all); + Future> getAllDocumentsRefs({ + required Campaign campaign, + }) async { + final allRefs = await _remoteDocuments.index(campaign: campaign).then(_uniqueTypedRefs); + final allConstRefs = allConstantDocumentRefs.expand((element) => element.all); final nonConstRefs = allRefs .where((ref) => allConstRefs.none((e) => e.id == ref.ref.id)) @@ -286,7 +294,7 @@ final class DocumentRepositoryImpl implements DocumentRepository { final uniqueRefs = { // Note. categories are mocked on backend so we can't not fetch them. - ...constantDocumentsRefs.expand( + ...activeConstantDocumentRefs.expand( (element) => element.allTyped.where((e) => !e.type.isCategory), ), ...allLatestRefs, @@ -396,9 +404,13 @@ final class DocumentRepositoryImpl implements DocumentRepository { } @override - Future removeAll() async { - final deletedDrafts = await _drafts.deleteAll(); - final deletedDocuments = await _localDocuments.deleteAll(); + Future removeAll({ + bool keepLocalDrafts = false, + }) async { + final deletedDrafts = keepLocalDrafts ? 0 : await _drafts.deleteAll(); + final deletedDocuments = keepLocalDrafts + ? await _localDocuments.deleteAllRespectingLocalDrafts() + : await _localDocuments.deleteAll(); return deletedDrafts + deletedDocuments; } diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/database_documents_data_source.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/database_documents_data_source.dart index 8900cf3a2242..4d64c93f8014 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/database_documents_data_source.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/database_documents_data_source.dart @@ -11,7 +11,14 @@ final class DatabaseDocumentsDataSource ); @override - Future deleteAll() => _database.documentsDao.deleteAll(); + Future deleteAll() { + return _database.documentsDao.deleteAll(); + } + + @override + Future deleteAllRespectingLocalDrafts() { + return _database.documentsDao.deleteAll(keepTemplatesForLocalDrafts: true); + } @override Future exists({required DocumentRef ref}) { @@ -50,7 +57,10 @@ final class DatabaseDocumentsDataSource required ProposalsFilterType type, }) { return _database.proposalsDao - .queryProposals(categoryRef: categoryRef, type: type) + .queryProposals( + categoryRef: categoryRef, + filters: ProposalsFilters.forActiveCampaign(type: type), + ) .then((value) => value.map((e) => e.toModel()).toList()); } diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/document_data_local_source.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/document_data_local_source.dart index ea21afda429d..9080c261f433 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/document_data_local_source.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/document_data_local_source.dart @@ -13,6 +13,8 @@ abstract interface class DocumentDataLocalSource implements DocumentDataSource { CatalystId? authorId, }); + Future> index(); + Future> queryVersionsOfId({required String id}); Future save({required DocumentData data}); @@ -43,6 +45,8 @@ abstract interface class DraftDataSource implements DocumentDataLocalSource { /// See [DatabaseDocumentsDataSource]. abstract interface class SignedDocumentDataSource implements DocumentDataLocalSource { + Future deleteAllRespectingLocalDrafts(); + Future getRefCount({ required DocumentRef ref, required DocumentType type, diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/document_data_remote_source.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/document_data_remote_source.dart index 32c5d564ce00..7d2445c0e648 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/document_data_remote_source.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/document_data_remote_source.dart @@ -36,7 +36,7 @@ final class CatGatewayDocumentDataSource implements DocumentDataRemoteSource { @override Future getLatestVersion(String id) async { - final constVersion = constantDocumentsRefs + final constVersion = allConstantDocumentRefs .expand((element) => element.all) .firstWhereOrNull((element) => element.id == id) ?.version; @@ -73,7 +73,9 @@ final class CatGatewayDocumentDataSource implements DocumentDataRemoteSource { } @override - Future> index() async { + Future> index({ + required Campaign campaign, + }) async { final allRefs = {}; var page = 0; @@ -85,6 +87,7 @@ final class CatGatewayDocumentDataSource implements DocumentDataRemoteSource { await _getDocumentIndexList( page: page, limit: maxPerPage, + campaign: campaign, ) // TODO(damian-molinski): Remove this workaround when migrated to V2 endpoint. // https://github.com/input-output-hk/catalyst-voices/issues/3199#issuecomment-3204803465 @@ -122,10 +125,15 @@ final class CatGatewayDocumentDataSource implements DocumentDataRemoteSource { Future _getDocumentIndexList({ required int page, required int limit, + required Campaign campaign, }) async { + final categoriesIds = campaign.categories.map((e) => e.selfRef.id).toList(); + return _api.gateway .apiV1DocumentIndexPost( - body: const DocumentIndexQueryFilter(), + body: DocumentIndexQueryFilter( + category: IdSelectorDto.inside(categoriesIds).toJson(), + ), limit: limit, page: page, ) @@ -149,8 +157,7 @@ final class CatGatewayDocumentDataSource implements DocumentDataRemoteSource { abstract interface class DocumentDataRemoteSource implements DocumentDataSource { Future getLatestVersion(String id); - @override - Future> index(); + Future> index({required Campaign campaign}); Future publish(SignedDocument document); } diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/document_data_source.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/document_data_source.dart index 5599af62de5f..4ff8140690db 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/document_data_source.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/source/document_data_source.dart @@ -1,7 +1,6 @@ import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +//ignore: one_member_abstracts abstract interface class DocumentDataSource { Future get({required DocumentRef ref}); - - Future> index(); } diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/app_meta_dto.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/app_meta_dto.dart new file mode 100644 index 000000000000..87eea5e5c35d --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/app_meta_dto.dart @@ -0,0 +1,33 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_repositories/src/dto/document/document_ref_dto.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'app_meta_dto.g.dart'; + +@JsonSerializable() +final class AppMetaDto { + final DocumentRefDto? activeCampaign; + + AppMetaDto({ + this.activeCampaign, + }); + + factory AppMetaDto.fromJson(Map json) { + return _$AppMetaDtoFromJson(json); + } + + AppMetaDto.fromModel(AppMeta data) + : this( + activeCampaign: data.activeCampaign != null + ? DocumentRefDto.fromModel(data.activeCampaign!) + : null, + ); + + Map toJson() => _$AppMetaDtoToJson(this); + + AppMeta toModel() { + return AppMeta( + activeCampaign: activeCampaign?.toModel(), + ); + } +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/sync/app_meta_storage.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/sync/app_meta_storage.dart new file mode 100644 index 000000000000..3c9bba81c575 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/sync/app_meta_storage.dart @@ -0,0 +1,40 @@ +import 'dart:convert'; + +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_repositories/src/dto/app_meta_dto.dart'; +import 'package:catalyst_voices_repositories/src/storage/local_storage.dart'; + +const _dataKey = 'Data'; +const _key = 'AppMeta'; + +abstract interface class AppMetaStorage { + Future read(); + + Future write(AppMeta data); +} + +final class AppMetaStorageLocalStorage extends LocalStorage implements AppMetaStorage { + AppMetaStorageLocalStorage({ + required super.sharedPreferences, + }) : super( + key: _key, + allowList: { + _dataKey, + }, + ); + + @override + Future read() async { + final encoded = await readString(key: _dataKey); + final decoded = encoded != null ? jsonDecode(encoded) as Map : null; + final dto = decoded != null ? AppMetaDto.fromJson(decoded) : AppMetaDto(); + return dto.toModel(); + } + + @override + Future write(AppMeta data) async { + final dto = AppMetaDto.fromModel(data); + final encoded = jsonEncode(dto.toJson()); + await writeString(encoded, key: _dataKey); + } +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/database/dao/documents_dao_test.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/database/dao/documents_dao_test.dart index e8962ae83a63..62651b5912e0 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/database/dao/documents_dao_test.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/database/dao/documents_dao_test.dart @@ -1215,6 +1215,55 @@ void main() { }, onPlatform: driftOnPlatforms, ); + + test( + 'templates used in local drafts are kept when flat is true', + () async { + // Given + final template = DocumentWithMetadataFactory.build( + metadata: DocumentDataMetadata( + type: DocumentType.proposalTemplate, + selfRef: DocumentRefFactory.signedDocumentRef(), + ), + ); + + final localDraft = DraftFactory.build( + metadata: DocumentDataMetadata( + type: DocumentType.proposalDocument, + selfRef: DocumentRefFactory.draftRef(), + template: template.document.metadata.selfRef as SignedDocumentRef, + ), + ); + + final randomDocuments = List.generate( + 10, + (index) => DocumentWithMetadataFactory.build(), + ); + + final allDocuments = [ + template, + ...randomDocuments, + ]; + + final allDrafts = [ + localDraft, + ]; + + // When + await database.documentsDao.saveAll(allDocuments); + await database.draftsDao.saveAll(allDrafts); + + // Then + await database.documentsDao.deleteAll(keepTemplatesForLocalDrafts: true); + + final documentsCount = await database.documentsDao.count(); + final draftsCount = await database.draftsDao.count(); + + expect(documentsCount, 1); + expect(draftsCount, 1); + }, + onPlatform: driftOnPlatforms, + ); }); }); } diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/database/dao/proposals_dao_test.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/database/dao/proposals_dao_test.dart index 6e985cbc9431..af6f21da204d 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/database/dao/proposals_dao_test.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/database/dao/proposals_dao_test.dart @@ -411,14 +411,14 @@ void main() { () async { // Given final userId = DummyCatalystIdFactory.create(username: 'damian'); - final categoryId = constantDocumentsRefs.first.category; + final categoryId = _getCategoryId(); final proposalOneRef = DocumentRefFactory.signedDocumentRef(); final proposalTwoRef = DocumentRefFactory.signedDocumentRef(); final proposals = [ _buildProposal( selfRef: proposalOneRef, - categoryId: constantDocumentsRefs[1].category, + categoryId: _getCategoryId(index: 1), ), _buildProposal( selfRef: proposalTwoRef, @@ -761,7 +761,7 @@ void main() { () async { // Given final templateRef = DocumentRefFactory.signedDocumentRef(); - final categoryId = constantDocumentsRefs.first.category; + final categoryId = _getCategoryId(); final templates = [ _buildProposalTemplate(selfRef: templateRef), @@ -785,7 +785,7 @@ void main() { ), _buildProposal( template: templateRef, - categoryId: constantDocumentsRefs[1].category, + categoryId: _getCategoryId(index: 1), ), ]; @@ -1641,7 +1641,7 @@ void main() { // Then final result = await database.proposalsDao.queryProposals( - type: ProposalsFilterType.total, + filters: const ProposalsFilters(), ); expect(result.length, 2); @@ -1671,18 +1671,18 @@ void main() { _buildProposal( selfRef: _buildRefAt(DateTime(2025, 4, 2)), template: templateRef, - categoryId: constantDocumentsRefs[1].category, + categoryId: _getCategoryId(index: 1), ), _buildProposal( selfRef: _buildRefAt(DateTime(2025, 4, 3)), template: templateRef, - categoryId: constantDocumentsRefs[1].category, + categoryId: _getCategoryId(index: 1), ), ]; final expectedRefs = proposals .where( - (p) => p.document.metadata.categoryId == constantDocumentsRefs[1].category, + (p) => p.document.metadata.categoryId == _getCategoryId(index: 1), ) .map((proposal) => proposal.document.ref) .toList(); @@ -1692,8 +1692,8 @@ void main() { // Then final result = await database.proposalsDao.queryProposals( - categoryRef: constantDocumentsRefs[1].category, - type: ProposalsFilterType.total, + categoryRef: _getCategoryId(index: 1), + filters: const ProposalsFilters(), ); expect(result.length, 2); @@ -1759,7 +1759,7 @@ void main() { // Then final result = await database.proposalsDao.queryProposals( - type: ProposalsFilterType.finals, + filters: const ProposalsFilters(type: ProposalsFilterType.finals), ); expect(result.length, 2); @@ -1831,7 +1831,7 @@ void main() { // Then final result = await database.proposalsDao.queryProposals( - type: ProposalsFilterType.total, + filters: const ProposalsFilters(), ); expect(result.length, 1); @@ -1993,6 +1993,8 @@ void main() { }); } +final _dummyCategoriesCache = {}; + DocumentEntityWithMetadata _buildProposal({ SignedDocumentRef? selfRef, SignedDocumentRef? template, @@ -2009,7 +2011,7 @@ DocumentEntityWithMetadata _buildProposal({ authors: [ if (author != null) author, ], - categoryId: categoryId ?? constantDocumentsRefs.first.category, + categoryId: categoryId ?? _getCategoryId(), ); final content = DocumentDataContent({ if (title != null || contentAuthorName != null) @@ -2132,6 +2134,13 @@ String _buildUuidAt(DateTime dateTime) { return const Uuid().v7(config: config); } +SignedDocumentRef _getCategoryId({ + int index = 0, +}) { + return activeConstantDocumentRefs.elementAtOrNull(index)?.category ?? + _dummyCategoriesCache.putIfAbsent(index, DocumentRefFactory.signedDocumentRef); +} + extension on DocumentEntity { SignedDocumentRef get ref { return SignedDocumentRef( diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/document/document_repository_test.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/document/document_repository_test.dart index ef824bd7148c..26dc53f39b6b 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/document/document_repository_test.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/document/document_repository_test.dart @@ -305,7 +305,7 @@ void main() { ); final remoteRefs = [...refs, ...refs]; final expectedRefs = [ - ...constantDocumentsRefs.expand( + ...activeConstantDocumentRefs.expand( (e) { return e.allTyped.where((element) => element.type != categoryType); }, @@ -313,10 +313,12 @@ void main() { ...refs, ]; - when(() => remoteDocuments.index()).thenAnswer((_) => Future.value(remoteRefs)); + when( + () => remoteDocuments.index(campaign: Campaign.f14()), + ).thenAnswer((_) => Future.value(remoteRefs)); // When - final allRefs = await repository.getAllDocumentsRefs(); + final allRefs = await repository.getAllDocumentsRefs(campaign: Campaign.f14()); // Then expect( @@ -338,9 +340,11 @@ void main() { ); // When - when(() => remoteDocuments.index()).thenAnswer((_) => Future.value(refs)); + when( + () => remoteDocuments.index(campaign: Campaign.f14()), + ).thenAnswer((_) => Future.value(refs)); - await repository.getAllDocumentsRefs(); + await repository.getAllDocumentsRefs(campaign: Campaign.f14()); // Then verifyNever(() => remoteDocuments.getLatestVersion(any())); @@ -365,12 +369,14 @@ void main() { final refs = [...exactRefs, ...looseRefs]; // When - when(() => remoteDocuments.index()).thenAnswer((_) => Future.value(refs)); + when( + () => remoteDocuments.index(campaign: Campaign.f14()), + ).thenAnswer((_) => Future.value(refs)); when( () => remoteDocuments.getLatestVersion(any()), ).thenAnswer((_) => Future(DocumentRefFactory.randomUuidV7)); - final allRefs = await repository.getAllDocumentsRefs(); + final allRefs = await repository.getAllDocumentsRefs(campaign: Campaign.f14()); // Then verify(() => remoteDocuments.getLatestVersion(any())).called(looseRefs.length); @@ -384,7 +390,7 @@ void main() { 'remote loose refs to const documents are removed', () async { // Given - final constTemplatesRefs = constantDocumentsRefs + final constTemplatesRefs = activeConstantDocumentRefs .expand( (element) => [ element.proposal.toTyped(DocumentType.proposalTemplate), @@ -396,22 +402,26 @@ void main() { 10, (_) => DocumentRefFactory.signedDocumentRef().toTyped(DocumentType.proposalDocument), ); - final looseTemplatesRefs = constTemplatesRefs.map( - (e) => e.copyWith(ref: e.ref.toLoose()), - ); + final looseTemplatesRefs = constTemplatesRefs + .map((e) => e.copyWith(ref: e.ref.toLoose())) + .toList(); final refs = [ ...docsRefs, ...looseTemplatesRefs, ]; // When - when(() => remoteDocuments.index()).thenAnswer((_) => Future.value(refs)); + when( + () => remoteDocuments.index(campaign: Campaign.f14()), + ).thenAnswer((_) => Future.value(refs)); - final allRefs = await repository.getAllDocumentsRefs(); + final allRefs = await repository.getAllDocumentsRefs(campaign: Campaign.f14()); // Then - expect(allRefs, isNot(containsAll(looseTemplatesRefs))); - expect(allRefs, containsAll(constTemplatesRefs)); + if (constTemplatesRefs.isNotEmpty) { + expect(allRefs, isNot(containsAll(looseTemplatesRefs))); + expect(allRefs, containsAll(constTemplatesRefs)); + } verifyNever(() => remoteDocuments.getLatestVersion(any())); }, @@ -422,7 +432,7 @@ void main() { 'categories refs are filtered out', () async { // Given - final categoriesRefs = constantDocumentsRefs + final categoriesRefs = allConstantDocumentRefs .expand( (element) => [ element.category.toTyped(DocumentType.categoryParametersDocument), @@ -442,9 +452,11 @@ void main() { ]; // When - when(() => remoteDocuments.index()).thenAnswer((_) => Future.value(refs)); + when( + () => remoteDocuments.index(campaign: Campaign.f14()), + ).thenAnswer((_) => Future.value(refs)); - final allRefs = await repository.getAllDocumentsRefs(); + final allRefs = await repository.getAllDocumentsRefs(campaign: Campaign.f14()); // Then expect(allRefs, isNot(containsAll(categoriesRefs))); @@ -467,16 +479,18 @@ void main() { TypedDocumentRef(ref: ref, type: DocumentType.unknown), ]; final expectedRefs = [ - ...constantDocumentsRefs.expand( + ...activeConstantDocumentRefs.expand( (refs) => refs.allTyped.where((e) => e.type != categoryType), ), TypedDocumentRef(ref: ref, type: DocumentType.proposalDocument), ]; // When - when(() => remoteDocuments.index()).thenAnswer((_) => Future.value(docsRefs)); + when( + () => remoteDocuments.index(campaign: Campaign.f14()), + ).thenAnswer((_) => Future.value(docsRefs)); - final allRefs = await repository.getAllDocumentsRefs(); + final allRefs = await repository.getAllDocumentsRefs(campaign: Campaign.f14()); // Then expect(allRefs, containsAll(expectedRefs)); diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/document/source/document_data_remote_source_test.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/document/source/document_data_remote_source_test.dart index 162c531d3b36..7e7d070d3373 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/document/source/document_data_remote_source_test.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/document/source/document_data_remote_source_test.dart @@ -60,20 +60,20 @@ void main() { // When when( () => gateway.apiV1DocumentIndexPost( - body: const DocumentIndexQueryFilter(), + body: any(named: 'body'), limit: maxPageSize, page: 0, ), ).thenAnswer((_) => Future.value(pageZeroResponse)); when( () => gateway.apiV1DocumentIndexPost( - body: const DocumentIndexQueryFilter(), + body: any(named: 'body'), limit: maxPageSize, page: 1, ), ).thenAnswer((_) => Future.value(pageOneResponse)); - final refs = await source.index(); + final refs = await source.index(campaign: Campaign.f14()); // Then expect(refs, isNotEmpty); @@ -124,13 +124,13 @@ void main() { // When when( () => gateway.apiV1DocumentIndexPost( - body: const DocumentIndexQueryFilter(), + body: any(named: 'body'), limit: maxPageSize, page: 0, ), ).thenAnswer((_) => Future.value(response)); - final refs = await source.index(); + final refs = await source.index(campaign: Campaign.f14()); // Then expect( diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/campaign/campaign_service.dart b/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/campaign/campaign_service.dart index f1ca6270e6da..67815e1a1918 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/campaign/campaign_service.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/campaign/campaign_service.dart @@ -58,7 +58,7 @@ final class CampaignServiceImpl implements CampaignService { return _activeCampaignObserver.campaign; } // TODO(LynxLynxx): Call backend to get latest active campaign - final campaign = _mockedActiveCampaign ?? await getCampaign(id: Campaign.f14Ref.id); + final campaign = _mockedActiveCampaign ?? await getCampaign(id: activeCampaignRef.id); _activeCampaignObserver.campaign = campaign; return campaign; } diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/comment/comment_service.dart b/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/comment/comment_service.dart index 10420ce0caf3..3fbba444f4f1 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/comment/comment_service.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/comment/comment_service.dart @@ -46,7 +46,7 @@ final class CommentServiceImpl implements CommentService { Future getCommentTemplateFor({ required DocumentRef category, }) async { - final commentTemplateRef = constantDocumentsRefs + final commentTemplateRef = allConstantDocumentRefs .firstWhereOrNull((element) => element.category.id == category.id) ?.comment; diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/documents/documents_service.dart b/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/documents/documents_service.dart index a7dfb60fb825..087370db5ed3 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/documents/documents_service.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/documents/documents_service.dart @@ -21,17 +21,21 @@ abstract interface class DocumentsService { ) = DocumentsServiceImpl; /// Removes all locally stored documents. - Future clear(); + /// + /// if [keepLocalDrafts] is true local drafts and their templates will be kept. + Future clear({bool keepLocalDrafts}); /// Returns all matching [DocumentData] for given [ref]. Future> lookup(DocumentRef ref); /// Syncs locally stored documents with api. /// - /// [onProgress] emits from 0.0 to 1.0. + /// * [campaign] is used to sync documents only for it. + /// * [onProgress] emits from 0.0 to 1.0. /// /// Returns list of added refs. Future> sync({ + required Campaign campaign, ValueChanged? onProgress, int maxConcurrent, }); @@ -48,7 +52,9 @@ final class DocumentsServiceImpl implements DocumentsService { ); @override - Future clear() => _documentRepository.removeAll(); + Future clear({bool keepLocalDrafts = false}) { + return _documentRepository.removeAll(keepLocalDrafts: keepLocalDrafts); + } @override Future> lookup(DocumentRef ref) { @@ -57,12 +63,15 @@ final class DocumentsServiceImpl implements DocumentsService { @override Future> sync({ + required Campaign campaign, ValueChanged? onProgress, int maxConcurrent = 100, }) async { + _logger.finer('Indexing documents for f${campaign.fundNumber}'); + onProgress?.call(0.1); - final allRefs = await _documentRepository.getAllDocumentsRefs(); + final allRefs = await _documentRepository.getAllDocumentsRefs(campaign: campaign); final cachedRefs = await _documentRepository.getCachedDocumentsRefs(); final missingRefs = List.of(allRefs)..removeWhere(cachedRefs.contains); diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/proposal/proposal_service.dart b/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/proposal/proposal_service.dart index a0b832b78017..6ab18c4d8032 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/proposal/proposal_service.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/proposal/proposal_service.dart @@ -603,9 +603,13 @@ final class ProposalServiceImpl implements ProposalService { return const Stream.empty(); } + final activeCampaign = _activeCampaignObserver.campaign; + final categoriesIds = activeCampaign?.categories.map((e) => e.selfRef.id).toList(); + final filters = ProposalsCountFilters( author: authorId, onlyAuthor: true, + campaign: categoriesIds != null ? CampaignFilters(categoriesIds: categoriesIds) : null, ); return watchProposalsCount(filters: filters); diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/sync/sync_manager.dart b/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/sync/sync_manager.dart index 7e111b3f8131..0ace7b988465 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/sync/sync_manager.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/sync/sync_manager.dart @@ -11,8 +11,10 @@ final _logger = Logger('SyncManager'); /// [SyncManager] provides synchronization functionality for documents. abstract interface class SyncManager { factory SyncManager( + AppMetaStorage appMetaStorage, SyncStatsStorage statsStorage, DocumentsService documentsService, + CampaignService campaignService, ) = SyncManagerImpl; Future get waitForSync; @@ -23,8 +25,10 @@ abstract interface class SyncManager { } final class SyncManagerImpl implements SyncManager { + final AppMetaStorage _appMetaStorage; final SyncStatsStorage _statsStorage; final DocumentsService _documentsService; + final CampaignService _campaignService; final _lock = Lock(); @@ -32,8 +36,10 @@ final class SyncManagerImpl implements SyncManager { var _synchronizationCompleter = Completer(); SyncManagerImpl( + this._appMetaStorage, this._statsStorage, this._documentsService, + this._campaignService, ); @override @@ -74,20 +80,24 @@ final class SyncManagerImpl implements SyncManager { } final stopwatch = Stopwatch()..start(); - try { _logger.fine('Synchronization started'); + // This means when campaign will become document we'll have get it first + // with separate request + final activeCampaign = await _updateActiveCampaign(); + if (activeCampaign == null) { + _logger.finer('No active campaign found!'); + return; + } + final newRefs = await _documentsService.sync( + campaign: activeCampaign, onProgress: (value) { _logger.finest('Documents sync progress[$value]'); }, ); - stopwatch.stop(); - - _logger.fine('Synchronization took ${stopwatch.elapsed}'); - await _updateSuccessfulSyncStats( newRefsCount: newRefs.length, duration: stopwatch.elapsed, @@ -96,15 +106,37 @@ final class SyncManagerImpl implements SyncManager { _logger.fine('Synchronization completed. NewRefs[${newRefs.length}]'); _synchronizationCompleter.complete(true); } catch (error, stack) { - stopwatch.stop(); - - _logger.severe('Synchronization failed after ${stopwatch.elapsed}', error, stack); + _logger.severe('Synchronization failed', error, stack); _synchronizationCompleter.complete(false); rethrow; + } finally { + stopwatch.stop(); + + _logger.fine('Synchronization took ${stopwatch.elapsed}'); } } + Future _updateActiveCampaign() async { + final appMeta = await _appMetaStorage.read(); + final activeCampaign = await _campaignService.getActiveCampaign(); + + final previous = appMeta.activeCampaign; + final current = activeCampaign?.selfRef; + + if (previous == current) { + return activeCampaign; + } + + _logger.fine('Active campaign changed from [$previous] to [$current]!'); + + final updatedAppMeta = appMeta.copyWith(activeCampaign: Optional(current)); + await _appMetaStorage.write(updatedAppMeta); + await _documentsService.clear(keepLocalDrafts: true); + + return activeCampaign; + } + Future _updateSuccessfulSyncStats({ required int newRefsCount, required Duration duration, diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/test/src/documents/documents_service_test.dart b/catalyst_voices/packages/internal/catalyst_voices_services/test/src/documents/documents_service_test.dart index 2bb52bb9083a..2779d4eab82a 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/test/src/documents/documents_service_test.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_services/test/src/documents/documents_service_test.dart @@ -32,13 +32,15 @@ void main() { final cachedRefs = []; // When - when(documentRepository.getAllDocumentsRefs).thenAnswer((_) => Future.value(allRefs)); + when( + () => documentRepository.getAllDocumentsRefs(campaign: Campaign.f14()), + ).thenAnswer((_) => Future.value(allRefs)); when(documentRepository.getCachedDocumentsRefs).thenAnswer((_) => Future.value(cachedRefs)); when( () => documentRepository.cacheDocument(ref: any(named: 'ref')), ).thenAnswer((_) => Future(() {})); - await service.sync(); + await service.sync(campaign: Campaign.f14()); // Then verify(() => documentRepository.cacheDocument(ref: any(named: 'ref'))).called(allRefs.length); @@ -54,13 +56,15 @@ void main() { final expectedCalls = allRefs.length - cachedRefs.length; // When - when(documentRepository.getAllDocumentsRefs).thenAnswer((_) => Future.value(allRefs)); + when( + () => documentRepository.getAllDocumentsRefs(campaign: Campaign.f14()), + ).thenAnswer((_) => Future.value(allRefs)); when(documentRepository.getCachedDocumentsRefs).thenAnswer((_) => Future.value(cachedRefs)); when( () => documentRepository.cacheDocument(ref: any(named: 'ref')), ).thenAnswer((_) => Future(() {})); - await service.sync(); + await service.sync(campaign: Campaign.f14()); // Then verify(() => documentRepository.cacheDocument(ref: any(named: 'ref'))).called(expectedCalls); @@ -81,13 +85,15 @@ void main() { ); // When - when(documentRepository.getAllDocumentsRefs).thenAnswer((_) => Future.value(allRefs)); + when( + () => documentRepository.getAllDocumentsRefs(campaign: Campaign.f14()), + ).thenAnswer((_) => Future.value(allRefs)); when(documentRepository.getCachedDocumentsRefs).thenAnswer((_) => Future.value(cachedRefs)); when( () => documentRepository.cacheDocument(ref: any(named: 'ref')), ).thenAnswer((_) => Future(() {})); - await service.sync(); + await service.sync(campaign: Campaign.f14()); // Then verifyNever( @@ -105,7 +111,9 @@ void main() { var progress = 0.0; // When - when(documentRepository.getAllDocumentsRefs).thenAnswer((_) => Future.value(allRefs)); + when( + () => documentRepository.getAllDocumentsRefs(campaign: Campaign.f14()), + ).thenAnswer((_) => Future.value(allRefs)); when(documentRepository.getCachedDocumentsRefs).thenAnswer((_) => Future.value(cachedRefs)); when( () => documentRepository.cacheDocument(ref: any(named: 'ref')), @@ -113,6 +121,7 @@ void main() { // Then await service.sync( + campaign: Campaign.f14(), onProgress: (value) { progress = value; }, @@ -131,14 +140,16 @@ void main() { final expectedNewRefs = allRefs.sublist(cachedRefs.length); // When - when(documentRepository.getAllDocumentsRefs).thenAnswer((_) => Future.value(allRefs)); + when( + () => documentRepository.getAllDocumentsRefs(campaign: Campaign.f14()), + ).thenAnswer((_) => Future.value(allRefs)); when(documentRepository.getCachedDocumentsRefs).thenAnswer((_) => Future.value(cachedRefs)); when( () => documentRepository.cacheDocument(ref: any(named: 'ref')), ).thenAnswer((_) => Future(() {})); // Then - final newRefs = await service.sync(); + final newRefs = await service.sync(campaign: Campaign.f14()); expect( newRefs, diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/test/src/sync/sync_manager_test.dart b/catalyst_voices/packages/internal/catalyst_voices_services/test/src/sync/sync_manager_test.dart index baa86cbfc2a5..bca264707475 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/test/src/sync/sync_manager_test.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_services/test/src/sync/sync_manager_test.dart @@ -13,21 +13,35 @@ import 'package:uuid_plus/uuid_plus.dart'; void main() { final DocumentRepository documentRepository = _MockDocumentRepository(); + + late final AppMetaStorage appMetaStorage; late final SyncStatsStorage statsStorage; late final DocumentsService documentsService; + late final CampaignService campaignService; late final SyncManager syncManager; - setUpAll(() { + setUpAll(() async { SharedPreferencesAsyncPlatform.instance = InMemorySharedPreferencesAsync.empty(); statsStorage = SyncStatsLocalStorage(sharedPreferences: SharedPreferencesAsync()); + appMetaStorage = AppMetaStorageLocalStorage(sharedPreferences: SharedPreferencesAsync()); + documentsService = DocumentsService(documentRepository); + campaignService = _MockCampaignService(); registerFallbackValue(SignedDocumentRef.first(const Uuid().v7())); + + await appMetaStorage.write(const AppMeta(activeCampaign: Campaign.f15Ref)); + when(() => campaignService.getActiveCampaign()).thenAnswer((_) => Future.value(Campaign.f15())); }); setUp(() { - syncManager = SyncManager(statsStorage, documentsService); + syncManager = SyncManager( + appMetaStorage, + statsStorage, + documentsService, + campaignService, + ); }); tearDown(() async { @@ -45,7 +59,9 @@ void main() { final cachedRefs = []; // When - when(documentRepository.getAllDocumentsRefs).thenAnswer((_) => Future.value(allRefs)); + when( + () => documentRepository.getAllDocumentsRefs(campaign: Campaign.f15()), + ).thenAnswer((_) => Future.value(allRefs)); when(documentRepository.getCachedDocumentsRefs).thenAnswer((_) => Future.value(cachedRefs)); when( () => documentRepository.cacheDocument(ref: any(named: 'ref')), @@ -60,4 +76,6 @@ void main() { }); } +class _MockCampaignService extends Mock implements CampaignService {} + class _MockDocumentRepository extends Mock implements DocumentRepository {} diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/proposal/user_proposal_overview.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/proposal/user_proposal_overview.dart index b3e7c2cd62eb..7192ea137927 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/proposal/user_proposal_overview.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/proposal/user_proposal_overview.dart @@ -13,6 +13,7 @@ final class UsersProposalOverview extends Equatable { final String category; final SignedDocumentRef categoryId; final int fundNumber; + final bool fromActiveCampaign; const UsersProposalOverview({ required this.selfRef, @@ -25,13 +26,15 @@ final class UsersProposalOverview extends Equatable { required this.category, required this.categoryId, required this.fundNumber, + required this.fromActiveCampaign, }); factory UsersProposalOverview.fromProposal( DetailProposal proposal, int fundNumber, - String categoryName, - ) { + String categoryName, { + required bool fromActiveCampaign, + }) { return UsersProposalOverview( selfRef: proposal.selfRef, title: proposal.title, @@ -43,6 +46,7 @@ final class UsersProposalOverview extends Equatable { category: categoryName, categoryId: proposal.categoryRef, fundNumber: fundNumber, + fromActiveCampaign: fromActiveCampaign, ); } @@ -69,6 +73,7 @@ final class UsersProposalOverview extends Equatable { category, categoryId, fundNumber, + fromActiveCampaign, ]; UsersProposalOverview copyWith({ @@ -82,6 +87,7 @@ final class UsersProposalOverview extends Equatable { String? category, SignedDocumentRef? categoryId, int? fundNumber, + bool? fromActiveCampaign, }) { return UsersProposalOverview( selfRef: selfRef ?? this.selfRef, @@ -94,6 +100,7 @@ final class UsersProposalOverview extends Equatable { category: category ?? this.category, categoryId: categoryId ?? this.categoryId, fundNumber: fundNumber ?? this.fundNumber, + fromActiveCampaign: fromActiveCampaign ?? this.fromActiveCampaign, ); } } diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/proposal_builder/proposal_menu_item_action_enum.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/proposal_builder/proposal_menu_item_action_enum.dart index e4e13377b1bb..40e1925e8840 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/proposal_builder/proposal_menu_item_action_enum.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/proposal_builder/proposal_menu_item_action_enum.dart @@ -103,8 +103,13 @@ enum ProposalMenuItemAction { /// Returns the available options for a proposal in a proposal builder static List proposalBuilderAvailableOptions( - ProposalPublish proposalPublish, - ) { + ProposalPublish proposalPublish, { + required bool fromActiveCampaign, + }) { + if (!fromActiveCampaign) { + return [view]; + } + switch (proposalPublish) { case ProposalPublish.localDraft: return [view, publish, submit, export, delete]; @@ -118,9 +123,11 @@ enum ProposalMenuItemAction { /// Returns the available options for a proposal in a workspace static List workspaceAvailableOptions( - ProposalPublish proposalPublish, - ) { + ProposalPublish proposalPublish, { + required bool fromActiveCampaign, + }) { return switch (proposalPublish) { + _ when !fromActiveCampaign => [view], ProposalPublish.localDraft => [edit, export, delete], _ => [ edit, diff --git a/catalyst_voices/pubspec.yaml b/catalyst_voices/pubspec.yaml index 5935e7dfc698..b55896c5d14b 100644 --- a/catalyst_voices/pubspec.yaml +++ b/catalyst_voices/pubspec.yaml @@ -4,7 +4,7 @@ environment: sdk: ">=3.3.3 <4.0.0" dependencies: - dart_pubspec_licenses: ^3.0.4 + dart_pubspec_licenses: 3.0.4 dev_dependencies: melos: ^6.3.3 \ No newline at end of file