1
+ import 'dart:convert' ;
2
+
1
3
import 'package:json_annotation/json_annotation.dart' ;
2
4
5
+ import '../../log.dart' ;
6
+
3
7
part 'submessage.g.dart' ;
4
8
5
9
/// Data used for certain experimental Zulip widgets including polls and todo
@@ -22,7 +26,7 @@ class Submessage {
22
26
// We cannot parse the String into one of the [SubmessageData] classes because
23
27
// information from other submessages are required. Specifically, we need:
24
28
// * the index of this submessage in [Message.submessages];
25
- // * the [WidgetType] of the first [Message.submessages].
29
+ // * the parsed [WidgetData] from the first [Message.submessages].
26
30
final String content;
27
31
// final int messageId; // ignored; redundant with [Message.id]
28
32
final int senderId;
@@ -265,3 +269,125 @@ enum PollEventSubmessageType {
265
269
static final _byRawString = _$PollEventSubmessageTypeEnumMap
266
270
.map ((key, value) => MapEntry (value, key));
267
271
}
272
+
273
+ /// States of a poll Zulip widget.
274
+ ///
275
+ /// See also:
276
+ /// - https://zulip.com/help/create-a-poll
277
+ /// - https://github.com/zulip/zulip/blob/304d948416465c1a085122af5d752f03d6797003/web/shared/src/poll_data.ts
278
+ class Poll {
279
+ Poll ({
280
+ required this .pollSenderId,
281
+ required this .question,
282
+ required final List <String > options,
283
+ }) {
284
+ for (int index = 0 ; index < options.length; index += 1 ) {
285
+ // Initial poll options use a placeholder senderId.
286
+ // See [PollEventSubmessage.optionKey] for details.
287
+ _addOption (null , options[index], optionIndex: index);
288
+ }
289
+ }
290
+
291
+ factory Poll .fromSubmessages (
292
+ List <Submessage > submessages,
293
+ {required int senderId, required PollWidgetData widgetData}
294
+ ) {
295
+ assert (submessages.isNotEmpty);
296
+ final widgetEventSubmessages = submessages.skip (1 );
297
+
298
+ final poll = Poll (
299
+ pollSenderId: senderId,
300
+ question: widgetData.extraData.question,
301
+ options: widgetData.extraData.options,
302
+ );
303
+
304
+ for (final submessage in widgetEventSubmessages) {
305
+ final event = PollEventSubmessage .fromJson (jsonDecode (submessage.content) as Map <String , Object ?>);
306
+ poll.applyEvent (submessage.senderId, event);
307
+ }
308
+ return poll;
309
+ }
310
+
311
+ final int pollSenderId;
312
+ String question;
313
+
314
+ /// The limit of options any single user can add to a poll.
315
+ ///
316
+ /// See https://github.com/zulip/zulip/blob/304d948416465c1a085122af5d752f03d6797003/web/shared/src/poll_data.ts#L69-L71
317
+ static const maxOptionIndex = 1000 ; // TODO validate
318
+
319
+ Iterable <PollOption > get options => _options.values;
320
+ final Set <String > _optionNames = {};
321
+ final Map <String , PollOption > _options = {};
322
+
323
+ void applyEvent (int senderId, PollEventSubmessage event) {
324
+ switch (event) {
325
+ case PollNewOptionEventSubmessage ():
326
+ _addOption (
327
+ senderId,
328
+ event.option,
329
+ optionIndex: event.latestOptionIndex,
330
+ );
331
+
332
+ case PollQuestionEventSubmessage ():
333
+ if (senderId != pollSenderId) {
334
+ // Only the message owner can edit the question.
335
+ assert (debugLog ('unexpected poll data: user $senderId is not allowed to edit the question' )); // TODO(log)
336
+ return ;
337
+ }
338
+
339
+ question = event.question;
340
+
341
+ case PollVoteEventSubmessage ():
342
+ final option = _options[event.key];
343
+ if (option == null ) {
344
+ assert (debugLog ('vote for unknown key ${event .key }' )); // TODO(log)
345
+ return ;
346
+ }
347
+
348
+ switch (event.op) {
349
+ case PollVoteOp .add:
350
+ option.voters.add (senderId);
351
+ case PollVoteOp .remove:
352
+ option.voters.remove (senderId);
353
+ case PollVoteOp .unknown:
354
+ }
355
+
356
+ case UnknownPollEventSubmessage ():
357
+ }
358
+ }
359
+
360
+ void _addOption (int ? senderId, String option, {required int optionIndex}) {
361
+ if (optionIndex > maxOptionIndex) return ;
362
+ final key = PollEventSubmessage .optionKey (senderId: senderId, optionIndex: optionIndex);
363
+ // The web client suppresses duplicate options, which can be created through
364
+ // the /poll command as there is no server-side validation.
365
+ if (_optionNames.contains (option)) return ;
366
+ assert (! _options.containsKey (key));
367
+ _options[key] = PollOption (text: option);
368
+ _optionNames.add (option);
369
+ }
370
+ }
371
+
372
+ class PollOption {
373
+ PollOption ({required this .text});
374
+
375
+ factory PollOption .withVoters (String text, Iterable <int > voters) =>
376
+ PollOption (text: text)..voters.addAll (voters);
377
+
378
+ final String text;
379
+ final Set <int > voters = {};
380
+
381
+ @override
382
+ bool operator == (Object other) {
383
+ if (other is ! PollOption ) return false ;
384
+
385
+ return other.hashCode == hashCode;
386
+ }
387
+
388
+ @override
389
+ int get hashCode => Object .hash ('Option' , text, voters.join (',' ));
390
+
391
+ @override
392
+ String toString () => 'Option(option: $text , voters: {${voters .join (', ' )}})' ;
393
+ }
0 commit comments