Repository: input-output-hk/catalyst-voices
Pull request #1924: feat(cat-voices): Connecting frontend with backend on discovery page 
Author: login: LynxLynxx
Labels: name: review me
description: PR is ready for review
name: ux/ui
description: UX/UI related issues
name: dart
description: Pull requests that update Dart code
Created: 2025-02-26T12:14:21Z | Merged: 2025-03-05T09:15:03Z

# Description

Connecting frontend actually to local db which automatically gets data from backend. Only one section on this page comes from db other things are static so they need to be updated when data will be provided.

## Related Issue(s)

#1553

## Description of Changes

- adding new methods to local db (document_dao)
- adding method to `DocumentRepository`
- creating a stream in `ProposalService` which updates when new proposal is inserted into db or new comment to any of watched proposal is inserted
- conneting to stream in `DiscoveryBloc`
- changing `Proposal` Model
- deleting `ProposalBase`
- adding new `ProposalData` which can be extended to have more data if we need


## Breaking Changes

- changing `Proposal` Model
- deleting `ProposalBase`

## Screenshots

N/A

## Related Pull Requests

N/A

## Please confirm the following checks

* [x] My code follows the style guidelines of this project
* [x] I have performed a self-review of my code
* [x] I have commented my code, particularly in hard-to-understand areas
* [x] I have made corresponding changes to the documentation
* [x] My changes generate no new warnings
* [x] I have added tests that prove my fix is effective or that my feature works
* [x] New and existing unit tests pass locally with my changes
* [x] Any dependent changes have been merged and published in downstream module



