-
Notifications
You must be signed in to change notification settings - Fork 309
poll: Support read-only poll widget UI. #912
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -35,7 +35,7 @@ class MessageListDateSeparatorItem extends MessageListItem { | |
/// A message to show in the message list. | ||
class MessageListMessageItem extends MessageListItem { | ||
final Message message; | ||
ZulipContent content; | ||
ZulipMessageContent content; | ||
bool showSender; | ||
bool isLastInBlock; | ||
|
||
|
@@ -98,7 +98,7 @@ mixin _MessageSequence { | |
/// | ||
/// This information is completely derived from [messages]. | ||
/// It exists as an optimization, to memoize the work of parsing. | ||
final List<ZulipContent> contents = []; | ||
final List<ZulipMessageContent> contents = []; | ||
|
||
/// The messages and their siblings in the UI, in order. | ||
/// | ||
|
@@ -134,10 +134,16 @@ mixin _MessageSequence { | |
} | ||
} | ||
|
||
ZulipMessageContent _parseMessageContent(Message message) { | ||
final poll = message.poll; | ||
if (poll != null) return PollContent(poll); | ||
Comment on lines
+138
to
+139
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This logic should get a test. I think the way to do that is
|
||
return parseContent(message.content); | ||
} | ||
|
||
/// Update data derived from the content of the index-th message. | ||
void _reparseContent(int index) { | ||
final message = messages[index]; | ||
final content = parseContent(message.content); | ||
final content = _parseMessageContent(message); | ||
contents[index] = content; | ||
|
||
final itemIndex = findItemWithMessageId(message.id); | ||
|
@@ -154,7 +160,7 @@ mixin _MessageSequence { | |
void _addMessage(Message message) { | ||
assert(contents.length == messages.length); | ||
messages.add(message); | ||
contents.add(parseContent(message.content)); | ||
contents.add(_parseMessageContent(message)); | ||
assert(contents.length == messages.length); | ||
_processMessage(messages.length - 1); | ||
} | ||
|
@@ -197,7 +203,7 @@ mixin _MessageSequence { | |
/// If none of [messageIds] are found, this is a no-op. | ||
bool _removeMessagesById(Iterable<int> messageIds) { | ||
final messagesToRemoveById = <int>{}; | ||
final contentToRemove = Set<ZulipContent>.identity(); | ||
final contentToRemove = Set<ZulipMessageContent>.identity(); | ||
for (final messageId in messageIds) { | ||
final index = _findMessageWithId(messageId); | ||
if (index == -1) continue; | ||
|
@@ -223,7 +229,7 @@ mixin _MessageSequence { | |
assert(contents.length == messages.length); | ||
messages.insertAll(index, toInsert); | ||
contents.insertAll(index, toInsert.map( | ||
(message) => parseContent(message.content))); | ||
(message) => _parseMessageContent(message))); | ||
assert(contents.length == messages.length); | ||
_reprocessAll(); | ||
} | ||
|
@@ -243,7 +249,7 @@ mixin _MessageSequence { | |
void _recompute() { | ||
assert(contents.length == messages.length); | ||
contents.clear(); | ||
contents.addAll(messages.map((message) => parseContent(message.content))); | ||
contents.addAll(messages.map((message) => _parseMessageContent(message))); | ||
assert(contents.length == messages.length); | ||
_reprocessAll(); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
import 'package:flutter/material.dart'; | ||
import 'package:flutter_gen/gen_l10n/zulip_localizations.dart'; | ||
|
||
import '../api/model/submessage.dart'; | ||
import 'content.dart'; | ||
import 'store.dart'; | ||
import 'text.dart'; | ||
|
||
class PollWidget extends StatefulWidget { | ||
const PollWidget({super.key, required this.poll}); | ||
|
||
final Poll poll; | ||
|
||
@override | ||
State<PollWidget> createState() => _PollWidgetState(); | ||
} | ||
|
||
class _PollWidgetState extends State<PollWidget> { | ||
@override | ||
void initState() { | ||
super.initState(); | ||
widget.poll.addListener(_modelChanged); | ||
} | ||
|
||
@override | ||
void didUpdateWidget(covariant PollWidget oldWidget) { | ||
super.didUpdateWidget(oldWidget); | ||
if (widget.poll != oldWidget.poll) { | ||
oldWidget.poll.removeListener(_modelChanged); | ||
widget.poll.addListener(_modelChanged); | ||
} | ||
} | ||
|
||
@override | ||
void dispose() { | ||
widget.poll.removeListener(_modelChanged); | ||
super.dispose(); | ||
} | ||
|
||
void _modelChanged() { | ||
setState(() { | ||
// The actual state lives in the [Poll] model. | ||
// This method was called because that just changed. | ||
}); | ||
} | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
final zulipLocalizations = ZulipLocalizations.of(context); | ||
final theme = ContentTheme.of(context); | ||
final store = PerAccountStoreWidget.of(context); | ||
|
||
final textStyleBold = const TextStyle(fontSize: 18) | ||
.merge(weightVariableTextStyle(context, wght: 600)); | ||
final textStyleVoterNames = TextStyle( | ||
fontSize: 16, color: theme.colorPollNames); | ||
|
||
Text question = (widget.poll.question.isNotEmpty) | ||
? Text(widget.poll.question, style: textStyleBold) | ||
: Text(zulipLocalizations.pollWidgetQuestionMissing, | ||
style: textStyleBold.copyWith(fontStyle: FontStyle.italic)); | ||
|
||
Widget buildOptionItem(PollOption option) { | ||
// TODO(i18n): List formatting, like you can do in JavaScript: | ||
// new Intl.ListFormat('ja').format(['Chris', 'Greg', 'Alya', 'Zixuan']) | ||
// // 'Chris、Greg、Alya、Zixuan' | ||
final voterNames = option.voters | ||
.map((userId) => | ||
store.users[userId]?.fullName ?? zulipLocalizations.unknownUserName) | ||
.join(', '); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indented this in a style taken from @override
List<DiagnosticsNode> debugDescribeChildren() {
return items
.mapIndexed((i, nodes) =>
_BlockContentListNode(nodes).toDiagnosticsNode(name: 'item $i'))
.toList();
} |
||
|
||
return Padding( | ||
padding: const EdgeInsets.only(bottom: 5), | ||
child: Row( | ||
spacing: 5, | ||
crossAxisAlignment: CrossAxisAlignment.baseline, | ||
textBaseline: localizedTextBaseline(context), | ||
children: [ | ||
ConstrainedBox( | ||
constraints: const BoxConstraints(minWidth: 25), | ||
child: Container( | ||
height: 25, | ||
padding: const EdgeInsets.symmetric(horizontal: 4), | ||
decoration: BoxDecoration( | ||
color: theme.colorPollVoteCountBackground, | ||
border: Border.all(color: theme.colorPollVoteCountBorder), | ||
borderRadius: BorderRadius.circular(3)), | ||
child: Center( | ||
child: Text(option.voters.length.toString(), | ||
textAlign: TextAlign.center, | ||
style: textStyleBold.copyWith( | ||
color: theme.colorPollVoteCountText, fontSize: 13))))), | ||
Expanded( | ||
child: Wrap( | ||
spacing: 5, | ||
children: [ | ||
Text(option.text, style: textStyleBold.copyWith(fontSize: 16)), | ||
if (option.voters.isNotEmpty) | ||
// TODO(i18n): Localize parenthesis characters. | ||
Text('($voterNames)', style: textStyleVoterNames), | ||
])), | ||
])); | ||
} | ||
|
||
return Column( | ||
crossAxisAlignment: CrossAxisAlignment.start, | ||
children: [ | ||
Padding(padding: const EdgeInsets.only(bottom: 6), child: question), | ||
if (widget.poll.options.isEmpty) | ||
Text(zulipLocalizations.pollWidgetOptionsMissing, | ||
style: textStyleVoterNames.copyWith(fontStyle: FontStyle.italic)), | ||
for (final option in widget.poll.options) | ||
buildOptionItem(option), | ||
]); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While this is pretty short, I'm not sure if we need a better way to organize
ZulipMessageContent
,PollContent
, andMessageContent
. The filelib/model/content.dart
is predominantly about our AST for Zulip HTML content.