Skip to content

Commit 6fb1848

Browse files
authored
feat(cat-voices): Proposal editor comments (#2113)
* feat: pass category id * feat: make category required * feat: refactor proposal builder to use cache object * chore: hide dialog * chore: generate code * feat: extract comments and add them in proposal builder * fix: comments editing in proposal builder * chore: cleanup * feat: add comments header * chore: cleanup * fix: can reply * chore: sort exports * chore: sort exports * chore: cleanup * feat: allow to view/add coments for the opened proposal version * fix: loading state
1 parent 47a48b6 commit 6fb1848

31 files changed

+923
-247
lines changed

catalyst_voices/apps/voices/lib/dependency/dependencies.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ final class Dependencies extends DependencyProvider {
108108
return ProposalBuilderBloc(
109109
get<ProposalService>(),
110110
get<CampaignService>(),
111+
get<CommentService>(),
112+
get<UserService>(),
111113
get<DownloaderService>(),
112114
get<DocumentMapper>(),
113115
);

catalyst_voices/apps/voices/lib/pages/proposal/proposal_content.dart

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import 'dart:math';
22

3-
import 'package:catalyst_voices/pages/proposal/tiles/proposal_add_comment_tile.dart';
43
import 'package:catalyst_voices/pages/proposal/tiles/proposal_comment_tile.dart';
5-
import 'package:catalyst_voices/pages/proposal/tiles/proposal_comments_header_tile.dart';
64
import 'package:catalyst_voices/pages/proposal/tiles/proposal_document_section_tile.dart';
75
import 'package:catalyst_voices/pages/proposal/tiles/proposal_document_segment_title.dart';
86
import 'package:catalyst_voices/pages/proposal/tiles/proposal_metadata_tile.dart';
97
import 'package:catalyst_voices/pages/proposal/tiles/proposal_overview_tile.dart';
10-
import 'package:catalyst_voices/pages/proposal/tiles/proposal_tile_decoration.dart';
8+
import 'package:catalyst_voices/widgets/comment/proposal_add_comment_tile.dart';
9+
import 'package:catalyst_voices/widgets/comment/proposal_comments_header_tile.dart';
10+
import 'package:catalyst_voices/widgets/tiles/specialized/proposal_tile_decoration.dart';
1111
import 'package:catalyst_voices/widgets/widgets.dart';
1212
import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart';
1313
import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart';
@@ -101,7 +101,7 @@ class _SegmentsListView extends StatelessWidget {
101101
final isLast = index == max(items.length - 1, 0);
102102

103103
final isSegment = item is Segment;
104-
final isNextComment = nextItem is CommentListItem;
104+
final isNextComment = nextItem is ProposalCommentListItem;
105105
final isNextSectionOrComment = nextItem is Section || isNextComment;
106106
final isCommentsSegment = item is ProposalCommentsSegment;
107107
final isNotEmptyCommentsSegment = isCommentsSegment && item.hasComments;
@@ -131,7 +131,7 @@ class _SegmentsListView extends StatelessWidget {
131131
return const ProposalSeparatorBox(height: 24);
132132
}
133133

134-
if (nextItem is AddCommentSection) {
134+
if (nextItem is ProposalAddCommentSection) {
135135
return const ProposalDivider(height: 48);
136136
}
137137

@@ -181,16 +181,22 @@ class _SegmentsListView extends StatelessWidget {
181181
ProposalCommentsSegment(:final sort) => ProposalCommentsHeaderTile(
182182
sort: sort,
183183
showSort: item.hasComments,
184+
onChanged: (value) {
185+
context.read<ProposalCubit>().updateCommentsSort(sort: value);
186+
},
184187
),
185188
ProposalCommentsSection() => switch (item) {
186-
ViewCommentsSection() => throw ArgumentError(
187-
'View comments not supported',
188-
),
189-
AddCommentSection(:final schema) => ProposalAddCommentTile(
189+
ProposalViewCommentsSection() => const SizedBox.shrink(),
190+
ProposalAddCommentSection(:final schema) => ProposalAddCommentTile(
190191
schema: schema,
192+
onSubmit: ({required document, reply}) async {
193+
return context
194+
.read<ProposalCubit>()
195+
.submitComment(document: document, reply: reply);
196+
},
191197
),
192198
},
193-
CommentListItem(
199+
ProposalCommentListItem(
194200
:final comment,
195201
:final canReply,
196202
) =>

catalyst_voices/apps/voices/lib/pages/proposal/tiles/proposal_comment_tile.dart

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import 'package:catalyst_voices/pages/proposal/widget/proposal_comment_with_replies_card.dart';
1+
import 'package:catalyst_voices/widgets/comment/proposal_comment_with_replies_card.dart';
22
import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart';
33
import 'package:catalyst_voices_models/catalyst_voices_models.dart';
44
import 'package:flutter/material.dart';
@@ -16,22 +16,36 @@ class ProposalCommentTile extends StatelessWidget {
1616

1717
@override
1818
Widget build(BuildContext context) {
19-
final showReplies = context.select<ProposalCubit, bool>((value) {
20-
return value.state.data.showReplies[comment.ref] ?? true;
19+
final showReplies =
20+
context.select<ProposalCubit, Map<DocumentRef, bool>>((value) {
21+
return value.state.comments.showReplies;
2122
});
2223

2324
final showReplyBuilder = context.select<ProposalCubit, bool>((value) {
24-
return value.state.data.showReplyBuilder[comment.ref] ?? false;
25+
return value.state.comments.showReplyBuilder[comment.ref] ?? false;
2526
});
2627

2728
final id = comment.comment.metadata.selfRef.id;
29+
final cubit = context.read<ProposalCubit>();
2830

2931
return ProposalCommentWithRepliesCard(
3032
key: ValueKey('ProposalComment.$id.WithReplies'),
3133
comment: comment,
3234
canReply: canReply,
3335
showReplies: showReplies,
3436
showReplyBuilder: showReplyBuilder,
37+
onSubmit: ({required document, reply}) async {
38+
return cubit.submitComment(document: document, reply: reply);
39+
},
40+
onCancel: () {
41+
cubit.updateCommentBuilder(ref: comment.ref, show: false);
42+
},
43+
onToggleBuilder: (show) {
44+
cubit.updateCommentBuilder(ref: comment.ref, show: show);
45+
},
46+
onToggleReplies: (show) {
47+
cubit.updateCommentReplies(ref: comment.ref, show: show);
48+
},
3549
);
3650
}
3751
}

catalyst_voices/apps/voices/lib/pages/proposal_builder/proposal_builder_page.dart

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ class _ProposalBuilderPageState extends State<ProposalBuilderPage>
9595
rightRail: const ProposalBuilderSetupPanel(),
9696
body: _ProposalBuilderContent(
9797
controller: _segmentsScrollController,
98-
onRetryTap: _updateSource,
98+
onRetryTap: _loadData,
9999
),
100100
bodyConstraints: const BoxConstraints.expand(),
101101
),
@@ -110,7 +110,7 @@ class _ProposalBuilderPageState extends State<ProposalBuilderPage>
110110

111111
if (widget.proposalId != oldWidget.proposalId ||
112112
widget.categoryId != oldWidget.categoryId) {
113-
_updateSource();
113+
_loadData();
114114
}
115115
}
116116

@@ -163,13 +163,15 @@ class _ProposalBuilderPageState extends State<ProposalBuilderPage>
163163
..attachItemsScrollController(_segmentsScrollController);
164164

165165
_segmentsSub = bloc.stream
166-
.map((event) => (segments: event.segments, nodeId: event.activeNodeId))
166+
.map(
167+
(event) => (segments: event.allSegments, nodeId: event.activeNodeId),
168+
)
167169
.distinct(
168170
(a, b) => listEquals(a.segments, b.segments) && a.nodeId == b.nodeId,
169171
)
170172
.listen((record) => _updateSegments(record.segments, record.nodeId));
171173

172-
_updateSource(bloc: bloc);
174+
_loadData(bloc: bloc);
173175
}
174176

175177
void _handleSegmentsControllerChange() {
@@ -237,7 +239,7 @@ class _ProposalBuilderPageState extends State<ProposalBuilderPage>
237239
_segmentsController.value = newState;
238240
}
239241

240-
void _updateSource({ProposalBuilderBloc? bloc}) {
242+
void _loadData({ProposalBuilderBloc? bloc}) {
241243
bloc ??= context.read<ProposalBuilderBloc>();
242244

243245
final proposalId = widget.proposalId;

catalyst_voices/apps/voices/lib/pages/proposal_builder/proposal_builder_segments.dart

Lines changed: 158 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart';
2+
import 'package:catalyst_voices/pages/proposal_builder/tiles/proposal_builder_comment_tile.dart';
3+
import 'package:catalyst_voices/widgets/comment/proposal_add_comment_tile.dart';
4+
import 'package:catalyst_voices/widgets/comment/proposal_comments_header_tile.dart';
5+
import 'package:catalyst_voices/widgets/tiles/specialized/proposal_tile_decoration.dart';
26
import 'package:catalyst_voices/widgets/widgets.dart';
37
import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart';
48
import 'package:catalyst_voices_localization/catalyst_voices_localization.dart';
@@ -40,42 +44,11 @@ class ProposalBuilderSegmentsSelector extends StatelessWidget {
4044
}
4145
}
4246

43-
class _ProposalBuilderSegments extends StatelessWidget {
44-
final ItemScrollController itemScrollController;
45-
46-
const _ProposalBuilderSegments({
47-
required this.itemScrollController,
48-
});
49-
50-
@override
51-
Widget build(BuildContext context) {
52-
return ValueListenableBuilder(
53-
valueListenable: SegmentsControllerScope.of(context),
54-
builder: (context, value, child) {
55-
final items = value.listItems;
56-
final selectedNodeId = value.activeSectionId;
57-
58-
return SegmentsListView<DocumentSegment, DocumentSection>(
59-
itemScrollController: itemScrollController,
60-
items: items,
61-
padding: const EdgeInsets.only(top: 16, bottom: 64),
62-
sectionBuilder: (context, data) {
63-
return _Section(
64-
property: data.property,
65-
isSelected: data.property.nodeId == selectedNodeId,
66-
);
67-
},
68-
);
69-
},
70-
);
71-
}
72-
}
73-
74-
class _Section extends StatelessWidget {
47+
class _DocumentSection extends StatelessWidget {
7548
final DocumentProperty property;
7649
final bool isSelected;
7750

78-
const _Section({
51+
const _DocumentSection({
7952
required this.property,
8053
required this.isSelected,
8154
});
@@ -115,3 +88,155 @@ class _Section extends StatelessWidget {
11588
);
11689
}
11790
}
91+
92+
class _ProposalBuilderSegments extends StatelessWidget {
93+
final ItemScrollController itemScrollController;
94+
95+
const _ProposalBuilderSegments({
96+
required this.itemScrollController,
97+
});
98+
99+
@override
100+
Widget build(BuildContext context) {
101+
return ValueListenableBuilder(
102+
valueListenable: SegmentsControllerScope.of(context),
103+
builder: (context, value, child) {
104+
final items = value.listItems;
105+
final selectedNodeId = value.activeSectionId;
106+
return BasicSegmentsListView(
107+
key: const ValueKey('ProposalBuilderSegmentsListView'),
108+
items: items,
109+
itemScrollController: itemScrollController,
110+
padding: const EdgeInsets.only(top: 16, bottom: 64),
111+
itemBuilder: (context, index) {
112+
final item = items[index];
113+
final previousItem =
114+
index == 0 ? null : items.elementAtOrNull(index - 1);
115+
final nextItem = items.elementAtOrNull(index + 1);
116+
117+
return _buildItem(
118+
context: context,
119+
item: item,
120+
previousItem: previousItem,
121+
nextItem: nextItem,
122+
selectedNodeId: selectedNodeId,
123+
);
124+
},
125+
separatorBuilder: (context, index) {
126+
final item = items[index];
127+
final nextItem = items.elementAtOrNull(index + 1);
128+
129+
if (nextItem is ProposalCommentsSegment) {
130+
return const SizedBox(height: 32);
131+
}
132+
133+
if (item is ProposalCommentsSegment && nextItem != null) {
134+
return const SizedBox(height: 32);
135+
}
136+
137+
if (item is ProposalViewCommentsSection && nextItem != null) {
138+
return const ProposalSeparatorBox(height: 24);
139+
}
140+
141+
if (item is ProposalViewCommentsSection &&
142+
nextItem is ProposalAddCommentSection) {
143+
return const ProposalDivider(height: 48);
144+
}
145+
146+
if (item is DocumentSegment || item is DocumentSection) {
147+
return const SizedBox(height: 12);
148+
}
149+
150+
return const SizedBox.shrink();
151+
},
152+
);
153+
},
154+
);
155+
}
156+
157+
Widget _buildCommentSection({
158+
required BuildContext context,
159+
required SegmentsListViewItem item,
160+
}) {
161+
return switch (item) {
162+
ProposalViewCommentsSection(:final sort) => ProposalCommentsHeaderTile(
163+
sort: sort,
164+
showSort: item.comments.isNotEmpty,
165+
onChanged: (value) {
166+
context
167+
.read<ProposalBuilderBloc>()
168+
.add(UpdateCommentsSortEvent(sort: value));
169+
},
170+
),
171+
ProposalCommentListItem(:final comment, :final canReply) =>
172+
ProposalBuilderCommentTile(
173+
key: ValueKey(comment.comment.metadata.selfRef),
174+
comment: comment,
175+
canReply: canReply,
176+
),
177+
ProposalAddCommentSection(:final schema) => ProposalAddCommentTile(
178+
schema: schema,
179+
onSubmit: ({required document, reply}) async {
180+
final event = SubmitCommentEvent(
181+
document: document,
182+
reply: reply,
183+
);
184+
context.read<ProposalBuilderBloc>().add(event);
185+
},
186+
),
187+
_ => throw ArgumentError('Not supported type ${item.runtimeType}'),
188+
};
189+
}
190+
191+
Widget _buildDecoratedCommentSection({
192+
required BuildContext context,
193+
required SegmentsListViewItem item,
194+
required SegmentsListViewItem? previousItem,
195+
required SegmentsListViewItem? nextItem,
196+
}) {
197+
final isFirst = previousItem is ProposalCommentsSegment;
198+
final isLast = nextItem == null;
199+
200+
return ProposalTileDecoration(
201+
key: ValueKey('Proposal.${item.id.value}.Tile'),
202+
corners: (
203+
isFirst: isFirst,
204+
isLast: isLast,
205+
),
206+
verticalPadding: (
207+
isFirst: isFirst,
208+
isLast: isLast,
209+
),
210+
child: _buildCommentSection(context: context, item: item),
211+
);
212+
}
213+
214+
Widget _buildItem({
215+
required BuildContext context,
216+
required SegmentsListViewItem item,
217+
required SegmentsListViewItem? previousItem,
218+
required SegmentsListViewItem? nextItem,
219+
required NodeId? selectedNodeId,
220+
}) {
221+
return switch (item) {
222+
DocumentSegment() => SegmentHeaderTile(
223+
id: item.id,
224+
name: item.resolveTitle(context),
225+
),
226+
ProposalCommentsSegment() => SegmentHeaderTile(
227+
id: item.id,
228+
name: item.resolveTitle(context),
229+
),
230+
DocumentSection() => _DocumentSection(
231+
property: item.property,
232+
isSelected: item.property.nodeId == selectedNodeId,
233+
),
234+
_ => _buildDecoratedCommentSection(
235+
context: context,
236+
item: item,
237+
previousItem: previousItem,
238+
nextItem: nextItem,
239+
),
240+
};
241+
}
242+
}

0 commit comments

Comments
 (0)