Diff:
diff --git a/catalyst_voices/apps/voices/lib/dependency/dependencies.dart b/catalyst_voices/apps/voices/lib/dependency/dependencies.dart
index 17edf6baf9ef..690f8581b542 100644
--- a/catalyst_voices/apps/voices/lib/dependency/dependencies.dart
+++ b/catalyst_voices/apps/voices/lib/dependency/dependencies.dart
@@ -111,6 +111,7 @@ final class Dependencies extends DependencyProvider {
       ..registerFactory<DiscoveryCubit>(() {
         return DiscoveryCubit(
           get<CampaignService>(),
+          get<ProposalService>(),
         );
       })
       ..registerFactory<CategoryDetailCubit>(() {
@@ -148,7 +149,7 @@ final class Dependencies extends DependencyProvider {
           get<CatalystDatabase>(),
         );
       })
-      ..registerLazySingleton<DocumentDataLocalSource>(() {
+      ..registerLazySingleton<SignedDocumentDataSource>(() {
         return DatabaseDocumentsDataSource(
           get<CatalystDatabase>(),
         );
@@ -167,7 +168,7 @@ final class Dependencies extends DependencyProvider {
       ..registerLazySingleton<DocumentRepository>(() {
         return DocumentRepository(
           get<DatabaseDraftsDataSource>(),
-          get<DocumentDataLocalSource>(),
+          get<SignedDocumentDataSource>(),
           get<CatGatewayDocumentDataSource>(),
         );
       })
diff --git a/catalyst_voices/apps/voices/lib/widgets/cards/pending_proposal_card.dart b/catalyst_voices/apps/voices/lib/widgets/cards/pending_proposal_card.dart
index 16d72dd8b7f4..73423ad76734 100644
--- a/catalyst_voices/apps/voices/lib/widgets/cards/pending_proposal_card.dart
+++ b/catalyst_voices/apps/voices/lib/widgets/cards/pending_proposal_card.dart
@@ -316,7 +316,7 @@ final class _ProposalBorderColor extends WidgetStateColor {
 class _ProposalInfo extends StatelessWidget {
   final ProposalPublish proposalStage;
   final int version;
-  final DateTime lastUpdate;
+  final DateTime? lastUpdate;
   final int commentsCount;
   final bool showLastUpdate;
 
@@ -339,12 +339,12 @@ class _ProposalInfo extends StatelessWidget {
           const FinalProposalChip(),
         const SizedBox(width: 4),
         ProposalVersionChip(version: version.toString()),
-        if (showLastUpdate) ...[
+        if (showLastUpdate && lastUpdate != null) ...[
           const SizedBox(width: 4),
           VoicesPlainTooltip(
             message: _tooltipMessage(context),
             child: DayMonthTimeText(
-              dateTime: lastUpdate,
+              dateTime: lastUpdate!,
             ),
           ),
         ],
@@ -363,7 +363,11 @@ class _ProposalInfo extends StatelessWidget {
   }
 
   String _tooltipMessage(BuildContext context) {
-    final dt = DateFormatter.formatDateTimeParts(lastUpdate, includeYear: true);
+    if (lastUpdate == null) {
+      return '';
+    }
+    final dt =
+        DateFormatter.formatDateTimeParts(lastUpdate!, includeYear: true);
 
     return context.l10n.publishedOn(dt.date, dt.time);
   }
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 efd998c20a53..d2406c5e8a2f 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
@@ -1,5 +1,7 @@
+import 'dart:async';
 import 'dart:math';
 
+import 'package:catalyst_voices_models/catalyst_voices_models.dart';
 import 'package:catalyst_voices_services/catalyst_voices_services.dart';
 import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart';
 import 'package:collection/collection.dart';
@@ -11,7 +13,20 @@ part 'discovery_state.dart';
 class DiscoveryCubit extends Cubit<DiscoveryState> {
   // ignore: unused_field
   final CampaignService _campaignService;
-  DiscoveryCubit(this._campaignService) : super(const DiscoveryState());
+  final ProposalService _proposalService;
+  late StreamSubscription<List<Proposal>>? _proposalsSubscription;
+
+  DiscoveryCubit(
+    this._campaignService,
+    this._proposalService,
+  ) : super(const DiscoveryState());
+
+  @override
+  Future<void> close() {
+    _proposalsSubscription?.cancel();
+    _proposalsSubscription = null;
+    return super.close();
+  }
 
   Future<void> getAllData() async {
     await Future.wait([
@@ -21,36 +36,6 @@ class DiscoveryCubit extends Cubit<DiscoveryState> {
     ]);
   }
 
-  Future<void> getCurrentCampaign() async {
-    emit(
-      state.copyWith(
-        currentCampaign: const DiscoveryCurrentCampaignState(),
-      ),
-    );
-
-    final isSuccess = await Future.delayed(
-      const Duration(seconds: 2),
-      () => Random().nextBool(),
-    );
-    if (isClosed) return;
-
-    final currentCampaign = isSuccess
-        ? CurrentCampaignInfoViewModel.dummy()
-        : const NullCurrentCampaignInfoViewModel();
-
-    final error = isSuccess ? null : const LocalizedUnknownException();
-
-    emit(
-      state.copyWith(
-        currentCampaign: DiscoveryCurrentCampaignState(
-          currentCampaign: currentCampaign,
-          error: error,
-          isLoading: false,
-        ),
-      ),
-    );
-  }
-
   Future<void> getCampaignCategories() async {
     emit(
       state.copyWith(
@@ -84,43 +69,88 @@ class DiscoveryCubit extends Cubit<DiscoveryState> {
     );
   }
 
-  Future<void> getMostRecentProposals() async {
+  Future<void> getCurrentCampaign() async {
     emit(
       state.copyWith(
-        mostRecentProposals: const DiscoveryMostRecentProposalsState(),
+        currentCampaign: const DiscoveryCurrentCampaignState(),
       ),
     );
 
     final isSuccess = await Future.delayed(
-      const Duration(seconds: 1),
+      const Duration(seconds: 2),
       () => Random().nextBool(),
     );
     if (isClosed) return;
 
-    // isSuccess = false;
-
-    final proposals = isSuccess
-        ? List<PendingProposal>.filled(
-            7,
-            PendingProposal.dummy(),
-          )
-        : const <PendingProposal>[];
+    final currentCampaign = isSuccess
+        ? CurrentCampaignInfoViewModel.dummy()
+        : const NullCurrentCampaignInfoViewModel();
 
     final error = isSuccess ? null : const LocalizedUnknownException();
 
-    final newState = state.copyWith(
-      mostRecentProposals: DiscoveryMostRecentProposalsState(
-        isLoading: false,
-        error: error,
-        proposals: proposals,
+    emit(
+      state.copyWith(
+        currentCampaign: DiscoveryCurrentCampaignState(
+          currentCampaign: currentCampaign,
+          error: error,
+          isLoading: false,
+        ),
+      ),
+    );
+  }
+
+  Future<void> getMostRecentProposals() async {
+    emit(
+      state.copyWith(
+        mostRecentProposals: const DiscoveryMostRecentProposalsState(),
       ),
     );
 
-    emit(newState);
+    await _proposalsSubscription?.cancel();
+    _proposalsSubscription = null;
+    _setupProposalsSubscription();
   }
 
   CampaignCategoryViewModel? localCategory(String id) {
     return state.campaignCategories.categories
         .firstWhereOrNull((e) => e.id == id);
   }
+
+  void _setupProposalsSubscription() {
+    _proposalsSubscription =
+        _proposalService.watchLatestProposals(limit: 7).listen(
+      (proposals) {
+        if (isClosed) return;
+        final proposalList = proposals
+            .map(
+              (e) => PendingProposal.fromProposal(
+                e,
+                campaignName: 'f14',
+              ),
+            )
+            .toList();
+        emit(
+          state.copyWith(
+            mostRecentProposals: DiscoveryMostRecentProposalsState(
+              isLoading: false,
+              error: null,
+              proposals: proposalList,
+            ),
+          ),
+        );
+      },
+      onError: (error) {
+        if (isClosed) return;
+        emit(
+          state.copyWith(
+            mostRecentProposals: const DiscoveryMostRecentProposalsState(
+              isLoading: false,
+              error: LocalizedUnknownException(),
+              proposals: [],
+            ),
+          ),
+        );
+      },
+    );
+  }
 }
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 9a7318247a23..2554001cd577 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
@@ -274,14 +274,15 @@ final class ProposalBuilderBloc
     _logger.info('Loading proposal[${event.ref}]');
 
     await _loadState(emit, () async {
-      final proposal = await _proposalService.getProposal(ref: event.ref);
+      final proposalData = await _proposalService.getProposal(ref: event.ref);
+      final proposal = Proposal.fromData(proposalData);
 
       return _createState(
-        document: proposal.document.document,
+        document: proposalData.document.document,
         metadata: ProposalBuilderMetadata(
           publish: proposal.publish,
-          documentRef: proposal.ref,
-          currentIteration: proposal.version,
+          documentRef: proposal.selfRef,
+          currentIteration: proposal.versionCount,
         ),
       );
     });
diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposal_builder/proposal_builder_state.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposal_builder/proposal_builder_state.dart
index ed4406cbb8a6..2ebbc32bfe56 100644
--- a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposal_builder/proposal_builder_state.dart
+++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposal_builder/proposal_builder_state.dart
@@ -69,7 +69,7 @@ final class ProposalBuilderState extends Equatable {
   });
 
   String? get proposalTitle {
-    final property = document?.getProperty(Proposal.titleNodeId)
+    final property = document?.getProperty(ProposalDocument.titleNodeId)
         as DocumentValueProperty<String>?;
 
     return property?.value;
diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/test/proposals/proposals_cubit_test.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/test/proposals/proposals_cubit_test.dart
index f9ffa2ec3bd6..8bc876f97eee 100644
--- a/catalyst_voices/packages/internal/catalyst_voices_blocs/test/proposals/proposals_cubit_test.dart
+++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/test/proposals/proposals_cubit_test.dart
@@ -10,28 +10,6 @@ import 'package:uuid/uuid.dart';
 
 void main() {
   group(ProposalsCubit, () {
-    const proposalTemplate = DocumentSchema(
-      parentSchemaUrl: '',
-      schemaSelfUrl: '',
-      title: '',
-      description: MarkdownData.empty,
-      properties: [],
-      order: [],
-    );
-
-    final proposalDocument = ProposalDocument(
-      metadata: ProposalMetadata(
-        selfRef: SignedDocumentRef(
-          id: const Uuid().v7(),
-          version: const Uuid().v7(),
-        ),
-      ),
-      document: const Document(
-        schema: proposalTemplate,
-        properties: [],
-      ),
-    );
-
     final campaign = Campaign(
       id: 'F14',
       name: 'campaign',
@@ -43,7 +21,10 @@ void main() {
     );
 
     final proposal = Proposal(
-      id: const Uuid().v7(),
+      selfRef: SignedDocumentRef(
+        id: const Uuid().v7(),
+        version: const Uuid().v7(),
+      ),
       title: 'Proposal 1',
       description: 'Description 1',
       category: '',
@@ -52,8 +33,7 @@ void main() {
       status: ProposalStatus.draft,
       publish: ProposalPublish.publishedDraft,
       commentsCount: 0,
-      document: proposalDocument,
-      version: 1,
+      versionCount: 1,
       duration: 6,
       author: 'Alex Wells',
     );
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 4afac948f45e..8b58f3614a65 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
@@ -35,7 +35,8 @@ export 'node_id.dart';
 export 'optional.dart';
 export 'pagination/pagination_page.dart';
 export 'proposal/proposal.dart';
-export 'proposal/proposal_base.dart';
+export 'proposal/proposal_data.dart';
+export 'proposal/proposal_enums.dart';
 export 'proposal/proposal_pagination_items.dart';
 export 'proposal/proposal_pagination_request.dart';
 export 'registration/registration.dart';
diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/data/document_data_content.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/data/document_data_content.dart
index 3dea5467870a..7412050cb024 100644
--- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/data/document_data_content.dart
+++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/data/document_data_content.dart
@@ -13,6 +13,9 @@ final class DocumentDataContent extends Equatable {
   @override
   List<Object?> get props => [data];
 
-  String? get title =>
-      DocumentNodeTraverser.getValue(Proposal.titleNodeId, data) as String?;
+  String? get title {
+    final value =
+        DocumentNodeTraverser.getValue(ProposalDocument.titleNodeId, data);
+    return value as String?;
+  }
 }
diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/data/document_type.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/data/document_type.dart
index 8c152a4f8855..8b2f884e03a7 100644
--- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/data/document_type.dart
+++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/data/document_type.dart
@@ -3,6 +3,17 @@
 enum DocumentType {
   proposalDocument(uuid: '7808d2ba-d511-40af-84e8-c0d1625fdfdc'),
   proposalTemplate(uuid: '0ce8ab38-9258-4fbc-a62e-7faa6e58318f'),
+  commentDocument(uuid: 'b679ded3-0e7c-41ba-89f8-da62a17898ea'),
+  commentTemplate(uuid: '0b8424d4-ebfd-46e3-9577-1775a69d290c'),
+  reviewDocument(uuid: 'e4caf5f0-098b-45fd-94f3-0702a4573db5'),
+  reviewTemplate(uuid: 'ebe5d0bf-5d86-4577-af4d-008fddbe2edc'),
+  categoryParametersDocument(uuid: '48c20109-362a-4d32-9bba-e0a9cf8b45be'),
+  categoryParametersTemplate(uuid: '65b1e8b0-51f1-46a5-9970-72cdf26884be'),
+  campaignParametersDocument(uuid: '0110ea96-a555-47ce-8408-36efe6ed6f7c'),
+  campaignParametersTemplate(uuid: '7e8f5fa2-44ce-49c8-bfd5-02af42c179a3'),
+  brandParametersDocument(uuid: '3e4808cc-c86e-467b-9702-d60baa9d1fca'),
+  brandParametersTemplate(uuid: 'fd3c1735-80b1-4eea-8d63-5f436d97ea31'),
+  proposalActionDocument(uuid: '5e60e623-ad02-4a1b-a1ac-406db978ee48'),
   unknown(uuid: '');
 
   final String uuid;
diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/document_ref.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/document_ref.dart
index 616a62dcd732..f2dc269102bd 100644
--- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/document_ref.dart
+++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/document_ref.dart
@@ -90,6 +90,14 @@ final class SignedDocumentRef extends DocumentRef {
     super.version,
   });
 
+  factory SignedDocumentRef.generateFirstRef() {
+    final id = const Uuid().v7();
+    return SignedDocumentRef(
+      id: id,
+      version: id,
+    );
+  }
+
   @override
   SignedDocumentRef copyWith({
     String? id,
diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/specialized/proposal_document.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/specialized/proposal_document.dart
index e34e616382a0..0df11234c62f 100644
--- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/specialized/proposal_document.dart
+++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/specialized/proposal_document.dart
@@ -3,6 +3,20 @@ import 'package:catalyst_voices_models/src/document/document_metadata.dart';
 import 'package:equatable/equatable.dart';
 
 final class ProposalDocument extends Equatable {
+  /// A hardcoded [NodeId] of the title property.
+  ///
+  /// Since properties are dynamic the application cannot determine
+  /// which property is the title in any other way than
+  /// by hardcoding it's node ID.
+  static final titleNodeId = DocumentNodeId.fromString('setup.title.title');
+  static final descriptionNodeId =
+      DocumentNodeId.fromString('summary.solution.summary');
+  static final requestedFundsNodeId =
+      DocumentNodeId.fromString('summary.budget.requestedFunds');
+  static final durationNodeId =
+      DocumentNodeId.fromString('summary.time.duration');
+  static final authorNameNodeId =
+      DocumentNodeId.fromString('summary.proposer.applicant');
   static const String exportFileExt = 'json';
 
   final ProposalMetadata metadata;
diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal.dart
index 14fe8f28686c..9a211414af03 100644
--- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal.dart
+++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal.dart
@@ -1,34 +1,89 @@
+import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart';
 import 'package:catalyst_voices_models/catalyst_voices_models.dart';
+import 'package:catalyst_voices_shared/catalyst_voices_shared.dart';
+import 'package:equatable/equatable.dart';
 
-final class Proposal extends ProposalBase {
-  /// A hardcoded [NodeId] of the title property.
-  ///
-  /// Since properties are dynamic the application cannot determine
-  /// which property is the title in any other way than
-  /// by hardcoding it's node ID.
-  static final titleNodeId = DocumentNodeId.fromString('setup.title.title');
-
-  final ProposalDocument document;
+final class Proposal extends Equatable {
+  final DocumentRef selfRef;
+  final String title;
+  final String description;
+  final DateTime? updateDate;
+  final DateTime? fundedDate;
+  final Coin fundsRequested;
+  final ProposalStatus status;
+  final ProposalPublish publish;
+  final int versionCount;
+  final int duration;
+  final String author;
+  final int commentsCount;
+  final String category;
 
   const Proposal({
-    required super.id,
-    required super.title,
-    required super.description,
-    required super.updateDate,
-    required super.fundsRequested,
-    required super.status,
-    required super.publish,
-    required super.category,
-    required super.commentsCount,
-    required this.document,
-    required super.version,
-    required super.duration,
-    required super.author,
+    required this.selfRef,
+    required this.title,
+    required this.description,
+    required this.updateDate,
+    this.fundedDate,
+    required this.fundsRequested,
+    required this.status,
+    required this.publish,
+    required this.versionCount,
+    required this.duration,
+    required this.author,
+    required this.commentsCount,
+    required this.category,
   });
 
+  factory Proposal.dummy(DocumentRef ref) => Proposal(
+        selfRef: ref,
+        category: 'Cardano Use Cases / MVP',
+        title: 'Dummy Proposal',
+        updateDate: DateTime.now(),
+        fundsRequested: Coin.fromAda(100000),
+        status: ProposalStatus.draft,
+        publish: ProposalPublish.localDraft,
+        commentsCount: 0,
+        description: 'Dummy description',
+        duration: 6,
+        author: 'Alex Wells',
+        versionCount: 1,
+      );
+
+  factory Proposal.fromData(ProposalData data) {
+    DateTime? updateDate;
+    final version = data.document.metadata.selfRef.version;
+    if (version != null) {
+      updateDate = UuidUtils.dateTime(version);
+    }
+    return Proposal(
+      selfRef: data.ref,
+      title: data.proposalTitle ?? '',
+      description: data.proposalDescription ?? '',
+      updateDate: updateDate,
+      fundsRequested: data.proposalFundsRequested ?? Coin.fromAda(0),
+      // TODO(LynxLynxx): from here we need to get the real status
+      status: ProposalStatus.inProgress,
+      // TODO(LynxLynxx): from here we need to get the real publish
+      publish: ProposalPublish.publishedDraft,
+      versionCount: data.versions.length,
+      duration: data.proposalDuration ?? 0,
+      author: data.proposalAuthor ?? '',
+      commentsCount: data.commentsCount,
+      category: data.categoryId,
+    );
+  }
+
   @override
   List<Object?> get props => [
-        ...super.props,
-        document,
+        selfRef,
+        title,
+        description,
+        updateDate,
+        fundedDate,
+        fundsRequested,
+        status,
+        publish,
+        category,
+        commentsCount,
       ];
 }
diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_base.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_base.dart
deleted file mode 100644
index 0b3e70aecfb1..000000000000
--- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_base.dart
+++ /dev/null
@@ -1,108 +0,0 @@
-import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart';
-import 'package:catalyst_voices_models/catalyst_voices_models.dart';
-import 'package:equatable/equatable.dart';
-import 'package:flutter/foundation.dart';
-
-// Note. Most, if not all, fields will be removed from here because they come
-// from document.
-base class ProposalBase extends Equatable {
-  final String id;
-  final String title;
-  final String description;
-  final DateTime updateDate;
-  final DateTime? fundedDate;
-  final Coin fundsRequested;
-  final ProposalStatus status;
-  final ProposalPublish publish;
-
-  // Most likely this will be inside the document class and not in this class
-  final int version;
-  final int duration;
-  final String author;
-
-  // This may be a reference to class
-  final String category;
-
-  // Those may be getters.
-  final int commentsCount;
-
-  const ProposalBase({
-    required this.id,
-    required this.title,
-    required this.description,
-    required this.updateDate,
-    this.fundedDate,
-    required this.fundsRequested,
-    required this.status,
-    required this.publish,
-    required this.category,
-    required this.commentsCount,
-    required this.version,
-    required this.duration,
-    required this.author,
-  });
-
-  int get completedSegments => 0;
-
-  @override
-  @mustCallSuper
-  List<Object?> get props => [
-        id,
-        title,
-        description,
-        updateDate,
-        fundedDate,
-        fundsRequested.value,
-        status,
-        publish,
-        category,
-        commentsCount,
-      ];
-
-  // TODO(damian-molinski): this should come from api
-  DocumentRef get ref => const SignedDocumentRef(id: 'document');
-
-  int get totalSegments => 0;
-
-  Proposal toProposal({
-    required ProposalDocument document,
-  }) {
-    return Proposal(
-      id: id,
-      title: title,
-      description: description,
-      updateDate: updateDate,
-      fundsRequested: fundsRequested,
-      status: status,
-      publish: publish,
-      category: category,
-      commentsCount: commentsCount,
-      document: document,
-      version: version,
-      duration: duration,
-      author: author,
-    );
-  }
-}
-
-enum ProposalPublish {
-  /// A proposal has not yet been published,
-  /// it's stored only in the local storage.
-  localDraft,
-
-  /// A proposal has been published as a draft.
-  publishedDraft,
-
-  /// A proposal has been published and submitted into review.
-  ///
-  /// After the review stage is done the proposal will be live
-  /// with no additional proposer action.
-  submittedProposal;
-
-  bool get isDraft => this == publishedDraft;
-
-  bool get isPublished => this == submittedProposal;
-}
-
-// Note. This enum may be deleted later. Its here for backwards compatibility.
-enum ProposalStatus { ready, draft, inProgress, private, open, live, completed }
diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_data.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_data.dart
new file mode 100644
index 000000000000..3646a504fd2d
--- /dev/null
+++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_data.dart
@@ -0,0 +1,68 @@
+import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart';
+import 'package:catalyst_voices_models/catalyst_voices_models.dart';
+import 'package:equatable/equatable.dart';
+
+class ProposalData extends Equatable {
+  final ProposalDocument document;
+  final DocumentRef ref;
+  final String categoryId;
+  final int commentsCount;
+  final List<String> versions;
+
+  const ProposalData({
+    required this.document,
+    required this.ref,
+    required this.categoryId,
+    this.commentsCount = 0,
+    this.versions = const [],
+  });
+
+  String? get proposalAuthor {
+    final property =
+        document.document.getProperty(ProposalDocument.authorNameNodeId)
+            as DocumentValueProperty<String>?;
+
+    return property?.value;
+  }
+
+  String? get proposalDescription {
+    final property =
+        document.document.getProperty(ProposalDocument.descriptionNodeId)
+            as DocumentValueProperty<String>?;
+
+    return property?.value;
+  }
+
+  int? get proposalDuration {
+    final property =
+        document.document.getProperty(ProposalDocument.durationNodeId)
+            as DocumentValueProperty<int>?;
+
+    return property?.value;
+  }
+
+  Coin? get proposalFundsRequested {
+    final property =
+        document.document.getProperty(ProposalDocument.requestedFundsNodeId)
+            as DocumentValueProperty<int>?;
+    final value = property?.value;
+    if (value == null) return null;
+    return Coin(value);
+  }
+
+  String? get proposalTitle {
+    final property = document.document.getProperty(ProposalDocument.titleNodeId)
+        as DocumentValueProperty<String>?;
+
+    return property?.value;
+  }
+
+  @override
+  List<Object?> get props => [
+        document,
+        ref,
+        categoryId,
+        commentsCount,
+        versions,
+      ];
+}
diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_enums.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_enums.dart
new file mode 100644
index 000000000000..81c58e889cef
--- /dev/null
+++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_enums.dart
@@ -0,0 +1,20 @@
+enum ProposalPublish {
+  /// A proposal has not yet been published,
+  /// it's stored only in the local storage.
+  localDraft,
+
+  /// A proposal has been published as a draft.
+  publishedDraft,
+
+  /// A proposal has been published and submitted into review.
+  ///
+  /// After the review stage is done the proposal will be live
+  /// with no additional proposer action.
+  submittedProposal;
+
+  bool get isDraft => this == publishedDraft;
+  bool get isPublished => this == submittedProposal;
+}
+
+// Note. This enum may be deleted later. Its here for backwards compatibility.
+enum ProposalStatus { ready, draft, inProgress, private, open, live, completed }
diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_pagination_items.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_pagination_items.dart
index c76aeb00ab4d..f8ab2027c7b0 100644
--- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_pagination_items.dart
+++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_pagination_items.dart
@@ -41,7 +41,7 @@ class ProposalPaginationItems<ProposalType> extends Equatable {
 
 class ProposalsSearchResult extends Equatable {
   final int maxResults;
-  final List<ProposalBase> proposals;
+  final List<Proposal> proposals;
 
   const ProposalsSearchResult({
     required this.maxResults,
diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/test/proposal/proposal_pagination_items_test.dart b/catalyst_voices/packages/internal/catalyst_voices_models/test/proposal/proposal_pagination_items_test.dart
index 64e5e8cef4b5..0ba1fe03f5fd 100644
--- a/catalyst_voices/packages/internal/catalyst_voices_models/test/proposal/proposal_pagination_items_test.dart
+++ b/catalyst_voices/packages/internal/catalyst_voices_models/test/proposal/proposal_pagination_items_test.dart
@@ -1,16 +1,18 @@
-import 'dart:math';
-
 import 'package:catalyst_cardano_serialization/src/types.dart';
 import 'package:catalyst_voices_models/catalyst_voices_models.dart';
 import 'package:catalyst_voices_shared/catalyst_voices_shared.dart';
 import 'package:test/test.dart';
+import 'package:uuid/uuid.dart';
 
 void main() {
-  late ProposalBase baseProposal;
+  late Proposal baseProposal;
 
   setUpAll(() {
-    baseProposal = ProposalBase(
-      id: '${Random().nextInt(1000)}/${Random().nextInt(1000)}',
+    baseProposal = Proposal(
+      selfRef: SignedDocumentRef(
+        id: const Uuid().v7(),
+        version: const Uuid().v7(),
+      ),
       category: 'Cardano Use Cases / MVP',
       title: 'Proposal Title that rocks the world',
       updateDate: DateTime.now().minusDays(2),
@@ -21,13 +23,13 @@ void main() {
       description: '',
       duration: 6,
       author: 'Alex Wells',
-      version: 1,
+      versionCount: 1,
     );
   });
 
   group(ProposalPaginationItems, () {
     test('creates instance with default values', () {
-      const pagination = ProposalPaginationItems<ProposalBase>();
+      const pagination = ProposalPaginationItems<Proposal>();
 
       expect(pagination.pageKey, equals(0));
       expect(pagination.maxResults, equals(0));
@@ -36,7 +38,7 @@ void main() {
 
     test('creates instance with custom values', () {
       final items = [baseProposal, baseProposal];
-      final pagination = ProposalPaginationItems<ProposalBase>(
+      final pagination = ProposalPaginationItems<Proposal>(
         pageKey: 1,
         maxResults: 10,
         items: items,
@@ -48,7 +50,7 @@ void main() {
     });
 
     test('copyWith creates new instance with updated values', () {
-      final original = ProposalPaginationItems<ProposalBase>(
+      final original = ProposalPaginationItems<Proposal>(
         pageKey: 1,
         maxResults: 10,
         items: [baseProposal],
@@ -67,13 +69,13 @@ void main() {
     });
 
     test('equals works correctly', () {
-      final pagination1 = ProposalPaginationItems<ProposalBase>(
+      final pagination1 = ProposalPaginationItems<Proposal>(
         pageKey: 1,
         maxResults: 10,
         items: [baseProposal],
       );
 
-      final pagination2 = ProposalPaginationItems<ProposalBase>(
+      final pagination2 = ProposalPaginationItems<Proposal>(
         pageKey: 1,
         maxResults: 10,
         items: [baseProposal],
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 4713646cb7e9..124875a3b23c 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,47 +5,78 @@ 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/typedefs.dart';
+import 'package:collection/collection.dart';
 import 'package:drift/drift.dart';
 import 'package:flutter/foundation.dart';
 
 /// Exposes only public operation on documents, and related, tables.
 abstract interface class DocumentsDao {
-  /// Similar to [queryAll] but emits when new records are inserted or deleted.
-  Stream<List<DocumentEntity>> watchAll();
-
-  /// Returns all entities. If same document have different versions
-  /// all will be returned.
-  Future<List<DocumentEntity>> queryAll();
-
-  /// If version is specified in [ref] returns this version or null.
-  /// Returns newest version with matching id or null of none found.
-  Future<DocumentEntity?> query({required DocumentRef ref});
-
-  /// Same as [query] but emits updates.
-  Stream<DocumentEntity?> watch({required DocumentRef ref});
-
-  /// Counts all documents.
-  Future<int> countAll();
-
   /// Counts documents matching required [ref] id and optional [ref] ver.
   ///
   /// If [ref] ver is not specified it will return count of all version
   /// matching [ref] id.
   Future<int> count({required DocumentRef ref});
 
+  /// Counts all documents.
+  Future<int> countAll();
+
   /// Counts unique documents. All versions of same document are counted as 1.
   Future<int> countDocuments();
 
   @visibleForTesting
   Future<int> countDocumentsMetadata();
 
+  /// Counts documents of specified [type]
+  /// that reference a given document [ref].
+  ///
+  /// [ref] is the reference to the parent document being referenced
+  /// [type] is the type of documents to count (e.g., comments, reactions, etc.)
+  ///
+  /// Returns the count of documents matching both the type and reference.
+  Future<int> countRefDocumentByType({
+    required DocumentRef ref,
+    required DocumentType type,
+  });
+
+  /// Deletes all documents. Cascades to metadata.
+  Future<void> deleteAll();
+
+  /// If version is specified in [ref] returns this version or null.
+  /// Returns newest version with matching id or null of none found.
+  Future<DocumentEntity?> query({required DocumentRef ref});
+
+  /// Returns all entities. If same document have different versions
+  /// all will be returned.
+  Future<List<DocumentEntity>> queryAll();
+
+  /// Returns a list of version of ref object.
+  /// Can be used to get versions count.
+  Future<List<String>> queryVersionIds({required String id});
+
   /// Inserts all documents and metadata. On conflicts ignores duplicates.
   Future<void> saveAll(
     Iterable<DocumentEntityWithMetadata> documentsWithMetadata,
   );
 
-  /// Deletes all documents. Cascades to metadata.
-  Future<void> deleteAll();
+  /// Same as [query] but emits updates.
+  Stream<DocumentEntity?> watch({required DocumentRef ref});
+
+  /// Similar to [queryAll] but emits when new records are inserted or deleted.
+  /// Returns all entities. If same document have different versions
+  /// all will be returned unless [unique] is true.
+  /// When [unique] is true, only latest versions of each document are returned.
+  /// Optional [limit] parameter limits the number of returned documents.
+  Stream<List<DocumentEntity>> watchAll({
+    bool unique = false,
+    int? limit,
+    DocumentType? type,
+  });
+
+  /// Watches for new comments that are reference by ref.
+  Stream<int> watchCount({
+    required DocumentRef ref,
+    required DocumentType type,
+  });
 }
 
 @DriftAccessor(
@@ -60,26 +91,8 @@ class DriftDocumentsDao extends DatabaseAccessor<DriftCatalystDatabase>
   DriftDocumentsDao(super.attachedDatabase);
 
   @override
-  Stream<List<DocumentEntity>> watchAll() {
-    return select(documents).watch();
-  }
-
-  @override
-  Future<List<DocumentEntity>> queryAll() {
-    return select(documents).get();
-  }
-
-  @override
-  Future<DocumentEntity?> query({required DocumentRef ref}) {
-    return _selectRef(ref).get().then((value) => value.firstOrNull);
-  }
-
-  @override
-  Stream<DocumentEntity?> watch({required DocumentRef ref}) {
-    return _selectRef(ref)
-        .watch()
-        .map((event) => event.firstOrNull)
-        .distinct(_entitiesEquals);
+  Future<int> count({required DocumentRef ref}) {
+    return documents.count(where: (row) => _filterRef(row, ref)).getSingle();
   }
 
   @override
@@ -87,11 +100,6 @@ class DriftDocumentsDao extends DatabaseAccessor<DriftCatalystDatabase>
     return documents.count().getSingle();
   }
 
-  @override
-  Future<int> count({required DocumentRef ref}) {
-    return documents.count(where: (row) => _filterRef(row, ref)).getSingle();
-  }
-
   @override
   Future<int> countDocuments() {
     final count = documents.idHi.count(distinct: true);
@@ -126,6 +134,58 @@ class DriftDocumentsDao extends DatabaseAccessor<DriftCatalystDatabase>
         .then((count) => count.firstOrNull ?? 0);
   }
 
+  @override
+  Future<int> countRefDocumentByType({
+    required DocumentRef ref,
+    required DocumentType type,
+  }) {
+    final query = select(documents)
+      ..where(
+        (row) => Expression.and([
+          row.type.equals(type.uuid),
+        ]),
+      );
+
+    return query.get().then(
+          (docs) => docs.where((doc) {
+            // TODO(damian-molinski): JSONB filter
+            return doc.metadata.ref == ref;
+          }).length,
+        );
+  }
+
+  @override
+  Future<void> deleteAll() async {
+    final deletedRows = await delete(documents).go();
+
+    if (kDebugMode) {
+      debugPrint('DocumentsDao: Deleted[$deletedRows] rows');
+    }
+  }
+
+  @override
+  Future<DocumentEntity?> query({required DocumentRef ref}) {
+    return _selectRef(ref).get().then((value) => value.firstOrNull);
+  }
+
+  @override
+  Future<List<DocumentEntity>> queryAll() {
+    return select(documents).get();
+  }
+
+  @override
+  Future<List<String>> queryVersionIds({required String id}) {
+    final query = select(documents)
+      ..where((tbl) => _filterRef(tbl, SignedDocumentRef(id: id)))
+      ..orderBy([
+        (u) => OrderingTerm.desc(u.verHi),
+      ]);
+
+    return query
+        .map((doc) => UuidHiLo(high: doc.verHi, low: doc.verLo).toString())
+        .get();
+  }
+
   @override
   Future<void> saveAll(
     Iterable<DocumentEntityWithMetadata> documentsWithMetadata,
@@ -149,23 +209,90 @@ class DriftDocumentsDao extends DatabaseAccessor<DriftCatalystDatabase>
   }
 
   @override
-  Future<void> deleteAll() async {
-    final deletedRows = await delete(documents).go();
+  Stream<DocumentEntity?> watch({required DocumentRef ref}) {
+    return _selectRef(ref)
+        .watch()
+        .map((event) => event.firstOrNull)
+        .distinct(_entitiesEquals);
+  }
 
-    if (kDebugMode) {
-      debugPrint('DocumentsDao: Deleted[$deletedRows] rows');
+  @override
+  Stream<List<DocumentEntity>> watchAll({
+    bool unique = false,
+    int? limit,
+    DocumentType? type,
+  }) {
+    final query = select(documents);
+
+    if (type != null) {
+      query.where((doc) => doc.type.equals(type.uuid));
     }
+
+    return query.watch().map((documents) {
+      if (unique) {
+        // Group by document ID and take latest version
+        final uniqueDocs = documents
+            .groupListsBy((doc) => '${doc.idHi}-${doc.idLo}')
+            .values
+            .map(
+              (versions) => versions.reduce((a, b) {
+                // Compare versions (higher version wins)
+                final compareHi = b.verHi.compareTo(a.verHi);
+                if (compareHi != 0) return compareHi > 0 ? b : a;
+                return b.verLo.compareTo(a.verLo) > 0 ? b : a;
+              }),
+            )
+            .toList()
+          ..sort((a, b) {
+            // Sort by version descending
+            final compareHi = b.verHi.compareTo(a.verHi);
+            if (compareHi != 0) return compareHi;
+            return b.verLo.compareTo(a.verLo);
+          });
+
+        if (limit != null) {
+          return uniqueDocs.take(limit).toList();
+        }
+        return uniqueDocs;
+      }
+
+      if (limit != null) {
+        return documents.take(limit).toList();
+      }
+      return documents;
+    });
   }
 
-  SimpleSelectStatement<$DocumentsTable, DocumentEntity> _selectRef(
-    DocumentRef ref,
-  ) {
-    return select(documents)
-      ..where((tbl) => _filterRef(tbl, ref))
-      ..orderBy([
-        (u) => OrderingTerm.desc(u.verHi),
-      ])
-      ..limit(1);
+  @override
+  Stream<int> watchCount({
+    required DocumentRef ref,
+    required DocumentType type,
+  }) {
+    final query = select(documents)
+      ..where(
+        (row) => Expression.and([
+          // TODO(damian-molinski): JSONB filtering
+          row.metadata.equalsValue(
+            DocumentDataMetadata(
+              type: type,
+              ref: ref,
+              selfRef: ref,
+            ),
+          ),
+        ]),
+      );
+
+    return query.watch().map((comments) => comments.length).distinct();
+  }
+
+  bool _entitiesEquals(DocumentEntity? previous, DocumentEntity? next) {
+    final previousId = (previous?.idHi, previous?.idLo);
+    final nextId = (next?.idHi, next?.idLo);
+
+    final previousVer = (previous?.verHi, previous?.verLo);
+    final nextVer = (next?.verHi, next?.verLo);
+
+    return previousId == nextId && previousVer == nextVer;
   }
 
   Expression<bool> _filterRef($DocumentsTable row, DocumentRef ref) {
@@ -182,13 +309,14 @@ class DriftDocumentsDao extends DatabaseAccessor<DriftCatalystDatabase>
     ]);
   }
 
-  bool _entitiesEquals(DocumentEntity? previous, DocumentEntity? next) {
-    final previousId = (previous?.idHi, previous?.idLo);
-    final nextId = (next?.idHi, next?.idLo);
-
-    final previousVer = (previous?.verHi, previous?.verLo);
-    final nextVer = (next?.verHi, next?.verLo);
-
-    return previousId == nextId && previousVer == nextVer;
+  SimpleSelectStatement<$DocumentsTable, DocumentEntity> _selectRef(
+    DocumentRef ref,
+  ) {
+    return select(documents)
+      ..where((tbl) => _filterRef(tbl, ref))
+      ..orderBy([
+        (u) => OrderingTerm.desc(u.verHi),
+      ])
+      ..limit(1);
   }
 }
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 6a4f077e175c..6a8fac72ac64 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
@@ -19,7 +19,7 @@ typedef DocumentsDataWithRefData = ({DocumentData data, DocumentData refData});
 abstract interface class DocumentRepository {
   factory DocumentRepository(
     DraftDataSource drafts,
-    DocumentDataLocalSource localDocuments,
+    SignedDocumentDataSource localDocuments,
     DocumentDataRemoteSource remoteDocuments,
   ) = DocumentRepositoryImpl;
 
@@ -73,6 +73,11 @@ abstract interface class DocumentRepository {
   /// Returns the reference to the imported document.
   Future<DocumentRef> importDocument({required Uint8List data});
 
+  /// Returns a list of version of ref object.
+  ///
+  /// Can be used to get versions count.
+  Future<List<String>> queryVersionIds({required String id});
+
   /// Updates local draft (or drafts if version is not specified)
   /// matching [ref] with given [content].
   ///
@@ -83,18 +88,28 @@ abstract interface class DocumentRepository {
     required DocumentDataContent content,
   });
 
+  Stream<int> watchCount({
+    required DocumentRef ref,
+    required DocumentType type,
+  });
+
   /// Observes matching [ProposalDocument] and emits updates.
   ///
   /// Source of data depends whether [ref] is [SignedDocumentRef] or [DraftRef].
   Stream<ProposalDocument> watchProposalDocument({
     required DocumentRef ref,
   });
+
+  Stream<List<ProposalDocument>> watchProposalsDocuments({
+    int? limit,
+    bool unique = false,
+  });
 }
 
 final class DocumentRepositoryImpl implements DocumentRepository {
   // ignore: unused_field
   final DraftDataSource _drafts;
-  final DocumentDataLocalSource _localDocuments;
+  final SignedDocumentDataSource _localDocuments;
   final DocumentDataRemoteSource _remoteDocuments;
 
   final _documentDataLock = Lock();
@@ -209,6 +224,11 @@ final class DocumentRepositoryImpl implements DocumentRepository {
     return newDocument.ref;
   }
 
+  @override
+  Future<List<String>> queryVersionIds({required String id}) {
+    return _localDocuments.queryVersionIds(id: id);
+  }
+
   @override
   Future<void> updateDocumentDraft({
     required DraftRef ref,
@@ -220,6 +240,41 @@ final class DocumentRepositoryImpl implements DocumentRepository {
     );
   }
 
+  Stream<List<DocumentsDataWithRefData>> watchAllDocuments({
+    int? limit,
+    bool unique = false,
+    DocumentType? type,
+  }) {
+    return _localDocuments
+        .watchAll(limit: limit, unique: unique, type: type)
+        .distinct()
+        .switchMap((documents) async* {
+      final results = await Future.wait(
+        documents
+            .where((doc) => doc.metadata.template != null)
+            .map((documentData) async {
+          final templateRef = documentData.metadata.template!;
+          final templateData = await _documentDataLock.synchronized(
+            () => getDocumentData(ref: templateRef),
+          );
+          return (data: documentData, refData: templateData);
+        }),
+      );
+      yield results;
+    });
+  }
+
+  @override
+  Stream<int> watchCount({
+    required DocumentRef ref,
+    required DocumentType type,
+  }) {
+    return _localDocuments.watchCount(
+      ref: ref,
+      type: type,
+    );
+  }
+
   @visibleForTesting
   Stream<DocumentsDataWithRefData?> watchDocumentWithRef({
     required DocumentRef ref,
@@ -274,6 +329,29 @@ final class DocumentRepositoryImpl implements DocumentRepository {
     );
   }
 
+  @override
+  Stream<List<ProposalDocument>> watchProposalsDocuments({
+    int? limit,
+    bool unique = false,
+  }) {
+    return watchAllDocuments(
+      limit: limit,
+      type: DocumentType.proposalDocument,
+    ).whereNotNull().map(
+          (documents) => documents.map(
+            (doc) {
+              final documentData = doc.data;
+              final templateData = doc.refData;
+
+              return _buildProposalDocument(
+                documentData: documentData,
+                templateData: templateData,
+              );
+            },
+          ).toList(),
+        );
+  }
+
   ProposalDocument _buildProposalDocument({
     required DocumentData documentData,
     required DocumentData templateData,
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 7d01bbee4c66..8e4f9c1b84d2 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
@@ -1,7 +1,7 @@
 import 'package:catalyst_voices_models/catalyst_voices_models.dart';
 import 'package:catalyst_voices_repositories/catalyst_voices_repositories.dart';
 
-final class DatabaseDocumentsDataSource implements DocumentDataLocalSource {
+final class DatabaseDocumentsDataSource implements SignedDocumentDataSource {
   final CatalystDatabase _database;
 
   DatabaseDocumentsDataSource(
@@ -24,10 +24,8 @@ final class DatabaseDocumentsDataSource implements DocumentDataLocalSource {
   }
 
   @override
-  Stream<DocumentData?> watch({required DocumentRef ref}) {
-    return _database.documentsDao
-        .watch(ref: ref)
-        .map((entity) => entity?.toModel());
+  Future<List<String>> queryVersionIds({required String id}) {
+    return _database.documentsDao.queryVersionIds(id: id);
   }
 
   @override
@@ -55,6 +53,32 @@ final class DatabaseDocumentsDataSource implements DocumentDataLocalSource {
 
     await _database.documentsDao.saveAll([documentWithMetadata]);
   }
+
+  @override
+  Stream<DocumentData?> watch({required DocumentRef ref}) {
+    return _database.documentsDao
+        .watch(ref: ref)
+        .map((entity) => entity?.toModel());
+  }
+
+  @override
+  Stream<List<DocumentData>> watchAll({
+    int? limit,
+    required bool unique,
+    DocumentType? type,
+  }) {
+    return _database.documentsDao
+        .watchAll(limit: limit, unique: unique, type: type)
+        .map((entities) => entities.map((e) => e.toModel()).toList());
+  }
+
+  @override
+  Stream<int> watchCount({
+    required DocumentRef ref,
+    required DocumentType type,
+  }) {
+    return _database.documentsDao.watchCount(ref: ref, type: type);
+  }
 }
 
 extension on DocumentEntity {
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 197e686590bc..cb8a43873f47 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
@@ -1,7 +1,6 @@
 import 'package:catalyst_voices_models/catalyst_voices_models.dart';
 import 'package:catalyst_voices_repositories/catalyst_voices_repositories.dart';
 
-/// See [DatabaseDocumentsDataSource].
 abstract interface class DocumentDataLocalSource implements DocumentDataSource {
   Future<bool> exists({required DocumentRef ref});
 
@@ -21,3 +20,18 @@ abstract interface class DraftDataSource implements DocumentDataLocalSource {
     required DocumentDataContent content,
   });
 }
+
+/// See [DatabaseDocumentsDataSource].
+abstract interface class SignedDocumentDataSource
+    implements DocumentDataLocalSource {
+  Future<List<String>> queryVersionIds({required String id});
+  Stream<List<DocumentData>> watchAll({
+    int? limit,
+    required bool unique,
+    DocumentType? type,
+  });
+  Stream<int> watchCount({
+    required DocumentRef ref,
+    required DocumentType type,
+  });
+}
diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/proposal/proposal_repository.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/proposal/proposal_repository.dart
index 2ab20be0f47e..7f36a8d531e3 100644
--- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/proposal/proposal_repository.dart
+++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/proposal/proposal_repository.dart
@@ -24,7 +24,6 @@ int _maxResults(ProposalPublish? stage) {
   return 32;
 }
 
-// ignore: one_member_abstracts
 abstract interface class ProposalRepository {
   const factory ProposalRepository() = ProposalRepositoryImpl;
 
@@ -32,7 +31,7 @@ abstract interface class ProposalRepository {
 
   Future<List<String>> getFavoritesProposalsIds();
 
-  Future<ProposalBase> getProposal({
+  Future<ProposalData> getProposal({
     required DocumentRef ref,
   });
 
@@ -62,22 +61,19 @@ final class ProposalRepositoryImpl implements ProposalRepository {
   }
 
   @override
-  Future<ProposalBase> getProposal({
+  Future<ProposalData> getProposal({
     required DocumentRef ref,
   }) async {
-    return ProposalBase(
-      id: ref.id,
-      category: 'Cardano Use Cases / MVP',
-      title: 'Proposal Title that rocks the world',
-      updateDate: DateTime.now().minusDays(2),
-      fundsRequested: Coin.fromAda(100000),
-      status: ProposalStatus.draft,
-      publish: ProposalPublish.localDraft,
-      commentsCount: 0,
-      description: _proposalDescription,
-      duration: 6,
-      author: 'Alex Wells',
-      version: 1,
+    return ProposalData(
+      categoryId: const Uuid().v7(),
+      document: ProposalDocument(
+        metadata: ProposalMetadata(selfRef: ref),
+        document: const Document(
+          properties: [],
+          schema: DocumentSchema.optional(),
+        ),
+      ),
+      ref: ref,
     );
   }
 
@@ -86,7 +82,7 @@ final class ProposalRepositoryImpl implements ProposalRepository {
     required ProposalPaginationRequest request,
   }) async {
     // optionally filter by status.
-    final proposals = <ProposalBase>[];
+    final proposals = <Proposal>[];
 
     // Return users proposals match his account id with proposals metadata from
     // author field.
@@ -102,8 +98,8 @@ final class ProposalRepositoryImpl implements ProposalRepository {
           ? ProposalPublish.submittedProposal
           : ProposalPublish.publishedDraft;
       proposals.add(
-        ProposalBase(
-          id: const Uuid().v7(),
+        Proposal(
+          selfRef: SignedDocumentRef.generateFirstRef(),
           category: 'Cardano Use Cases / MVP',
           title: 'Proposal Title that rocks the world',
           updateDate: DateTime.now().minusDays(2),
@@ -114,7 +110,7 @@ final class ProposalRepositoryImpl implements ProposalRepository {
           description: _proposalDescription,
           duration: 6,
           author: 'Alex Wells',
-          version: 1,
+          versionCount: 1,
         ),
       );
     }
@@ -141,7 +137,7 @@ final class ProposalRepositoryImpl implements ProposalRepository {
     ProposalPaginationRequest request,
   ) async {
     final favoritesIds = await getFavoritesProposalsIds();
-    final proposals = <ProposalBase>[];
+    final proposals = <Proposal>[];
     final range = PagingRange.calculateRange(
       pageKey: request.pageKey,
       itemsPerPage: request.pageSize,
@@ -155,7 +151,8 @@ final class ProposalRepositoryImpl implements ProposalRepository {
     }
     for (var i = range.from; i <= range.to; i++) {
       final ref = SignedDocumentRef(id: favoritesIds[i]);
-      final proposal = await getProposal(ref: ref);
+      final proposalData = await getProposal(ref: ref);
+      final proposal = Proposal.fromData(proposalData);
       proposals.add(proposal);
     }
 
@@ -169,7 +166,7 @@ final class ProposalRepositoryImpl implements ProposalRepository {
     ProposalPaginationRequest request,
   ) async {
     final userProposalsIds = await getUserProposalsIds('');
-    final proposals = <ProposalBase>[];
+    final proposals = <Proposal>[];
     final range = PagingRange.calculateRange(
       pageKey: request.pageKey,
       itemsPerPage: request.pageSize,
@@ -183,7 +180,8 @@ final class ProposalRepositoryImpl implements ProposalRepository {
     }
     for (var i = range.from; i <= range.to; i++) {
       final ref = SignedDocumentRef(id: userProposalsIds[i]);
-      final proposal = await getProposal(ref: ref);
+      final proposalData = await getProposal(ref: ref);
+      final proposal = Proposal.fromData(proposalData);
       proposals.add(proposal);
     }
 
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 6dcade5474ce..f0b0dc451639 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
@@ -1,6 +1,7 @@
 import 'package:catalyst_voices_models/catalyst_voices_models.dart';
 import 'package:catalyst_voices_repositories/src/database/catalyst_database.dart';
 import 'package:catalyst_voices_repositories/src/database/database.dart';
+import 'package:collection/collection.dart';
 import 'package:drift/drift.dart' show DatabaseConnection;
 import 'package:drift/native.dart';
 import 'package:flutter_test/flutter_test.dart';
@@ -187,6 +188,232 @@ void main() {
 
         expect(entity, isNull);
       });
+
+      test('Return latest unique documents', () async {
+        final id = const Uuid().v7();
+        final version = const Uuid().v7();
+        await Future<void>.delayed(const Duration(milliseconds: 1));
+        final version2 = const Uuid().v7();
+        final document = DocumentWithMetadataFactory.build(
+          metadata: DocumentDataMetadata(
+            type: DocumentType.proposalDocument,
+            selfRef: SignedDocumentRef(id: id, version: version),
+          ),
+        );
+        final document2 = DocumentWithMetadataFactory.build(
+          metadata: DocumentDataMetadata(
+            type: DocumentType.proposalDocument,
+            selfRef: SignedDocumentRef(id: id, version: version2),
+          ),
+        );
+        final documentsStream =
+            database.documentsDao.watchAll(unique: true).asBroadcastStream();
+
+        await database.documentsDao.saveAll([document]);
+        final firstEmission = await documentsStream.first;
+        await database.documentsDao.saveAll([document2]);
+        final secondEmission = await documentsStream.first;
+
+        expect(firstEmission, equals([document.document]));
+        expect(secondEmission, equals([document2.document]));
+        expect(secondEmission.length, equals(1));
+        expect(secondEmission.first.metadata.selfRef.version, equals(version2));
+      });
+
+      test('Returns latest document limited by quantity if provided', () async {
+        // Given
+        final documentsWithMetadata = List<DocumentEntityWithMetadata>.generate(
+          20,
+          (index) => DocumentWithMetadataFactory.build(),
+        );
+
+        final expectedDocuments = documentsWithMetadata
+            .map((e) => e.document)
+            .groupListsBy((doc) => '${doc.idHi}-${doc.idLo}')
+            .values
+            .map(
+              (versions) => versions.reduce((a, b) {
+                // Compare versions (higher version wins)
+                final compareHi = b.verHi.compareTo(a.verHi);
+                if (compareHi != 0) return compareHi > 0 ? b : a;
+                return b.verLo.compareTo(a.verLo) > 0 ? b : a;
+              }),
+            )
+            .toList()
+          ..sort((a, b) {
+            // Sort by version descending
+            final compareHi = b.verHi.compareTo(a.verHi);
+            if (compareHi != 0) return compareHi;
+            return b.verLo.compareTo(a.verLo);
+          });
+
+        final limitedExpectedDocuments = expectedDocuments.take(7).toList();
+
+        // When
+        final documentsStream =
+            database.documentsDao.watchAll(limit: 7, unique: true);
+
+        await database.documentsDao.saveAll(documentsWithMetadata);
+
+        // Then
+        expect(
+          documentsStream,
+          emitsInOrder([
+            equals(limitedExpectedDocuments),
+          ]),
+        );
+
+        expect(
+          limitedExpectedDocuments.length,
+          equals(7),
+          reason: 'should have 7 documents',
+        );
+
+        final uniqueIds =
+            limitedExpectedDocuments.map((d) => '${d.idHi}-${d.idLo}').toSet();
+        expect(
+          uniqueIds.length,
+          equals(limitedExpectedDocuments.length),
+          reason: 'should have unique document IDs',
+        );
+      });
+
+      test('returns latest version when document has more than 1 version',
+          () async {
+        final id = const Uuid().v7();
+        final v1 = const Uuid().v7();
+        await Future<void>.delayed(const Duration(milliseconds: 1));
+        final v2 = const Uuid().v7();
+
+        final documentsWithMetadata = [v1, v2].map((version) {
+          final metadata = DocumentDataMetadata(
+            type: DocumentType.proposalDocument,
+            selfRef: DocumentRefFactory.buildSigned(
+              id: id,
+              version: version,
+            ),
+          );
+          return DocumentWithMetadataFactory.build(metadata: metadata);
+        }).toList();
+
+        // When
+        final documentsStream =
+            database.documentsDao.watchAll(limit: 7, unique: true);
+
+        await database.documentsDao.saveAll(documentsWithMetadata);
+        // Then
+        expect(
+          documentsStream,
+          emitsInOrder([
+            predicate<List<DocumentEntity>>(
+              (documents) {
+                if (documents.length != 1) return false;
+                final doc = documents.first;
+                return doc.metadata.version == v2;
+              },
+              'should return document with version $v2',
+            ),
+          ]),
+        );
+      });
+
+      test('emits new version of recent document', () async {
+        // Generate base ID
+        final id = const Uuid().v7();
+
+        // Create versions with enforced order (v2 is newer than v1)
+        final v1 = const Uuid().v7();
+        // Wait a moment to ensure second UUID is newer
+        await Future<void>.delayed(const Duration(milliseconds: 1));
+        final v2 = const Uuid().v7();
+
+        final documentsWithMetadata = DocumentWithMetadataFactory.build(
+          metadata: DocumentDataMetadata(
+            type: DocumentType.proposalDocument,
+            selfRef: DocumentRefFactory.buildSigned(
+              id: id,
+              version: v1,
+            ),
+          ),
+        );
+
+        final newVersion = DocumentWithMetadataFactory.build(
+          metadata: DocumentDataMetadata(
+            type: DocumentType.proposalDocument,
+            selfRef: DocumentRefFactory.buildSigned(
+              id: id,
+              version: v2,
+            ),
+          ),
+        );
+
+        // When
+        final documentsStream = database.documentsDao
+            .watchAll(limit: 7, unique: true)
+            .asBroadcastStream();
+
+        // Save first version and wait for emission
+        await database.documentsDao.saveAll([documentsWithMetadata]);
+        final firstEmission = await documentsStream.first;
+
+        // Save second version and wait for emission
+        await database.documentsDao.saveAll([newVersion]);
+        final secondEmission = await documentsStream.first;
+
+        // Then verify both emissions
+        expect(firstEmission, equals([documentsWithMetadata.document]));
+        expect(secondEmission, equals([newVersion.document]));
+      });
+
+      test('emits new document when is inserted', () async {
+        // Generate base ID
+        final id1 = const Uuid().v7();
+
+        // Create versions with enforced order (v2 is newer than v1)
+        final v1 = const Uuid().v7();
+        // Wait a moment to ensure second UUID is newer
+        await Future<void>.delayed(const Duration(milliseconds: 1));
+        final id2 = const Uuid().v7();
+        final v2 = const Uuid().v7();
+
+        final document1 = DocumentWithMetadataFactory.build(
+          metadata: DocumentDataMetadata(
+            type: DocumentType.proposalDocument,
+            selfRef: DocumentRefFactory.buildSigned(
+              id: id1,
+              version: v1,
+            ),
+          ),
+        );
+
+        final document2 = DocumentWithMetadataFactory.build(
+          metadata: DocumentDataMetadata(
+            type: DocumentType.proposalDocument,
+            selfRef: DocumentRefFactory.buildSigned(
+              id: id2,
+              version: v2,
+            ),
+          ),
+        );
+
+        // When
+        final documentsStream = database.documentsDao
+            .watchAll(limit: 1, unique: true)
+            .asBroadcastStream();
+
+        await database.documentsDao.saveAll([document1]);
+        final firstEmission = await documentsStream.first;
+
+        await database.documentsDao.saveAll([document2]);
+        final secondEmission = await documentsStream.first;
+
+        // Then verify both emissions
+        expect(firstEmission, equals([document1.document]));
+        expect(
+          secondEmission,
+          equals([document2.document]),
+        );
+      });
     });
 
     group('count', () {
@@ -307,6 +534,154 @@ void main() {
 
         expect(count, 1);
       });
+
+      test('Counts comments for specific proposal document version', () async {
+        final proposalId = const Uuid().v7();
+        final versionId = const Uuid().v7();
+        final proposalRef = DocumentRefFactory.buildSigned(
+          id: proposalId,
+          version: versionId,
+        );
+        final proposal = DocumentWithMetadataFactory.build(
+          metadata: DocumentDataMetadata(
+            type: DocumentType.proposalDocument,
+            selfRef: proposalRef,
+          ),
+        );
+
+        await database.documentsDao.saveAll([proposal]);
+
+        final comments = List.generate(
+          10,
+          (index) => DocumentWithMetadataFactory.build(
+            metadata: DocumentDataMetadata(
+              type: DocumentType.commentTemplate,
+              selfRef: DocumentRefFactory.buildSigned(),
+              ref: proposalRef,
+            ),
+          ),
+        );
+        final otherComments = List.generate(
+          5,
+          (index) => DocumentWithMetadataFactory.build(
+            metadata: DocumentDataMetadata(
+              type: DocumentType.commentTemplate,
+              selfRef: DocumentRefFactory.buildSigned(),
+              ref: DocumentRefFactory.buildSigned(),
+            ),
+          ),
+        );
+        await database.documentsDao.saveAll([...comments, ...otherComments]);
+
+        final count = await database.documentsDao.countRefDocumentByType(
+          ref: proposalRef,
+          type: DocumentType.commentTemplate,
+        );
+
+        expect(count, equals(10));
+      });
+
+      test('Count versions of specific document', () async {
+        final proposalId = const Uuid().v7();
+        final versionId = const Uuid().v7();
+        final proposalRef = DocumentRefFactory.buildSigned(
+          id: proposalId,
+          version: versionId,
+        );
+        final proposal = DocumentWithMetadataFactory.build(
+          metadata: DocumentDataMetadata(
+            type: DocumentType.proposalDocument,
+            selfRef: proposalRef,
+          ),
+        );
+
+        await database.documentsDao.saveAll([proposal]);
+
+        final versions = List.generate(
+          10,
+          (index) {
+            return DocumentWithMetadataFactory.build(
+              metadata: DocumentDataMetadata(
+                type: DocumentType.proposalDocument,
+                selfRef: DocumentRefFactory.buildSigned(
+                  id: proposalId,
+                ),
+                ref: proposalRef,
+              ),
+            );
+          },
+        );
+
+        await database.documentsDao.saveAll(versions);
+
+        final ids = await database.documentsDao.queryVersionIds(
+          id: proposalId,
+        );
+
+        expect(ids.length, equals(11));
+      });
+
+      test(
+        'Watches comments count',
+        () async {
+          final proposalId = const Uuid().v7();
+          await Future<void>.delayed(const Duration(milliseconds: 1));
+          final versionId = const Uuid().v7();
+          final proposalId2 = const Uuid().v7();
+          final proposalRef = DocumentRefFactory.buildSigned(
+            id: proposalId,
+            version: versionId,
+          );
+          final proposal = DocumentWithMetadataFactory.build(
+            metadata: DocumentDataMetadata(
+              type: DocumentType.proposalDocument,
+              selfRef: proposalRef,
+            ),
+          );
+
+          final proposalRef2 = DocumentRefFactory.buildSigned(
+            id: proposalId2,
+            version: versionId,
+          );
+
+          await database.documentsDao.saveAll([proposal]);
+
+          final comments = List.generate(2, (index) {
+            return DocumentWithMetadataFactory.build(
+              metadata: DocumentDataMetadata(
+                type: DocumentType.commentTemplate,
+                selfRef: DocumentRefFactory.buildSigned(),
+                ref: proposalRef,
+              ),
+            );
+          });
+
+          final otherComment = DocumentWithMetadataFactory.build(
+            metadata: DocumentDataMetadata(
+              type: DocumentType.commentTemplate,
+              selfRef: DocumentRefFactory.buildSigned(),
+              ref: proposalRef2,
+            ),
+          );
+
+          await database.documentsDao.saveAll([comments.first, otherComment]);
+
+          final documentCount = database.documentsDao
+              .watchCount(ref: proposalRef, type: DocumentType.commentTemplate)
+              .asBroadcastStream();
+
+          final firstEmission = await documentCount.first;
+          // TODO(damian-molinski): JSONB filtering
+          // After proper filtering this test should pass
+          expect(firstEmission, equals(1));
+
+          // Save second comment and wait for update
+          await database.documentsDao.saveAll([comments.last]);
+          final secondEmission = await documentCount.first;
+          expect(secondEmission, equals(2));
+        },
+        skip: true,
+      );
     });
 
     group('delete all', () {
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 6d2a6eaf2fe3..61ff5419c5e6 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
@@ -20,7 +20,7 @@ void main() {
   late DriftCatalystDatabase database;
 
   late DraftDataSource draftsSource;
-  late DocumentDataLocalSource localDocuments;
+  late SignedDocumentDataSource localDocuments;
   late DocumentDataRemoteSource remoteDocuments;
 
   setUp(() {
@@ -361,6 +361,44 @@ void main() {
         ]),
       );
     });
+
+    test(
+      'watchProposalsDocuments returns correct model',
+      () async {
+        final templateRef = DocumentRefFactory.buildSigned();
+        final templateData = DocumentDataFactory.build(
+          selfRef: templateRef,
+          type: DocumentType.proposalTemplate,
+        );
+        const publicDraftContent =
+            DocumentDataContent({'title': 'My proposal'});
+        final publicDraftRef = DocumentRefFactory.buildDraft();
+        final publicDraftData = DocumentDataFactory.build(
+          type: DocumentType.proposalDocument,
+          selfRef: publicDraftRef,
+          template: templateData.ref,
+          content: publicDraftContent,
+        );
+
+        await localDocuments.save(data: templateData);
+        await localDocuments.save(data: publicDraftData);
+
+        final latestProposals = repository.watchAllDocuments();
+
+        expect(
+          latestProposals,
+          emitsInOrder([
+            predicate<List<DocumentsDataWithRefData>>((dataList) {
+              if (dataList.isEmpty) return false;
+              final data = dataList.first;
+              final isRef = data.data.ref == publicDraftRef;
+              final isContent = data.data.content == publicDraftContent;
+              return isRef && isContent;
+            }),
+          ]),
+        );
+      },
+    );
   });
 }
 
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 f2c1b019e989..7a53873f28eb 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
@@ -2,8 +2,8 @@ import 'dart:typed_data';
 
 import 'package:catalyst_voices_models/catalyst_voices_models.dart';
 import 'package:catalyst_voices_repositories/catalyst_voices_repositories.dart';
+import 'package:rxdart/rxdart.dart';
 
-// ignore: one_member_abstracts
 abstract interface class ProposalService {
   const factory ProposalService(
     ProposalRepository proposalRepository,
@@ -30,7 +30,7 @@ abstract interface class ProposalService {
   /// Fetches favorites proposals ids of the user
   Future<List<String>> getFavoritesProposalsIds();
 
-  Future<Proposal> getProposal({
+  Future<ProposalData> getProposal({
     required DocumentRef ref,
   });
 
@@ -68,6 +68,8 @@ abstract interface class ProposalService {
     required DraftRef ref,
     required DocumentDataContent content,
   });
+
+  Stream<List<Proposal>> watchLatestProposals({int? limit});
 }
 
 final class ProposalServiceImpl implements ProposalService {
@@ -107,11 +109,10 @@ final class ProposalServiceImpl implements ProposalService {
   }
 
   @override
-  Future<Proposal> getProposal({
+  Future<ProposalData> getProposal({
     required DocumentRef ref,
   }) async {
-    final proposalBase = await _proposalRepository.getProposal(ref: ref);
-    final proposal = await _buildProposal(proposalBase);
+    final proposal = await _proposalRepository.getProposal(ref: ref);
 
     return proposal;
   }
@@ -120,18 +121,14 @@ final class ProposalServiceImpl implements ProposalService {
   Future<ProposalPaginationItems<Proposal>> getProposals({
     required ProposalPaginationRequest request,
   }) async {
-    final proposalBases = await _proposalRepository.getProposals(
+    final proposals = await _proposalRepository.getProposals(
       request: request,
     );
 
-    final futures = proposalBases.proposals.map(_buildProposal);
-
-    final proposals = await Future.wait(futures);
-
     return ProposalPaginationItems(
-      items: proposals,
+      items: proposals.proposals,
       pageKey: request.pageKey,
-      maxResults: proposalBases.maxResults,
+      maxResults: proposals.maxResults,
     );
   }
 
@@ -185,11 +182,41 @@ final class ProposalServiceImpl implements ProposalService {
     );
   }
 
-  Future<Proposal> _buildProposal(ProposalBase base) async {
-    final proposalDocument = await _documentRepository.getProposalDocument(
-      ref: base.ref,
-    );
-
-    return base.toProposal(document: proposalDocument);
+  @override
+  Stream<List<Proposal>> watchLatestProposals({int? limit}) {
+    return _documentRepository
+        .watchProposalsDocuments(limit: limit)
+        .switchMap((documents) async* {
+      final proposalsStreams = await Future.wait(
+        documents.map((doc) async {
+          final versionIds = await _documentRepository.queryVersionIds(
+            id: doc.metadata.selfRef.id,
+          );
+
+          return _documentRepository
+              .watchCount(
+            ref: doc.metadata.selfRef,
+            type: DocumentType.commentTemplate,
+          )
+              .map((commentsCount) {
+            final proposalData = ProposalData(
+              document: doc,
+              categoryId: DocumentType.categoryParametersDocument.uuid,
+              versions: versionIds,
+              commentsCount: commentsCount,
+              ref: doc.metadata.selfRef,
+            );
+            return Proposal.fromData(proposalData);
+          });
+        }),
+      );
+
+      await for (final commentsUpdates in Rx.combineLatest(
+        proposalsStreams,
+        (List<Proposal> proposals) => proposals,
+      )) {
+        yield commentsUpdates;
+      }
+    });
   }
 }
diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/pubspec.yaml b/catalyst_voices/packages/internal/catalyst_voices_services/pubspec.yaml
index ff0f5464bf8d..c919b22ad117 100644
--- a/catalyst_voices/packages/internal/catalyst_voices_services/pubspec.yaml
+++ b/catalyst_voices/packages/internal/catalyst_voices_services/pubspec.yaml
@@ -24,6 +24,8 @@ dependencies:
   flutter_driver:
     sdk: flutter
   flutter_secure_storage: ^9.2.2
+  flutter_test:
+    sdk: flutter
   logging: ^1.3.0
   path: ^1.9.1
   rxdart: ^0.27.7
diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/test/src/proposal/proposal_service_test.dart b/catalyst_voices/packages/internal/catalyst_voices_services/test/src/proposal/proposal_service_test.dart
new file mode 100644
index 000000000000..cb2e5f779d6c
--- /dev/null
+++ b/catalyst_voices/packages/internal/catalyst_voices_services/test/src/proposal/proposal_service_test.dart
@@ -0,0 +1,214 @@
+// Create mocks
+import 'dart:async';
+
+import 'package:catalyst_voices_models/catalyst_voices_models.dart';
+import 'package:catalyst_voices_repositories/catalyst_voices_repositories.dart';
+import 'package:catalyst_voices_services/catalyst_voices_services.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:mocktail/mocktail.dart';
+import 'package:uuid/uuid.dart';
+
+void main() {
+  late MockDocumentRepository mockDocumentRepository;
+  late MockProposalRepository mockProposalRepository;
+  late ProposalService proposalService;
+
+  setUp(() {
+    mockDocumentRepository = MockDocumentRepository();
+    mockProposalRepository = MockProposalRepository();
+
+    proposalService =
+        ProposalService(mockProposalRepository, mockDocumentRepository);
+
+    registerFallbackValue(const SignedDocumentRef(id: 'fallback-id'));
+
+    // Add a default response for any watchProposalCommentsCount call
+    when(
+      () => mockDocumentRepository.watchCount(
+        ref: any(named: 'ref'),
+        type: DocumentType.commentTemplate,
+      ),
+    ).thenAnswer((_) => Stream.fromIterable([5]));
+  });
+
+  group(ProposalService, () {
+    test('watchLatestProposals returns correct proposals', () async {
+      const proposalTemplate = DocumentSchema(
+        parentSchemaUrl: '',
+        schemaSelfUrl: '',
+        title: '',
+        description: MarkdownData.empty,
+        properties: [],
+        order: [],
+      );
+
+      final proposalId1 = const Uuid().v7();
+      final versionId1 = const Uuid().v7();
+      await Future.delayed(const Duration(milliseconds: 1), () {});
+      final proposalId2 = const Uuid().v7();
+
+      final proposalData1 = ProposalDocument(
+        metadata: ProposalMetadata(
+          selfRef: SignedDocumentRef(
+            id: proposalId1,
+            version: versionId1,
+          ),
+        ),
+        document: const Document(
+          schema: proposalTemplate,
+          properties: [],
+        ),
+      );
+      final proposalData2 = ProposalDocument(
+        metadata: ProposalMetadata(
+          selfRef: SignedDocumentRef(
+            id: proposalId2,
+            version: versionId1,
+          ),
+        ),
+        document: const Document(
+          schema: proposalTemplate,
+          properties: [],
+        ),
+      );
+
+      // Setup repository responses
+      when(
+        () => mockDocumentRepository.watchProposalsDocuments(
+          limit: null,
+        ),
+      ).thenAnswer((_) => Stream.value([proposalData1, proposalData2]));
+
+      when(
+        () => mockDocumentRepository.queryVersionIds(
+          id: any(named: 'id'),
+        ),
+      ).thenAnswer((_) => Future.value([versionId1]));
+
+      when(
+        () => mockDocumentRepository.watchCount(
+          ref: any(named: 'ref'),
+          type: DocumentType.commentTemplate,
+        ),
+      ).thenAnswer((_) => Stream.fromIterable([5]));
+
+      // Execute
+      final proposals = await proposalService.watchLatestProposals().first;
+
+      // Verify
+      expect(proposals.length, equals(2));
+      verify(
+        () => mockDocumentRepository.watchProposalsDocuments(
+          limit: null,
+        ),
+      ).called(1);
+
+      verify(
+        () => mockDocumentRepository.queryVersionIds(
+          id: any(named: 'id'),
+        ),
+      ).called(2);
+
+      verify(
+        () => mockDocumentRepository.watchCount(
+          ref: any(named: 'ref'),
+          type: DocumentType.commentTemplate,
+        ),
+      ).called(2);
+    });
+
+    test(
+      'watchProposalsDocuments reacts to comments count changes',
+      () async {
+        // TODO(damian-molinski): JSONB filtering
+        const proposalTemplate = DocumentSchema(
+          parentSchemaUrl: '',
+          schemaSelfUrl: '',
+          title: '',
+          description: MarkdownData.empty,
+          properties: [],
+          order: [],
+        );
+
+        final proposalId1 = const Uuid().v7();
+        final proposalId2 = const Uuid().v7();
+        final versionId = const Uuid().v7();
+
+        final proposalData1 = ProposalDocument(
+          metadata: ProposalMetadata(
+            selfRef: SignedDocumentRef(
+              id: proposalId1,
+              version: versionId,
+            ),
+          ),
+          document: const Document(
+            schema: proposalTemplate,
+            properties: [],
+          ),
+        );
+
+        final proposalData2 = ProposalDocument(
+          metadata: ProposalMetadata(
+            selfRef: SignedDocumentRef(
+              id: proposalId1,
+              version: versionId,
+            ),
+          ),
+          document: const Document(
+            schema: proposalTemplate,
+            properties: [],
+          ),
+        );
+
+        final proposalsStream =
+            Stream.value([proposalData1, proposalData2]).asBroadcastStream();
+
+        when(
+          () => mockDocumentRepository.watchProposalsDocuments(
+            limit: null,
+          ),
+        ).thenAnswer((_) => proposalsStream);
+
+        when(
+          () => mockDocumentRepository.queryVersionIds(
+            id: any(named: 'id'),
+          ),
+        ).thenAnswer((_) => Future.value([versionId]));
+
+        final commentsStream1 =
+            Stream.fromIterable([5, 10]).asBroadcastStream();
+        final commentsStream2 = Stream.fromIterable([3, 7]).asBroadcastStream();
+
+        when(
+          () => mockDocumentRepository.watchCount(
+            ref: SignedDocumentRef(id: proposalId1),
+            type: DocumentType.commentTemplate,
+          ),
+        ).thenAnswer((_) => commentsStream1);
+
+        when(
+          () => mockDocumentRepository.watchCount(
+            ref: SignedDocumentRef(id: proposalId2),
+            type: DocumentType.commentTemplate,
+          ),
+        ).thenAnswer((_) => commentsStream2);
+
+        await expectLater(
+          proposalService.watchLatestProposals().map(
+                (proposals) => proposals.map((p) => p.commentsCount).toList(),
+              ),
+          emitsInOrder([
+            [5, 3],
+            [5, 7],
+            [10, 7],
+          ]),
+        );
+      },
+      skip: true,
+    );
+  });
+}
+
+class MockDocumentRepository extends Mock implements DocumentRepository {}
+
+class MockProposalRepository extends Mock implements ProposalRepository {}
diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/proposal/proposal_view_model.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/proposal/proposal_view_model.dart
index d65248ec198b..21bdb4bf8268 100644
--- a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/proposal/proposal_view_model.dart
+++ b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/proposal/proposal_view_model.dart
@@ -31,7 +31,7 @@ final class FundedProposal extends ProposalViewModel {
     Proposal proposal, {
     required String campaignName,
   }) : this(
-          id: proposal.id,
+          id: proposal.selfRef.id,
           campaignName: campaignName,
           category: proposal.category,
           title: proposal.title,
@@ -91,7 +91,7 @@ final class PendingProposal extends ProposalViewModel {
   final String campaignName;
   final String category;
   final String title;
-  final DateTime lastUpdateDate;
+  final DateTime? lastUpdateDate;
   final Coin _fundsRequested;
   final int commentsCount;
   final String description;
@@ -143,7 +143,7 @@ and PRISM, but its potential is only barely exploited.
     Proposal proposal, {
     required String campaignName,
   }) : this(
-          id: proposal.id,
+          id: proposal.selfRef.id,
           campaignName: campaignName,
           category: proposal.category,
           title: proposal.title,
@@ -152,7 +152,7 @@ and PRISM, but its potential is only barely exploited.
           commentsCount: proposal.commentsCount,
           description: proposal.description,
           publishStage: proposal.publish,
-          version: proposal.version,
+          version: proposal.versionCount,
           duration: proposal.duration,
           author: proposal.author,
         );
diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/session/session_account.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/session/session_account.dart
index 7cc6491490bc..edada81a27d2 100644
--- a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/session/session_account.dart
+++ b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/session/session_account.dart
@@ -16,14 +16,6 @@ final class SessionAccount extends Equatable {
     this.isDrep = false,
   });
 
-  const SessionAccount.mocked()
-      : this(
-          catalystId: 'cardano/uuid',
-          displayName: 'Account Mocked',
-          isAdmin: true,
-          isProposer: true,
-        );
-
   factory SessionAccount.fromAccount(Account account) {
     return SessionAccount(
       catalystId: account.catalystId,
@@ -34,6 +26,14 @@ final class SessionAccount extends Equatable {
     );
   }
 
+  const SessionAccount.mocked()
+      : this(
+          catalystId: 'cardano/uuid',
+          displayName: 'Account Mocked',
+          isAdmin: true,
+          isProposer: true,
+        );
+
   @override
   List<Object?> get props => [
         catalystId,


Reviews:
user: login: damian-molinski
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-28T11:39:58Z
user: login: dt-iohk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-28T12:46:44Z
user: login: dt-iohk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T11:26:12Z
user: login: dt-iohk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T11:28:18Z
user: login: dt-iohk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T11:30:17Z
user: login: dt-iohk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T11:32:07Z
user: login: damian-molinski
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T08:24:05Z
user: login: damian-molinski
body: LGTM
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T09:00:32Z

Review comments:
diff_hunk: @@ -11,7 +13,18 @@ part 'discovery_state.dart';
 class DiscoveryCubit extends Cubit<DiscoveryState> {
   // ignore: unused_field
   final CampaignService _campaignService;
-  DiscoveryCubit(this._campaignService) : super(const DiscoveryState());
+  final ProposalService _proposalService;
+  late StreamSubscription<List<Proposal>> _proposalsSubscription;
path: catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/discovery/discovery_cubit.dart
user: login: damian-molinski
body: make it nullable pls
created_at: 2025-02-28T10:56:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4,7 +4,7 @@ import 'package:equatable/equatable.dart';
 
 final class ProposalBuilderMetadata extends Equatable {
   final ProposalPublish publish;
-  final DocumentRef? documentRef;
+  final ProposalMetadata? documentRef;
path: catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposal_builder/proposal_builder_state.dart
user: login: damian-molinski
body: naming problem?
created_at: 2025-02-28T10:56:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -14,5 +14,6 @@ final class DocumentDataContent extends Equatable {
   List<Object?> get props => [data];
 
   String? get title =>
-      DocumentNodeTraverser.getValue(Proposal.titleNodeId, data) as String?;
+      DocumentNodeTraverser.getValue(ProposalDocument.titleNodeId, data)
+          as String?;
path: catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/data/document_data_content.dart
user: login: damian-molinski
body: it clearly does not fit into single like, split it please. 

```dart
String? get title {
      final value = DocumentNodeTraverser.getValue(ProposalDocument.titleNodeId, data);
      return value as String?;
}

```
created_at: 2025-02-28T10:58:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3,6 +3,23 @@ import 'package:catalyst_voices_models/src/document/document_metadata.dart';
 import 'package:equatable/equatable.dart';
 
 final class ProposalDocument extends Equatable {
+  /// A hardcoded [NodeId] of the title property.
+  ///
+  /// Since properties are dynamic the application cannot determine
+  /// which property is the title in any other way than
+  /// by hardcoding it's node ID.
+  static final titleNodeId = DocumentNodeId.fromString('setup.title.title');
+  static final descriptionNodeId =
+      DocumentNodeId.fromString('summary.solution.summary');
+
+  static final requestedFundsNodeId =
+      DocumentNodeId.fromString('summary.budget.requestedFunds');
+
+  static final durationNodeId =
+      DocumentNodeId.fromString('summary.time.duration');
+  static final authorNodeId =
path: catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/specialized/proposal_document.dart
user: login: damian-molinski
body: authorNameNodeId
created_at: 2025-02-28T10:59:06Z
author_association: CONTRIBUTOR
path: catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/specialized/proposal_document.dart
user: login: damian-molinski
body: why do you have new lines only between some of those NodeIds ? 
created_at: 2025-02-28T10:59:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,34 +1,90 @@
+import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart';
 import 'package:catalyst_voices_models/catalyst_voices_models.dart';
+import 'package:equatable/equatable.dart';
 
-final class Proposal extends ProposalBase {
-  /// A hardcoded [NodeId] of the title property.
-  ///
-  /// Since properties are dynamic the application cannot determine
-  /// which property is the title in any other way than
-  /// by hardcoding it's node ID.
-  static final titleNodeId = DocumentNodeId.fromString('setup.title.title');
-
-  final ProposalDocument document;
+final class Proposal extends Equatable {
+  final String id;
+  final String version;
+  final String title;
+  final String description;
+  final DateTime updateDate;
+  final DateTime? fundedDate;
+  final Coin fundsRequested;
+  final ProposalStatus status;
+  final ProposalPublish publish;
+  final int versionCount;
+  final int duration;
+  final String author;
+  final int commentsCount;
+  final String category;
 
   const Proposal({
-    required super.id,
-    required super.title,
-    required super.description,
-    required super.updateDate,
-    required super.fundsRequested,
-    required super.status,
-    required super.publish,
-    required super.category,
-    required super.commentsCount,
-    required this.document,
-    required super.version,
-    required super.duration,
-    required super.author,
+    required this.id,
+    required this.version,
+    required this.title,
+    required this.description,
+    required this.updateDate,
+    this.fundedDate,
+    required this.fundsRequested,
+    required this.status,
+    required this.publish,
+    required this.versionCount,
+    required this.duration,
+    required this.author,
+    required this.commentsCount,
+    required this.category,
   });
 
+  factory Proposal.dummy(String id, String version) => Proposal(
path: catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal.dart
user: login: damian-molinski
body: use ref
created_at: 2025-02-28T11:00:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,34 +1,90 @@
+import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart';
 import 'package:catalyst_voices_models/catalyst_voices_models.dart';
+import 'package:equatable/equatable.dart';
 
-final class Proposal extends ProposalBase {
-  /// A hardcoded [NodeId] of the title property.
-  ///
-  /// Since properties are dynamic the application cannot determine
-  /// which property is the title in any other way than
-  /// by hardcoding it's node ID.
-  static final titleNodeId = DocumentNodeId.fromString('setup.title.title');
-
-  final ProposalDocument document;
+final class Proposal extends Equatable {
+  final String id;
+  final String version;
+  final String title;
+  final String description;
+  final DateTime updateDate;
+  final DateTime? fundedDate;
+  final Coin fundsRequested;
+  final ProposalStatus status;
+  final ProposalPublish publish;
+  final int versionCount;
+  final int duration;
+  final String author;
+  final int commentsCount;
+  final String category;
 
   const Proposal({
-    required super.id,
-    required super.title,
-    required super.description,
-    required super.updateDate,
-    required super.fundsRequested,
-    required super.status,
-    required super.publish,
-    required super.category,
-    required super.commentsCount,
-    required this.document,
-    required super.version,
-    required super.duration,
-    required super.author,
+    required this.id,
+    required this.version,
+    required this.title,
+    required this.description,
+    required this.updateDate,
+    this.fundedDate,
+    required this.fundsRequested,
+    required this.status,
+    required this.publish,
+    required this.versionCount,
+    required this.duration,
+    required this.author,
+    required this.commentsCount,
+    required this.category,
   });
 
+  factory Proposal.dummy(String id, String version) => Proposal(
+        id: id,
+        version: version,
+        category: 'Cardano Use Cases / MVP',
+        title: 'Dummy Proposal',
+        updateDate: DateTime.now(),
+        fundsRequested: Coin.fromAda(100000),
+        status: ProposalStatus.draft,
+        publish: ProposalPublish.localDraft,
+        commentsCount: 0,
+        description: 'Dummy description',
+        duration: 6,
+        author: 'Alex Wells',
+        versionCount: 1,
+      );
+
+  factory Proposal.fromData(ProposalData data) {
+    final updateDate = UuidV7.parseTimestamp(data.document.metadata.version);
path: catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal.dart
user: login: damian-molinski
body: add TODO to change it to use this one https://github.com/input-output-hk/catalyst-voices/pull/1928
created_at: 2025-02-28T11:01:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,34 +1,90 @@
+import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart';
 import 'package:catalyst_voices_models/catalyst_voices_models.dart';
+import 'package:equatable/equatable.dart';
 
-final class Proposal extends ProposalBase {
-  /// A hardcoded [NodeId] of the title property.
-  ///
-  /// Since properties are dynamic the application cannot determine
-  /// which property is the title in any other way than
-  /// by hardcoding it's node ID.
-  static final titleNodeId = DocumentNodeId.fromString('setup.title.title');
-
-  final ProposalDocument document;
+final class Proposal extends Equatable {
+  final String id;
+  final String version;
+  final String title;
+  final String description;
+  final DateTime updateDate;
+  final DateTime? fundedDate;
+  final Coin fundsRequested;
+  final ProposalStatus status;
+  final ProposalPublish publish;
+  final int versionCount;
+  final int duration;
+  final String author;
+  final int commentsCount;
+  final String category;
 
   const Proposal({
-    required super.id,
-    required super.title,
-    required super.description,
-    required super.updateDate,
-    required super.fundsRequested,
-    required super.status,
-    required super.publish,
-    required super.category,
-    required super.commentsCount,
-    required this.document,
-    required super.version,
-    required super.duration,
-    required super.author,
+    required this.id,
+    required this.version,
+    required this.title,
+    required this.description,
+    required this.updateDate,
+    this.fundedDate,
+    required this.fundsRequested,
+    required this.status,
+    required this.publish,
+    required this.versionCount,
+    required this.duration,
+    required this.author,
+    required this.commentsCount,
+    required this.category,
   });
 
+  factory Proposal.dummy(String id, String version) => Proposal(
+        id: id,
+        version: version,
+        category: 'Cardano Use Cases / MVP',
+        title: 'Dummy Proposal',
+        updateDate: DateTime.now(),
+        fundsRequested: Coin.fromAda(100000),
+        status: ProposalStatus.draft,
+        publish: ProposalPublish.localDraft,
+        commentsCount: 0,
+        description: 'Dummy description',
+        duration: 6,
+        author: 'Alex Wells',
+        versionCount: 1,
+      );
+
+  factory Proposal.fromData(ProposalData data) {
+    final updateDate = UuidV7.parseTimestamp(data.document.metadata.version);
+    return Proposal(
+      id: data.document.metadata.id,
+      version: data.document.metadata.version,
+      title: data.proposalTitle,
+      description: data.proposalDescription,
+      updateDate: updateDate,
+      fundsRequested: data.proposalFundsRequested,
+      // TODO(LynxLynxx): from here we need to get the real status
+      status: ProposalStatus.inProgress,
+      // TODO(LynxLynxx): from here we need to get the real publish
+      publish: ProposalPublish.publishedDraft,
+      versionCount: data.versions.length,
+      duration: data.proposalDuration,
+      author: data.proposalAuthor,
+      commentsCount: data.commentsCount,
+      category: data.categoryId,
+    );
+  }
+
   @override
   List<Object?> get props => [
-        ...super.props,
-        document,
+        id,
+        title,
+        description,
+        updateDate,
+        fundedDate,
+        fundsRequested,
+        status,
+        publish,
+        category,
+        commentsCount,
       ];
+
+  ProposalMetadata get ref => ProposalMetadata(id: id, version: version);
path: catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal.dart
user: login: damian-molinski
body: smth is off here
created_at: 2025-02-28T11:01:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,34 +1,90 @@
+import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart';
 import 'package:catalyst_voices_models/catalyst_voices_models.dart';
+import 'package:equatable/equatable.dart';
 
-final class Proposal extends ProposalBase {
-  /// A hardcoded [NodeId] of the title property.
-  ///
-  /// Since properties are dynamic the application cannot determine
-  /// which property is the title in any other way than
-  /// by hardcoding it's node ID.
-  static final titleNodeId = DocumentNodeId.fromString('setup.title.title');
-
-  final ProposalDocument document;
+final class Proposal extends Equatable {
+  final String id;
+  final String version;
path: catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal.dart
user: login: damian-molinski
body: replace with `selfRef`
created_at: 2025-02-28T11:02:24Z
author_association: CONTRIBUTOR
path: catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_data.dart
user: login: damian-molinski
body: Can we make all of those getters nullable?
created_at: 2025-02-28T11:03:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -10,42 +10,55 @@ import 'package:flutter/foundation.dart';
 
 /// Exposes only public operation on documents, and related, tables.
 abstract interface class DocumentsDao {
-  /// Similar to [queryAll] but emits when new records are inserted or deleted.
-  Stream<List<DocumentEntity>> watchAll();
-
-  /// Returns all entities. If same document have different versions
-  /// all will be returned.
-  Future<List<DocumentEntity>> queryAll();
-
-  /// If version is specified in [ref] returns this version or null.
-  /// Returns newest version with matching id or null of none found.
-  Future<DocumentEntity?> query({required DocumentRef ref});
-
-  /// Same as [query] but emits updates.
-  Stream<DocumentEntity?> watch({required DocumentRef ref});
-
-  /// Counts all documents.
-  Future<int> countAll();
-
   /// Counts documents matching required [ref] id and optional [ref] ver.
   ///
   /// If [ref] ver is not specified it will return count of all version
   /// matching [ref] id.
   Future<int> count({required DocumentRef ref});
 
+  /// Counts all documents.
+  Future<int> countAll();
+
+  /// Count how many comments are attached to proposal document version.
+  Future<int> countComments({required DocumentRef ref});
+
   /// Counts unique documents. All versions of same document are counted as 1.
   Future<int> countDocuments();
 
   @visibleForTesting
   Future<int> countDocumentsMetadata();
 
+  /// Deletes all documents. Cascades to metadata.
+  Future<void> deleteAll();
+
+  /// Returns a list of version of ref object.
+  /// Can be used to get versions count.
+  Future<List<String>> documentVersionIds({required DocumentRef ref});
+
+  /// If version is specified in [ref] returns this version or null.
+  /// Returns newest version with matching id or null of none found.
+  Future<DocumentEntity?> query({required DocumentRef ref});
+
+  /// Returns all entities. If same document have different versions
+  /// all will be returned.
+  Future<List<DocumentEntity>> queryAll();
+
   /// Inserts all documents and metadata. On conflicts ignores duplicates.
   Future<void> saveAll(
     Iterable<DocumentEntityWithMetadata> documentsWithMetadata,
   );
 
-  /// Deletes all documents. Cascades to metadata.
-  Future<void> deleteAll();
+  /// Same as [query] but emits updates.
+  Stream<DocumentEntity?> watch({required DocumentRef ref});
+
+  /// Similar to [queryAll] but emits when new records are inserted or deleted.
+  Stream<List<DocumentEntity>> watchAll();
+
+  /// Watches for new comments that are reference by ref.
+  Stream<int> watchDocumentCommentsCount({required DocumentRef ref});
+
+  /// Emits latest documents limited by quantity if provided
+  Stream<List<DocumentEntity>> watchLatestVersions({int? limit});
path: catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/dao/documents_dao.dart
user: login: damian-molinski
body: I think it should be just `watchAll` with optional `limit` and maybe `unique` with explanation that it means that ids will be distinct 
created_at: 2025-02-28T11:07:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -10,42 +10,55 @@ import 'package:flutter/foundation.dart';
 
 /// Exposes only public operation on documents, and related, tables.
 abstract interface class DocumentsDao {
-  /// Similar to [queryAll] but emits when new records are inserted or deleted.
-  Stream<List<DocumentEntity>> watchAll();
-
-  /// Returns all entities. If same document have different versions
-  /// all will be returned.
-  Future<List<DocumentEntity>> queryAll();
-
-  /// If version is specified in [ref] returns this version or null.
-  /// Returns newest version with matching id or null of none found.
-  Future<DocumentEntity?> query({required DocumentRef ref});
-
-  /// Same as [query] but emits updates.
-  Stream<DocumentEntity?> watch({required DocumentRef ref});
-
-  /// Counts all documents.
-  Future<int> countAll();
-
   /// Counts documents matching required [ref] id and optional [ref] ver.
   ///
   /// If [ref] ver is not specified it will return count of all version
   /// matching [ref] id.
   Future<int> count({required DocumentRef ref});
 
+  /// Counts all documents.
+  Future<int> countAll();
+
+  /// Count how many comments are attached to proposal document version.
+  Future<int> countComments({required DocumentRef ref});
+
   /// Counts unique documents. All versions of same document are counted as 1.
   Future<int> countDocuments();
 
   @visibleForTesting
   Future<int> countDocumentsMetadata();
 
+  /// Deletes all documents. Cascades to metadata.
+  Future<void> deleteAll();
+
+  /// Returns a list of version of ref object.
+  /// Can be used to get versions count.
+  Future<List<String>> documentVersionIds({required DocumentRef ref});
+
+  /// If version is specified in [ref] returns this version or null.
+  /// Returns newest version with matching id or null of none found.
+  Future<DocumentEntity?> query({required DocumentRef ref});
+
+  /// Returns all entities. If same document have different versions
+  /// all will be returned.
+  Future<List<DocumentEntity>> queryAll();
+
   /// Inserts all documents and metadata. On conflicts ignores duplicates.
   Future<void> saveAll(
     Iterable<DocumentEntityWithMetadata> documentsWithMetadata,
   );
 
-  /// Deletes all documents. Cascades to metadata.
-  Future<void> deleteAll();
+  /// Same as [query] but emits updates.
+  Stream<DocumentEntity?> watch({required DocumentRef ref});
+
+  /// Similar to [queryAll] but emits when new records are inserted or deleted.
+  Stream<List<DocumentEntity>> watchAll();
+
+  /// Watches for new comments that are reference by ref.
+  Stream<int> watchDocumentCommentsCount({required DocumentRef ref});
path: catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/dao/documents_dao.dart
user: login: damian-molinski
body: `watchCount` with `type` as parameter, it will be more generic and reusable. Maybe even make `ref` optional but maybe it does not make sense 
created_at: 2025-02-28T11:08:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -10,42 +10,55 @@ import 'package:flutter/foundation.dart';
 
 /// Exposes only public operation on documents, and related, tables.
 abstract interface class DocumentsDao {
-  /// Similar to [queryAll] but emits when new records are inserted or deleted.
-  Stream<List<DocumentEntity>> watchAll();
-
-  /// Returns all entities. If same document have different versions
-  /// all will be returned.
-  Future<List<DocumentEntity>> queryAll();
-
-  /// If version is specified in [ref] returns this version or null.
-  /// Returns newest version with matching id or null of none found.
-  Future<DocumentEntity?> query({required DocumentRef ref});
-
-  /// Same as [query] but emits updates.
-  Stream<DocumentEntity?> watch({required DocumentRef ref});
-
-  /// Counts all documents.
-  Future<int> countAll();
-
   /// Counts documents matching required [ref] id and optional [ref] ver.
   ///
   /// If [ref] ver is not specified it will return count of all version
   /// matching [ref] id.
   Future<int> count({required DocumentRef ref});
 
+  /// Counts all documents.
+  Future<int> countAll();
+
+  /// Count how many comments are attached to proposal document version.
+  Future<int> countComments({required DocumentRef ref});
+
   /// Counts unique documents. All versions of same document are counted as 1.
   Future<int> countDocuments();
 
   @visibleForTesting
   Future<int> countDocumentsMetadata();
 
+  /// Deletes all documents. Cascades to metadata.
+  Future<void> deleteAll();
+
+  /// Returns a list of version of ref object.
+  /// Can be used to get versions count.
+  Future<List<String>> documentVersionIds({required DocumentRef ref});
path: catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/dao/documents_dao.dart
user: login: damian-molinski
body: `Future<List<String>> queryVersionIds({required String id})`

version does nothing here
created_at: 2025-02-28T11:10:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -126,6 +128,44 @@ class DriftDocumentsDao extends DatabaseAccessor<DriftCatalystDatabase>
         .then((count) => count.firstOrNull ?? 0);
   }
 
+  @override
+  Future<void> deleteAll() async {
+    final deletedRows = await delete(documents).go();
+
+    if (kDebugMode) {
+      debugPrint('DocumentsDao: Deleted[$deletedRows] rows');
+    }
+  }
+
+  @override
+  Future<List<String>> documentVersionIds({required DocumentRef ref}) {
+    final id = UuidHiLo.from(ref.id);
+
+    return (select(documents)
+          ..where(
+            (tbl) => Expression.and([
+              tbl.idHi.equals(id.high),
+              tbl.idLo.equals(id.low),
+            ]),
+          )
+          ..orderBy([
+            (u) => OrderingTerm.desc(u.verHi),
+            (u) => OrderingTerm.desc(u.verLo),
+          ]))
+        .map((doc) => UuidHiLo(high: doc.verHi, low: doc.verLo).toString())
+        .get();
path: catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/dao/documents_dao.dart
user: login: damian-molinski
body: please extract select into variable so its easier to read. Have a look at `countDocuments` for reference.
created_at: 2025-02-28T11:12:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -126,6 +128,44 @@ class DriftDocumentsDao extends DatabaseAccessor<DriftCatalystDatabase>
         .then((count) => count.firstOrNull ?? 0);
   }
 
+  @override
+  Future<void> deleteAll() async {
+    final deletedRows = await delete(documents).go();
+
+    if (kDebugMode) {
+      debugPrint('DocumentsDao: Deleted[$deletedRows] rows');
+    }
+  }
+
+  @override
+  Future<List<String>> documentVersionIds({required DocumentRef ref}) {
+    final id = UuidHiLo.from(ref.id);
+
+    return (select(documents)
+          ..where(
+            (tbl) => Expression.and([
+              tbl.idHi.equals(id.high),
+              tbl.idLo.equals(id.low),
+            ]),
+          )
path: catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/dao/documents_dao.dart
user: login: damian-molinski
body: there is already `_filterRef`. Please use it
created_at: 2025-02-28T11:14:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -126,6 +128,44 @@ class DriftDocumentsDao extends DatabaseAccessor<DriftCatalystDatabase>
         .then((count) => count.firstOrNull ?? 0);
   }
 
+  @override
+  Future<void> deleteAll() async {
+    final deletedRows = await delete(documents).go();
+
+    if (kDebugMode) {
+      debugPrint('DocumentsDao: Deleted[$deletedRows] rows');
+    }
+  }
+
+  @override
+  Future<List<String>> documentVersionIds({required DocumentRef ref}) {
+    final id = UuidHiLo.from(ref.id);
+
+    return (select(documents)
+          ..where(
+            (tbl) => Expression.and([
+              tbl.idHi.equals(id.high),
+              tbl.idLo.equals(id.low),
+            ]),
+          )
+          ..orderBy([
+            (u) => OrderingTerm.desc(u.verHi),
+            (u) => OrderingTerm.desc(u.verLo),
path: catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/dao/documents_dao.dart
user: login: damian-molinski
body: `verLo` have random values. Timestamp is only in `verHi` so you should order only by it
created_at: 2025-02-28T11:15:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -88,8 +83,15 @@ class DriftDocumentsDao extends DatabaseAccessor<DriftCatalystDatabase>
   }
 
   @override
-  Future<int> count({required DocumentRef ref}) {
-    return documents.count(where: (row) => _filterRef(row, ref)).getSingle();
+  Future<int> countComments({required DocumentRef ref}) {
+    return (select(documents)
+          ..where((row) => row.type.equals(DocumentType.commentTemplate.uuid)))
+        .get()
+        .then(
+          (docs) => docs.where((doc) {
+            return doc.metadata.ref == ref;
+          }).length,
+        );
path: catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/dao/documents_dao.dart
user: login: damian-molinski
body: 1. Extract select statement into variable
2. Use where for filtering not after you queries all comments. This is a waste  
created_at: 2025-02-28T11:17:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -149,23 +189,84 @@ class DriftDocumentsDao extends DatabaseAccessor<DriftCatalystDatabase>
   }
 
   @override
-  Future<void> deleteAll() async {
-    final deletedRows = await delete(documents).go();
+  Stream<DocumentEntity?> watch({required DocumentRef ref}) {
+    return _selectRef(ref)
+        .watch()
+        .map((event) => event.firstOrNull)
+        .distinct(_entitiesEquals);
+  }
 
-    if (kDebugMode) {
-      debugPrint('DocumentsDao: Deleted[$deletedRows] rows');
-    }
+  @override
+  Stream<List<DocumentEntity>> watchAll() {
+    return select(documents).watch();
   }
 
-  SimpleSelectStatement<$DocumentsTable, DocumentEntity> _selectRef(
-    DocumentRef ref,
-  ) {
-    return select(documents)
-      ..where((tbl) => _filterRef(tbl, ref))
+  @override
+  Stream<int> watchDocumentCommentsCount({required DocumentRef ref}) {
+    return (select(documents)
+          ..where((row) => row.type.equals(DocumentType.commentTemplate.uuid)))
+        .watch()
+        .map(
+          (comments) => comments.where((doc) => doc.metadata.ref == ref).length,
+        )
+        .distinct();
path: catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/dao/documents_dao.dart
user: login: damian-molinski
body: 1. extract select into variable
2. filter in where statement not here
created_at: 2025-02-28T11:17:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -149,23 +189,84 @@ class DriftDocumentsDao extends DatabaseAccessor<DriftCatalystDatabase>
   }
 
   @override
-  Future<void> deleteAll() async {
-    final deletedRows = await delete(documents).go();
+  Stream<DocumentEntity?> watch({required DocumentRef ref}) {
+    return _selectRef(ref)
+        .watch()
+        .map((event) => event.firstOrNull)
+        .distinct(_entitiesEquals);
+  }
 
-    if (kDebugMode) {
-      debugPrint('DocumentsDao: Deleted[$deletedRows] rows');
-    }
+  @override
+  Stream<List<DocumentEntity>> watchAll() {
+    return select(documents).watch();
   }
 
-  SimpleSelectStatement<$DocumentsTable, DocumentEntity> _selectRef(
-    DocumentRef ref,
-  ) {
-    return select(documents)
-      ..where((tbl) => _filterRef(tbl, ref))
+  @override
+  Stream<int> watchDocumentCommentsCount({required DocumentRef ref}) {
+    return (select(documents)
+          ..where((row) => row.type.equals(DocumentType.commentTemplate.uuid)))
+        .watch()
+        .map(
+          (comments) => comments.where((doc) => doc.metadata.ref == ref).length,
+        )
+        .distinct();
+  }
+
+  @override
+  Stream<List<DocumentEntity>> watchLatestVersions({int? limit}) {
+    final uniqueIds = selectOnly(documents)
+      ..addColumns([documents.idHi, documents.idLo])
+      ..groupBy([documents.idHi, documents.idLo])
       ..orderBy([
-        (u) => OrderingTerm.desc(u.verHi),
-      ])
-      ..limit(1);
+        OrderingTerm(expression: documents.verHi, mode: OrderingMode.desc),
+        OrderingTerm(expression: documents.verLo, mode: OrderingMode.desc),
+        OrderingTerm(expression: documents.idHi),
+        OrderingTerm(expression: documents.idLo),
path: catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/dao/documents_dao.dart
user: login: damian-molinski
body: why ordering by `lo` part?
created_at: 2025-02-28T11:22:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -149,23 +189,84 @@ class DriftDocumentsDao extends DatabaseAccessor<DriftCatalystDatabase>
   }
 
   @override
-  Future<void> deleteAll() async {
-    final deletedRows = await delete(documents).go();
+  Stream<DocumentEntity?> watch({required DocumentRef ref}) {
+    return _selectRef(ref)
+        .watch()
+        .map((event) => event.firstOrNull)
+        .distinct(_entitiesEquals);
+  }
 
-    if (kDebugMode) {
-      debugPrint('DocumentsDao: Deleted[$deletedRows] rows');
-    }
+  @override
+  Stream<List<DocumentEntity>> watchAll() {
+    return select(documents).watch();
   }
 
-  SimpleSelectStatement<$DocumentsTable, DocumentEntity> _selectRef(
-    DocumentRef ref,
-  ) {
-    return select(documents)
-      ..where((tbl) => _filterRef(tbl, ref))
+  @override
+  Stream<int> watchDocumentCommentsCount({required DocumentRef ref}) {
+    return (select(documents)
+          ..where((row) => row.type.equals(DocumentType.commentTemplate.uuid)))
+        .watch()
+        .map(
+          (comments) => comments.where((doc) => doc.metadata.ref == ref).length,
+        )
+        .distinct();
+  }
+
+  @override
+  Stream<List<DocumentEntity>> watchLatestVersions({int? limit}) {
+    final uniqueIds = selectOnly(documents)
+      ..addColumns([documents.idHi, documents.idLo])
+      ..groupBy([documents.idHi, documents.idLo])
       ..orderBy([
-        (u) => OrderingTerm.desc(u.verHi),
-      ])
-      ..limit(1);
+        OrderingTerm(expression: documents.verHi, mode: OrderingMode.desc),
+        OrderingTerm(expression: documents.verLo, mode: OrderingMode.desc),
+        OrderingTerm(expression: documents.idHi),
+        OrderingTerm(expression: documents.idLo),
+      ]);
+
+    if (limit != null) {
+      uniqueIds.limit(limit);
+    }
+
+    return uniqueIds.watch().asyncMap((ids) async {
+      final latestVersions = await Future.wait(
+        ids.map(
+          (row) {
+            final idHi = row.read(documents.idHi);
+            final idLo = row.read(documents.idLo);
+
+            if (idHi == null || idLo == null) {
+              throw StateError('Document ID cannot be null');
+            }
path: catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/dao/documents_dao.dart
user: login: damian-molinski
body: `idHi` and `idLo` are primary keys, you can safely just use `!`

 ```dart
            final idHi = row.read(documents.idHi)!;
            final idLo = row.read(documents.idLo)!;
```
created_at: 2025-02-28T11:28:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -62,12 +60,25 @@ abstract interface class DocumentRepository {
     required DraftRef ref,
     required DocumentDataContent content,
   });
+
+  Stream<List<ProposalDocument>> watchLatestPublicProposalsDocuments({
+    int? limit,
+  });
path: catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/document_repository.dart
user: login: damian-molinski
body: maybe just `watchProposalsDocuments` with optional limit
created_at: 2025-02-28T11:31:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -62,12 +60,25 @@ abstract interface class DocumentRepository {
     required DraftRef ref,
     required DocumentDataContent content,
   });
+
+  Stream<List<ProposalDocument>> watchLatestPublicProposalsDocuments({
+    int? limit,
+  });
+
+  Stream<int> watchProposalCommentsCount({required DocumentRef ref});
path: catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/document_repository.dart
user: login: damian-molinski
body: `watchCount` with optional `type` parameter
created_at: 2025-02-28T11:31:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -149,23 +189,84 @@ class DriftDocumentsDao extends DatabaseAccessor<DriftCatalystDatabase>
   }
 
   @override
-  Future<void> deleteAll() async {
-    final deletedRows = await delete(documents).go();
+  Stream<DocumentEntity?> watch({required DocumentRef ref}) {
+    return _selectRef(ref)
+        .watch()
+        .map((event) => event.firstOrNull)
+        .distinct(_entitiesEquals);
+  }
 
-    if (kDebugMode) {
-      debugPrint('DocumentsDao: Deleted[$deletedRows] rows');
-    }
+  @override
+  Stream<List<DocumentEntity>> watchAll() {
+    return select(documents).watch();
   }
 
-  SimpleSelectStatement<$DocumentsTable, DocumentEntity> _selectRef(
-    DocumentRef ref,
-  ) {
-    return select(documents)
-      ..where((tbl) => _filterRef(tbl, ref))
+  @override
+  Stream<int> watchDocumentCommentsCount({required DocumentRef ref}) {
+    return (select(documents)
+          ..where((row) => row.type.equals(DocumentType.commentTemplate.uuid)))
+        .watch()
+        .map(
+          (comments) => comments.where((doc) => doc.metadata.ref == ref).length,
+        )
+        .distinct();
+  }
+
+  @override
+  Stream<List<DocumentEntity>> watchLatestVersions({int? limit}) {
+    final uniqueIds = selectOnly(documents)
+      ..addColumns([documents.idHi, documents.idLo])
+      ..groupBy([documents.idHi, documents.idLo])
       ..orderBy([
-        (u) => OrderingTerm.desc(u.verHi),
-      ])
-      ..limit(1);
+        OrderingTerm(expression: documents.verHi, mode: OrderingMode.desc),
+        OrderingTerm(expression: documents.verLo, mode: OrderingMode.desc),
+        OrderingTerm(expression: documents.idHi),
+        OrderingTerm(expression: documents.idLo),
+      ]);
+
+    if (limit != null) {
+      uniqueIds.limit(limit);
+    }
+
+    return uniqueIds.watch().asyncMap((ids) async {
+      final latestVersions = await Future.wait(
+        ids.map(
+          (row) {
+            final idHi = row.read(documents.idHi);
+            final idLo = row.read(documents.idLo);
+
+            if (idHi == null || idLo == null) {
+              throw StateError('Document ID cannot be null');
+            }
+            return (select(documents)
+                  ..where(
+                    (tbl) => Expression.and([
+                      tbl.idHi.equals(idHi),
+                      tbl.idLo.equals(idLo),
+                    ]),
+                  )
+                  ..orderBy([
+                    (u) => OrderingTerm.desc(u.verHi),
+                    (u) => OrderingTerm.desc(u.verLo),
+                  ])
+                  ..limit(1))
+                .getSingle();
path: catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/database/dao/documents_dao.dart
user: login: damian-molinski
body: this `asyncMap` should be done my `DocumentRepository`. Right now this dao knows too much.
created_at: 2025-02-28T11:33:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -133,37 +157,117 @@ final class DocumentRepositoryImpl implements DocumentRepository {
   }
 
   @override
-  Future<DraftRef> createProposalDraft({
+  Future<List<String>> getProposalVersionIds({required DocumentRef ref}) {
+    return _localDocuments.documentVersionIds(ref: ref);
+  }
+
+  @override
+  Future<void> updateProposalDraftContent({
+    required DraftRef ref,
     required DocumentDataContent content,
-    required SignedDocumentRef template,
-    SignedDocumentRef? of,
   }) async {
-    final id = of?.id ?? const Uuid().v7();
-    final version = of != null ? const Uuid().v7() : id;
+    await _drafts.update(ref: ref, content: content);
+  }
 
-    final ref = DraftRef(id: id, version: version);
-    final metadata = DocumentDataMetadata(
-      type: DocumentType.proposalDocument,
-      selfRef: ref,
-      template: template,
-    );
+  @visibleForTesting
+  Stream<DocumentsDataWithRefData?> watchDocumentWithRef({
+    required DocumentRef ref,
+    required ValueResolver<DocumentData, DocumentRef> refGetter,
+  }) {
+    return _watchDocumentData(ref: ref)
+        .distinct()
+        .switchMap<DocumentsDataWithRefData?>((document) {
+      if (document == null) {
+        return Stream.value(null);
+      }
 
-    final data = DocumentData(
-      metadata: metadata,
-      content: content,
-    );
+      final ref = refGetter(document);
+      final refDocumentStream = _watchDocumentData(
+        ref: ref,
+        // Synchronized because we may have many documents which are referring
+        // to the same template. When loading multiple documents at the same
+        // time we want to fetch only once template.
+        synchronizedUpdate: true,
+      );
 
-    await _drafts.save(data: data);
+      return refDocumentStream.map<DocumentsDataWithRefData?>(
+        (refDocumentData) {
+          return refDocumentData != null
+              ? (data: document, refData: refDocumentData)
+              : null;
+        },
+      );
+    });
+  }
 
-    return ref;
+  @visibleForTesting
+  Stream<List<DocumentsDataWithRefData>> watchLatestDocumentsWithRef({
+    int? limit,
+  }) {
+    return _localDocuments
+        .watchLatestVersions(limit: limit)
+        .distinct()
+        .switchMap((documents) async* {
+      final results = await Future.wait(
+        documents
+            .where((doc) => doc.metadata.template != null)
+            .map((documentData) async {
+          final templateRef = documentData.metadata.template!;
+          final templateData = await getDocumentData(ref: templateRef);
path: catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/document_repository.dart
user: login: damian-molinski
body: this should use `_documentDataLock` as we should not fetch same template twice
created_at: 2025-02-28T11:37:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,187 @@
+// Create mocks
+import 'dart:async';
+
+import 'package:catalyst_voices_models/catalyst_voices_models.dart';
+import 'package:catalyst_voices_repositories/catalyst_voices_repositories.dart';
+import 'package:catalyst_voices_services/catalyst_voices_services.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:mocktail/mocktail.dart';
+import 'package:uuid/uuid.dart';
+
+void main() {
+  late MockDocumentRepository mockDocumentRepository;
+  late MockProposalRepository mockProposalRepository;
+  late ProposalService proposalService;
+
+  setUp(() {
+    mockDocumentRepository = MockDocumentRepository();
+    mockProposalRepository = MockProposalRepository();
+
+    proposalService =
+        ProposalService(mockProposalRepository, mockDocumentRepository);
+
+    registerFallbackValue(const SignedDocumentRef(id: 'fallback-id'));
+
+    // Add a default response for any watchProposalCommentsCount call
+    when(
+      () => mockDocumentRepository.watchProposalCommentsCount(
+        ref: any(named: 'ref'),
+      ),
+    ).thenAnswer((_) => Stream.fromIterable([5]));
+  });
path: catalyst_voices/packages/internal/catalyst_voices_services/test/src/proposal/proposal_service_test.dart
user: login: dt-iohk
body: Usually we put it inside of the `group()`
created_at: 2025-02-28T12:46:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -11,7 +13,19 @@ part 'discovery_state.dart';
 class DiscoveryCubit extends Cubit<DiscoveryState> {
   // ignore: unused_field
   final CampaignService _campaignService;
-  DiscoveryCubit(this._campaignService) : super(const DiscoveryState());
+  final ProposalService _proposalService;
+  late StreamSubscription<List<Proposal>>? _proposalsSubscription;
+  DiscoveryCubit(
path: catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/discovery/discovery_cubit.dart
user: login: dt-iohk
body: ```suggestion
  late StreamSubscription<List<Proposal>>? _proposalsSubscription;
  
  DiscoveryCubit(
```
created_at: 2025-03-03T11:26:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -362,6 +362,45 @@ void main() {
         ]),
       );
     });
+
+    test(
+      'watchLatestPublicProposalsDocuments returns correct model',
+      () async {
+        final templateRef = DocumentRefFactory.buildSigned();
+        final templateData = DocumentDataFactory.build(
+          selfRef: templateRef,
+          type: DocumentType.proposalTemplate,
+        );
+        const publicDraftContent =
+            DocumentDataContent({'title': 'My proposal'});
+        final publicDraftRef = DocumentRefFactory.buildDraft();
+        final publicDraftData = DocumentDataFactory.build(
+          type: DocumentType.proposalDocument,
+          selfRef: publicDraftRef,
+          template: templateData.ref,
+          content: publicDraftContent,
+        );
+
+        await localDocuments.save(data: templateData);
+        await Future.delayed(const Duration(seconds: 2), () {});
path: catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/document/document_repository_test.dart
user: login: dt-iohk
body: Is this needed? It's slowing tests massively. If there's anything related to time consider to fake it, i.e. `fakeAsync` allows you to simulate a certain duration has ellapsed
created_at: 2025-03-03T11:28:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -19,6 +19,7 @@ void main() {
       order: [],
     );
 
+    // ignore: unused_local_variable
path: catalyst_voices/packages/internal/catalyst_voices_blocs/test/proposals/proposals_cubit_test.dart
user: login: dt-iohk
body: if not needed drop it
created_at: 2025-03-03T11:30:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,67 @@
+import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart';
+import 'package:catalyst_voices_models/catalyst_voices_models.dart';
+import 'package:equatable/equatable.dart';
+
+class ProposalData extends Equatable {
+  final ProposalDocument document;
+  final DocumentRef ref;
+  final String categoryId;
+  final int commentsCount;
+  final List<String> versions;
+
+  const ProposalData({
+    required this.document,
+    required this.ref,
+    required this.categoryId,
+    this.commentsCount = 0,
+    this.versions = const [],
+  });
+
+  String? get proposalAuthor {
+    final property =
+        document.document.getProperty(ProposalDocument.authorNameNodeId)
+            as DocumentValueProperty<String>?;
+
+    return property?.value;
+  }
+
+  String? get proposalDescription {
+    final property =
+        document.document.getProperty(ProposalDocument.descriptionNodeId)
+            as DocumentValueProperty<String>?;
+
+    return property?.value;
+  }
+
+  int? get proposalDuration {
+    final property =
+        document.document.getProperty(ProposalDocument.durationNodeId)
+            as DocumentValueProperty<int>?;
+
+    return property?.value;
+  }
+
+  Coin? get proposalFundsRequested {
+    final property =
+        document.document.getProperty(ProposalDocument.requestedFundsNodeId)
+            as DocumentValueProperty<int>?;
+    if (property == null || property.value == null) return null;
+    return Coin(property.value!);
path: catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_data.dart
user: login: dt-iohk
body: If you rewrite it like this then you don't need to force unwrap it
```suggestion
    final value = property?.value;
    if (value == null) return null;
    return Coin(value);
```
created_at: 2025-03-03T11:32:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,34 +1,90 @@
+import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart';
 import 'package:catalyst_voices_models/catalyst_voices_models.dart';
+import 'package:equatable/equatable.dart';
 
-final class Proposal extends ProposalBase {
-  /// A hardcoded [NodeId] of the title property.
-  ///
-  /// Since properties are dynamic the application cannot determine
-  /// which property is the title in any other way than
-  /// by hardcoding it's node ID.
-  static final titleNodeId = DocumentNodeId.fromString('setup.title.title');
-
-  final ProposalDocument document;
+final class Proposal extends Equatable {
+  final String id;
+  final String version;
+  final String title;
+  final String description;
+  final DateTime updateDate;
+  final DateTime? fundedDate;
+  final Coin fundsRequested;
+  final ProposalStatus status;
+  final ProposalPublish publish;
+  final int versionCount;
+  final int duration;
+  final String author;
+  final int commentsCount;
+  final String category;
 
   const Proposal({
-    required super.id,
-    required super.title,
-    required super.description,
-    required super.updateDate,
-    required super.fundsRequested,
-    required super.status,
-    required super.publish,
-    required super.category,
-    required super.commentsCount,
-    required this.document,
-    required super.version,
-    required super.duration,
-    required super.author,
+    required this.id,
+    required this.version,
+    required this.title,
+    required this.description,
+    required this.updateDate,
+    this.fundedDate,
+    required this.fundsRequested,
+    required this.status,
+    required this.publish,
+    required this.versionCount,
+    required this.duration,
+    required this.author,
+    required this.commentsCount,
+    required this.category,
   });
 
+  factory Proposal.dummy(String id, String version) => Proposal(
+        id: id,
+        version: version,
+        category: 'Cardano Use Cases / MVP',
+        title: 'Dummy Proposal',
+        updateDate: DateTime.now(),
+        fundsRequested: Coin.fromAda(100000),
+        status: ProposalStatus.draft,
+        publish: ProposalPublish.localDraft,
+        commentsCount: 0,
+        description: 'Dummy description',
+        duration: 6,
+        author: 'Alex Wells',
+        versionCount: 1,
+      );
+
+  factory Proposal.fromData(ProposalData data) {
+    final updateDate = UuidV7.parseTimestamp(data.document.metadata.version);
path: catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal.dart
user: login: damian-molinski
body: It was merged, you may change it
created_at: 2025-03-04T08:13:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -62,12 +60,25 @@ abstract interface class DocumentRepository {
     required DraftRef ref,
     required DocumentDataContent content,
   });
+
+  Stream<List<ProposalDocument>> watchLatestPublicProposalsDocuments({
+    int? limit,
+  });
path: catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/document_repository.dart
user: login: damian-molinski
body: i think this one was forgotten :D 
created_at: 2025-03-04T08:16:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -101,8 +100,11 @@ final class ProposalRepositoryImpl implements ProposalRepository {
           ? ProposalPublish.submittedProposal
           : ProposalPublish.publishedDraft;
       proposals.add(
-        ProposalBase(
-          id: '${Random().nextInt(1000)}/${Random().nextInt(1000)}',
+        Proposal(
+          selfRef: SignedDocumentRef(
+            id: '${Random().nextInt(1000)}/${Random().nextInt(1000)}',
+            version: '1',
+          ),
path: catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/proposal/proposal_repository.dart
user: login: damian-molinski
body: use Uuid.v7. Maybe add factory constructor to `SignedDocumentRef` which will build new instance with it?

like `SignedDocumentRef.generateFirstRef()`? We have smth similar for `DraftRef`
created_at: 2025-03-04T08:23:40Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-02-27T14:33:30Z
body: ✅ [Test Report](https://input-output-hk.github.io/catalyst-voices/allure-action/feat_1553-connect-backend-discovery/test-report/13568338267_1740666794905) | ${\color{lightgreen}Pass: 594/599}$ | ${\color{red}Fail: 0/599}$ |
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-02-27T20:06:04Z
body: ✅ [Test Report](https://input-output-hk.github.io/catalyst-voices/allure-action/feat_1553-connect-backend-discovery/test-report/13574571820_1740686749167) | ${\color{lightgreen}Pass: 599/604}$ | ${\color{red}Fail: 0/604}$ |
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-02-27T20:40:46Z
body: ✅ [Test Report](https://input-output-hk.github.io/catalyst-voices/allure-action/feat_1553-connect-backend-discovery/test-report/13574966961_1740688831131) | ${\color{lightgreen}Pass: 599/604}$ | ${\color{red}Fail: 0/604}$ |
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-02-27T21:47:11Z
body: ✅ [Test Report](https://input-output-hk.github.io/catalyst-voices/allure-action/feat_1553-connect-backend-discovery/test-report/13576261337_1740692816107) | ${\color{lightgreen}Pass: 601/606}$ | ${\color{red}Fail: 0/606}$ |
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-02-28T09:03:57Z
body: ✅ [Test Report](https://input-output-hk.github.io/catalyst-voices/allure-action/feat_1553-connect-backend-discovery/test-report/13584472768_1740733430144) | ${\color{lightgreen}Pass: 601/606}$ | ${\color{red}Fail: 0/606}$ |
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-02-28T09:07:36Z
body: ✅ [Test Report](https://input-output-hk.github.io/catalyst-voices/allure-action/feat_1553-connect-backend-discovery/test-report/13584564505_1740733648033) | ${\color{lightgreen}Pass: 601/606}$ | ${\color{red}Fail: 0/606}$ |
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-03T10:57:37Z
body: ❌ [Test Report](https://input-output-hk.github.io/catalyst-voices/allure-action/feat_1553-connect-backend-discovery/test-report/13628979347_1740999448790) | ${\color{lightgreen}Pass: 496/519}$ | ${\color{red}Fail: 18/519}$ |
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-04T10:18:42Z
body: ✅ [Test Report](https://input-output-hk.github.io/catalyst-voices/allure-action/feat_1553-connect-backend-discovery/test-report/13671894640_1741166053496) | ${\color{lightgreen}Pass: 614/619}$ | ${\color{red}Fail: 0/619}$ |
<!-- thollander/actions-comment-pull-request "test_report" -->
author_association: CONTRIBUTOR