Skip to content

Commit e2aa51b

Browse files
PIG208gnprice
authored andcommitted
content: Start rendering poll content
We could also make Poll a subclass of ZulipMessageContent, but that takes away our ability to seal ZulipMessageContent. Making a wrapper class around Poll is easy enough for keeping that benefit. Fixes: #165 Signed-off-by: Zixuan James Li <[email protected]>
1 parent 8ebbeed commit e2aa51b

File tree

5 files changed

+63
-0
lines changed

5 files changed

+63
-0
lines changed

lib/model/content.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'package:html/dom.dart' as dom;
44
import 'package:html/parser.dart';
55

66
import '../api/model/model.dart';
7+
import '../api/model/submessage.dart';
78
import 'code_block.dart';
89

910
/// A node in a parse tree for Zulip message-style content.
@@ -76,6 +77,16 @@ mixin UnimplementedNode on ContentNode {
7677
/// A parsed, ready-to-render representation of Zulip message content.
7778
sealed class ZulipMessageContent {}
7879

80+
/// A wrapper around a mutable representation of a Zulip poll message.
81+
///
82+
/// Consumers are expected to listen for [Poll]'s changes to receive
83+
/// live-updates.
84+
class PollContent implements ZulipMessageContent {
85+
const PollContent(this.poll);
86+
87+
final Poll poll;
88+
}
89+
7990
/// A complete parse tree for a Zulip message's content,
8091
/// or other complete piece of Zulip HTML content.
8192
///

lib/model/message_list.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,8 @@ mixin _MessageSequence {
135135
}
136136

137137
ZulipMessageContent _parseMessageContent(Message message) {
138+
final poll = message.poll;
139+
if (poll != null) return PollContent(poll);
138140
return parseContent(message.content);
139141
}
140142

lib/widgets/content.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import 'dialog.dart';
1818
import 'icons.dart';
1919
import 'lightbox.dart';
2020
import 'message_list.dart';
21+
import 'poll.dart';
2122
import 'store.dart';
2223
import 'text.dart';
2324

@@ -263,6 +264,7 @@ class MessageContent extends StatelessWidget {
263264
style: ContentTheme.of(context).textStylePlainParagraph,
264265
child: switch (content) {
265266
ZulipContent() => BlockContentList(nodes: content.nodes),
267+
PollContent() => PollWidget(poll: content.poll),
266268
}));
267269
}
268270
}

test/model/content_checks.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import 'package:checks/checks.dart';
12
import 'package:checks/context.dart';
23
import 'package:flutter/foundation.dart';
4+
import 'package:zulip/api/model/submessage.dart';
35
import 'package:zulip/model/content.dart';
46

57
extension ContentNodeChecks on Subject<ContentNode> {
@@ -115,3 +117,7 @@ Iterable<LinkNode> _findLinkNodes(Iterable<InlineContentNode> nodes) {
115117
return _findLinkNodes(node.nodes);
116118
});
117119
}
120+
121+
extension PollContentChecks on Subject<PollContent> {
122+
Subject<Poll> get poll => has((x) => x.poll, 'poll');
123+
}

test/model/message_list_test.dart

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1488,6 +1488,43 @@ void main() {
14881488
});
14891489
});
14901490

1491+
group('handle content parsing into subclasses of ZulipMessageContent', () {
1492+
test('ZulipContent', () async {
1493+
final stream = eg.stream();
1494+
await prepare(narrow: ChannelNarrow(stream.streamId));
1495+
await prepareMessages(foundOldest: true, messages: []);
1496+
1497+
await store.handleEvent(MessageEvent(id: 0,
1498+
message: eg.streamMessage(stream: stream)));
1499+
// Each [checkNotifiedOnce] call ensures there's been a [checkInvariants]
1500+
// call, where the [ContentNode] gets checked. The additional checks to
1501+
// make this test explicit.
1502+
checkNotifiedOnce();
1503+
check(model).messages.single.poll.isNull();
1504+
check(model).contents.single.isA<ZulipContent>();
1505+
});
1506+
1507+
test('PollContent', () async {
1508+
final stream = eg.stream();
1509+
await prepare(narrow: ChannelNarrow(stream.streamId));
1510+
await prepareMessages(foundOldest: true, messages: []);
1511+
1512+
await store.handleEvent(MessageEvent(id: 0, message: eg.streamMessage(
1513+
stream: stream,
1514+
sender: eg.selfUser,
1515+
submessages: [
1516+
eg.submessage(senderId: eg.selfUser.userId,
1517+
content: eg.pollWidgetData(question: 'question', options: ['A'])),
1518+
])));
1519+
// Each [checkNotifiedOnce] call ensures there's been a [checkInvariants]
1520+
// call, where the value of the [Poll] gets checked. The additional
1521+
// checks make this test explicit.
1522+
checkNotifiedOnce();
1523+
check(model).messages.single.poll.isNotNull();
1524+
check(model).contents.single.isA<PollContent>();
1525+
});
1526+
});
1527+
14911528
test('recipient headers are maintained consistently', () async {
14921529
// TODO test date separators are maintained consistently too
14931530
// This tests the code that maintains the invariant that recipient headers
@@ -1741,6 +1778,11 @@ void checkInvariants(MessageListView model) {
17411778

17421779
check(model).contents.length.equals(model.messages.length);
17431780
for (int i = 0; i < model.contents.length; i++) {
1781+
final poll = model.messages[i].poll;
1782+
if (poll != null) {
1783+
check(model).contents[i].isA<PollContent>().poll.identicalTo(poll);
1784+
continue;
1785+
}
17441786
check(model.contents[i]).isA<ZulipContent>()
17451787
.equalsNode(parseContent(model.messages[i].content));
17461788
}

0 commit comments

Comments
 (0)