-
Notifications
You must be signed in to change notification settings - Fork 470
Expand file tree
/
Copy pathchannel.dart
More file actions
697 lines (613 loc) · 26.6 KB
/
Copy pathchannel.dart
File metadata and controls
697 lines (613 loc) · 26.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
import 'dart:collection';
import 'package:flutter/foundation.dart';
import '../api/model/events.dart';
import '../api/model/initial_snapshot.dart';
import '../api/model/model.dart';
import '../generated/l10n/zulip_localizations.dart';
import 'realm.dart';
import 'store.dart';
import 'user.dart';
/// The portion of [PerAccountStore] for channels, topics, and stuff about them.
///
/// This type is useful for expressing the needs of other parts of the
/// implementation of [PerAccountStore], to avoid circularity.
///
/// The data structures described here are implemented at [ChannelStoreImpl].
mixin ChannelStore on UserStore {
@protected
UserStore get userStore;
/// All known channels, indexed by [ZulipStream.streamId].
///
/// The same [ZulipStream] objects also appear in [channelsByName].
///
/// For channels the self-user is subscribed to, the value is in fact
/// a [Subscription] object and also appears in [subscriptions].
Map<int, ZulipStream> get channels;
/// All known channels, indexed by [ZulipStream.name].
///
/// The same [ZulipStream] objects also appear in [channels].
///
/// For channels the self-user is subscribed to, the value is in fact
/// a [Subscription] object and also appears in [subscriptions].
Map<String, ZulipStream> get channelsByName;
/// All the channels the self-user is subscribed to, indexed by
/// [Subscription.streamId], with subscription details.
///
/// The same [Subscription] objects are among the values in [channels]
/// and [channelsByName].
Map<int, Subscription> get subscriptions;
/// All the channel folders, including archived ones, indexed by ID.
Map<int, ChannelFolder> get channelFolders;
static int compareChannelsByName(ZulipStream a, ZulipStream b) {
// A user gave feedback wanting zulip-flutter to match web in putting
// emoji-prefixed channels first; see #1202.
// TODO(#1165) for matching web's ordering completely, which
// (for the all-channels view) I think just means locale-aware sorting.
final aStartsWithEmoji = _startsWithEmojiRegex.hasMatch(a.name);
final bStartsWithEmoji = _startsWithEmojiRegex.hasMatch(b.name);
if (aStartsWithEmoji && !bStartsWithEmoji) return -1;
if (!aStartsWithEmoji && bStartsWithEmoji) return 1;
return a.name.toLowerCase().compareTo(b.name.toLowerCase());
}
// TODO(linter): The linter incorrectly flags the following regexp string
// as invalid. See: https://github.com/dart-lang/sdk/issues/61246
// ignore: valid_regexps
static final _startsWithEmojiRegex = RegExp(r'^\p{Emoji}', unicode: true);
/// The visibility policy that the self-user has for the given topic.
///
/// This does not incorporate the user's channel-level policy,
/// and is mainly used in the implementation of other [ChannelStore] methods.
///
/// For policies directly applicable in the UI, see
/// [isTopicVisibleInStream] and [isTopicVisible].
///
/// Topics are treated case-insensitively; see [TopicName.isSameAs].
UserTopicVisibilityPolicy topicVisibilityPolicy(int streamId, TopicName topic);
/// The raw data structure underlying [topicVisibilityPolicy].
///
/// This is sometimes convenient for checks in tests.
/// It differs from [topicVisibilityPolicy] in on the one hand omitting
/// all topics where the value would be [UserTopicVisibilityPolicy.none],
/// and on the other hand being a concrete, finite data structure that
/// can be compared using `deepEquals`.
@visibleForTesting
Map<int, Map<TopicName, UserTopicVisibilityPolicy>> get debugTopicVisibility;
/// Whether this topic should appear when already focusing on its stream.
///
/// This is determined purely by the user's visibility policy for the topic.
///
/// This function is appropriate for muting calculations in UI contexts that
/// are already specific to a stream: for example the stream's unread count,
/// or the message list in the stream's narrow.
///
/// For UI contexts that are not specific to a particular stream, see
/// [isTopicVisible].
bool isTopicVisibleInStream(int streamId, TopicName topic) {
return _isTopicVisibleInStream(topicVisibilityPolicy(streamId, topic));
}
/// Whether the given event will change the result of [isTopicVisibleInStream]
/// for its stream and topic, compared to the current state.
UserTopicVisibilityEffect willChangeIfTopicVisibleInStream(UserTopicEvent event) {
final streamId = event.streamId;
final topic = event.topicName;
return UserTopicVisibilityEffect._fromBeforeAfter(
_isTopicVisibleInStream(topicVisibilityPolicy(streamId, topic)),
_isTopicVisibleInStream(event.visibilityPolicy));
}
static bool _isTopicVisibleInStream(UserTopicVisibilityPolicy policy) {
switch (policy) {
case UserTopicVisibilityPolicy.none:
return true;
case UserTopicVisibilityPolicy.muted:
return false;
case UserTopicVisibilityPolicy.unmuted:
case UserTopicVisibilityPolicy.followed:
return true;
case UserTopicVisibilityPolicy.unknown:
assert(false);
return true;
}
}
/// Whether this topic should appear when not specifically focusing
/// on this stream.
///
/// This takes into account the user's visibility policy for the stream
/// overall, as well as their policy for this topic.
///
/// For UI contexts that are specific to a particular stream, see
/// [isTopicVisibleInStream].
bool isTopicVisible(int streamId, TopicName topic) {
return _isTopicVisible(streamId, topicVisibilityPolicy(streamId, topic));
}
/// Whether the given event will change the result of [isTopicVisible]
/// for its stream and topic, compared to the current state.
UserTopicVisibilityEffect willChangeIfTopicVisible(UserTopicEvent event) {
final streamId = event.streamId;
final topic = event.topicName;
return UserTopicVisibilityEffect._fromBeforeAfter(
_isTopicVisible(streamId, topicVisibilityPolicy(streamId, topic)),
_isTopicVisible(streamId, event.visibilityPolicy));
}
bool _isTopicVisible(int streamId, UserTopicVisibilityPolicy policy) {
switch (policy) {
case UserTopicVisibilityPolicy.none:
switch (subscriptions[streamId]?.isMuted) {
case false: return true;
case true: return false;
case null: return false; // not subscribed; treat like muted
}
case UserTopicVisibilityPolicy.muted:
return false;
case UserTopicVisibilityPolicy.unmuted:
case UserTopicVisibilityPolicy.followed:
return true;
case UserTopicVisibilityPolicy.unknown:
assert(false);
return true;
}
}
/// The channel folder of [channelId],
/// including the "PINNED" or "OTHER" pseudo-channels (e.g. for the inbox).
UiChannelFolder uiChannelFolder(int channelId) =>
switch (channels[channelId]) {
Subscription(:final pinToTop) when pinToTop =>
UiChannelFolderPseudoPinned(),
ZulipStream(:final folderId) when folderId != null =>
UiChannelFolderRealmFolder(id: folderId),
_ => UiChannelFolderPseudoOther(),
};
/// A compare function for [UiChannelFolder]s,
/// using [ChannelFolder.order] for realm channel folders.
///
/// Puts "PINNED CHANNELS" first,
/// then realm channel folders by [ChannelFolder.order]
/// (or alphabetically if that's absent),
/// then "OTHER CHANNELS".
// TODO(server-11) Once [ChannelFolder.order] is required,
// remove alphabetical sorting and update dartdoc.
int compareUiChannelFolders(UiChannelFolder a, UiChannelFolder b) {
switch ((a, b)) {
case (UiChannelFolderPseudoPinned(), _): return -1;
case (_, UiChannelFolderPseudoPinned()): return 1;
case (UiChannelFolderPseudoOther(), _): return 1;
case (_, UiChannelFolderPseudoOther()): return -1;
case (
UiChannelFolderRealmFolder(id: final idA),
UiChannelFolderRealmFolder(id: final idB),
):
final folderA = channelFolders[idA];
final folderB = channelFolders[idB];
if (folderA == null || folderB == null) { // TODO(log)
assert(false);
return 0;
}
return switch ((folderA.order, folderB.order)) {
(null, null) => folderA.name.toLowerCase().compareTo(folderB.name.toLowerCase()),
(null, int()) => -1,
(int(), null) => 1,
(int a, int b) => a.compareTo(b),
};
}
}
bool selfHasContentAccess(ZulipStream channel) {
// Compare web's stream_data.has_content_access.
if (channel.isWebPublic) return true;
if (channel is Subscription) return true;
// Here web calls has_metadata_access... but that always returns true,
// as its comment says.
if (selfUser.role == UserRole.guest) return false;
if (!channel.inviteOnly) return true;
return _selfHasContentAccessViaGroupPermissions(channel);
}
bool _selfHasContentAccessViaGroupPermissions(ZulipStream channel) {
// Compare web's stream_data.has_content_access_via_group_permissions.
if (selfHasPermissionForGroupSetting(channel.canAddSubscribersGroup,
GroupSettingType.stream, 'can_add_subscribers_group')) {
return true;
}
if (selfHasPermissionForGroupSetting(channel.canSubscribeGroup,
GroupSettingType.stream, 'can_subscribe_group')) {
return true;
}
return false;
}
bool selfCanSendMessage({
required ZulipStream inChannel,
required DateTime byDate,
}) {
// (selfHasPermissionForGroupSetting isn't equipped to handle the old-server
// fallback logic for this specific permission; it's dynamic and depends on
// channelPostPolicy, so we do our own null check here.)
if (inChannel.canSendMessageGroup != null) {
return selfHasPermissionForGroupSetting(inChannel.canSendMessageGroup!,
GroupSettingType.stream, 'can_send_message_group');
} else if (inChannel.channelPostPolicy != null) {
return _selfPassesLegacyChannelPostPolicy(inChannel: inChannel, atDate: byDate);
} else {
assert(false); // TODO(log)
return true;
}
}
bool _selfPassesLegacyChannelPostPolicy({
required ZulipStream inChannel,
required DateTime atDate,
}) {
assert(inChannel.channelPostPolicy != null);
final role = selfUser.role;
// (Could early-return true on [UserRole.unknown],
// but pre-333 servers shouldn't be giving us an unknown role.)
switch (inChannel.channelPostPolicy!) {
case ChannelPostPolicy.any: return true;
case ChannelPostPolicy.fullMembers: {
if (!role.isAtLeast(UserRole.member)) return false;
if (role == UserRole.member) {
return selfHasPassedWaitingPeriod(byDate: atDate);
}
return true;
}
case ChannelPostPolicy.moderators: return role.isAtLeast(UserRole.moderator);
case ChannelPostPolicy.administrators: return role.isAtLeast(UserRole.administrator);
case ChannelPostPolicy.unknown: return true;
}
}
}
/// Whether and how a given [UserTopicEvent] will affect the results
/// that [ChannelStore.isTopicVisible] or [ChannelStore.isTopicVisibleInStream]
/// would give for some messages.
enum UserTopicVisibilityEffect {
/// The event will have no effect on the visibility results.
none,
/// The event will change some visibility results from true to false.
muted,
/// The event will change some visibility results from false to true.
unmuted;
factory UserTopicVisibilityEffect._fromBeforeAfter(bool before, bool after) {
return switch ((before, after)) {
(false, true) => UserTopicVisibilityEffect.unmuted,
(true, false) => UserTopicVisibilityEffect.muted,
_ => UserTopicVisibilityEffect.none,
};
}
}
/// A realm-level channel folder or the "PINNED" or "OTHER" channel folder.
///
/// See the Help Center doc:
/// https://zulip.com/help/channel-folders
sealed class UiChannelFolder {
const UiChannelFolder();
/// This folder's name, not yet UPPERCASED for the UI.
String name({
required ChannelStore store,
required ZulipLocalizations zulipLocalizations,
});
}
class UiChannelFolderPseudoPinned extends UiChannelFolder {
@override
String name({required store, required zulipLocalizations}) =>
zulipLocalizations.pinnedChannelsFolderName;
@override
bool operator ==(Object other) {
if (other is! UiChannelFolderPseudoPinned) return false;
// Conceptually there's only one value of this type.
return true;
}
@override
int get hashCode => 'UiChannelFolderPseudoPinned'.hashCode;
}
class UiChannelFolderPseudoOther extends UiChannelFolder {
@override
String name({required store, required zulipLocalizations}) =>
zulipLocalizations.otherChannelsFolderName;
@override
bool operator ==(Object other) {
if (other is! UiChannelFolderPseudoOther) return false;
// Conceptually there's only one value of this type.
return true;
}
@override
int get hashCode => 'UiChannelFolderPseudoOther'.hashCode;
}
class UiChannelFolderRealmFolder extends UiChannelFolder {
const UiChannelFolderRealmFolder({required this.id});
final int id;
@override
String name({required store, required zulipLocalizations}) {
final folder = store.channelFolders[id];
return folder!.name;
}
@override
bool operator ==(Object other) {
if (other is! UiChannelFolderRealmFolder) return false;
return id == other.id;
}
@override
int get hashCode => Object.hash('UiChannelFolderRealmFolder', id);
}
mixin ProxyChannelStore on ChannelStore {
@protected
ChannelStore get channelStore;
@override
Map<int, ZulipStream> get channels => channelStore.channels;
@override
Map<String, ZulipStream> get channelsByName => channelStore.channelsByName;
@override
Map<int, Subscription> get subscriptions => channelStore.subscriptions;
@override
Map<int, ChannelFolder> get channelFolders => channelStore.channelFolders;
@override
UserTopicVisibilityPolicy topicVisibilityPolicy(int streamId, TopicName topic) =>
channelStore.topicVisibilityPolicy(streamId, topic);
@override
Map<int, Map<TopicName, UserTopicVisibilityPolicy>> get debugTopicVisibility =>
channelStore.debugTopicVisibility;
}
/// A base class for [PerAccountStore] substores
/// that need access to [ChannelStore] as well as to its prerequisites
/// [CorePerAccountStore], [RealmStore], and [UserStore].
abstract class HasChannelStore extends HasUserStore with ChannelStore, ProxyChannelStore {
HasChannelStore({required ChannelStore channels})
: channelStore = channels, super(users: channels.userStore);
@protected
@override
final ChannelStore channelStore;
}
/// The implementation of [ChannelStore] that does the work.
///
/// Generally the only code that should need this class is [PerAccountStore]
/// itself. Other code accesses this functionality through [PerAccountStore],
/// or through the mixin [ChannelStore] which describes its interface.
class ChannelStoreImpl extends HasUserStore with ChannelStore {
factory ChannelStoreImpl({
required UserStore users,
required InitialSnapshot initialSnapshot,
}) {
final subscriptions = Map.fromEntries(initialSnapshot.subscriptions.map(
(subscription) => MapEntry(subscription.streamId, subscription)));
final channels = Map<int, ZulipStream>.of(subscriptions);
for (final stream in initialSnapshot.streams) {
channels.putIfAbsent(stream.streamId, () => stream);
}
final channelFolders = Map.fromEntries((initialSnapshot.channelFolders ?? [])
.map((channelFolder) => MapEntry(channelFolder.id, channelFolder)));
final topicVisibility = <int, TopicKeyedMap<UserTopicVisibilityPolicy>>{};
for (final item in initialSnapshot.userTopics) {
if (_warnInvalidVisibilityPolicy(item.visibilityPolicy)) {
// Not a value we expect. Keep it out of our data structures. // TODO(log)
continue;
}
final forStream = topicVisibility.putIfAbsent(item.streamId, () => makeTopicKeyedMap());
forStream[item.topicName] = item.visibilityPolicy;
}
return ChannelStoreImpl._(
users: users,
channels: channels,
channelsByName: channels.map((_, channel) => MapEntry(channel.name, channel)),
subscriptions: subscriptions,
channelFolders: channelFolders,
topicVisibility: topicVisibility,
);
}
ChannelStoreImpl._({
required super.users,
required this.channels,
required this.channelsByName,
required this.subscriptions,
required this.channelFolders,
required this.topicVisibility,
});
@override
final Map<int, ZulipStream> channels;
@override
final Map<String, ZulipStream> channelsByName;
@override
final Map<int, Subscription> subscriptions;
@override
final Map<int, ChannelFolder> channelFolders;
@override
Map<int, TopicKeyedMap<UserTopicVisibilityPolicy>> get debugTopicVisibility => topicVisibility;
final Map<int, TopicKeyedMap<UserTopicVisibilityPolicy>> topicVisibility;
@override
UserTopicVisibilityPolicy topicVisibilityPolicy(int streamId, TopicName topic) {
return topicVisibility[streamId]?[topic] ?? UserTopicVisibilityPolicy.none;
}
static bool _warnInvalidVisibilityPolicy(UserTopicVisibilityPolicy visibilityPolicy) {
if (visibilityPolicy == UserTopicVisibilityPolicy.unknown) {
// Not a value we expect. Keep it out of our data structures. // TODO(log)
return true;
}
return false;
}
void handleChannelEvent(ChannelEvent event) {
switch (event) {
case ChannelCreateEvent():
assert(event.streams.every((stream) =>
!channels.containsKey(stream.streamId)
&& !channelsByName.containsKey(stream.name)));
channels.addEntries(event.streams.map((stream) => MapEntry(stream.streamId, stream)));
channelsByName.addEntries(event.streams.map((stream) => MapEntry(stream.name, stream)));
// (Don't touch `subscriptions`. If the user is subscribed to the stream,
// details will come in a later `subscription` event.)
case ChannelDeleteEvent():
for (final channelId in event.channelIds) {
final channel = channels.remove(channelId);
if (channel == null) continue; // TODO(log)
assert(channelId == channel.streamId);
assert(identical(channel, channelsByName[channel.name]));
assert(subscriptions[channelId] == null
|| identical(subscriptions[channelId], channel));
channelsByName.remove(channel.name);
subscriptions.remove(channelId);
}
case ChannelUpdateEvent():
final stream = channels[event.streamId];
if (stream == null) return; // TODO(log)
assert(stream.streamId == event.streamId);
if (event.renderedDescription != null) {
stream.renderedDescription = event.renderedDescription!;
}
if (event.historyPublicToSubscribers != null) {
stream.historyPublicToSubscribers = event.historyPublicToSubscribers!;
}
if (event.isWebPublic != null) {
stream.isWebPublic = event.isWebPublic!;
}
if (event.property == null) {
// unrecognized property; do nothing
return;
}
switch (event.property!) {
case ChannelPropertyName.name:
final streamName = stream.name;
assert(streamName == event.name);
assert(identical(channels[stream.streamId], channelsByName[streamName]));
stream.name = event.value as String;
channelsByName.remove(streamName);
channelsByName[stream.name] = stream;
case ChannelPropertyName.isArchived:
stream.isArchived = event.value as bool;
case ChannelPropertyName.description:
stream.description = event.value as String;
case ChannelPropertyName.firstMessageId:
stream.firstMessageId = event.value as int?;
case ChannelPropertyName.inviteOnly:
stream.inviteOnly = event.value as bool;
case ChannelPropertyName.messageRetentionDays:
stream.messageRetentionDays = event.value as int?;
case ChannelPropertyName.channelPostPolicy:
stream.channelPostPolicy = event.value as ChannelPostPolicy;
case ChannelPropertyName.folderId:
stream.folderId = event.value as int?;
case ChannelPropertyName.canAddSubscribersGroup:
stream.canAddSubscribersGroup = event.value as GroupSettingValue;
case ChannelPropertyName.canDeleteAnyMessageGroup:
stream.canDeleteAnyMessageGroup = event.value as GroupSettingValue;
case ChannelPropertyName.canDeleteOwnMessageGroup:
stream.canDeleteOwnMessageGroup = event.value as GroupSettingValue;
case ChannelPropertyName.canSendMessageGroup:
stream.canSendMessageGroup = event.value as GroupSettingValue;
case ChannelPropertyName.canSubscribeGroup:
stream.canSubscribeGroup = event.value as GroupSettingValue;
case ChannelPropertyName.isRecentlyActive:
stream.isRecentlyActive = event.value as bool;
case ChannelPropertyName.streamWeeklyTraffic:
stream.streamWeeklyTraffic = event.value as int?;
}
}
}
void handleSubscriptionEvent(SubscriptionEvent event) {
switch (event) {
case SubscriptionAddEvent():
for (final subscription in event.subscriptions) {
assert(channels.containsKey(subscription.streamId));
assert(channels[subscription.streamId] is! Subscription);
assert(channelsByName.containsKey(subscription.name));
assert(channelsByName[subscription.name] is! Subscription);
assert(!subscriptions.containsKey(subscription.streamId));
channels[subscription.streamId] = subscription;
channelsByName[subscription.name] = subscription;
subscriptions[subscription.streamId] = subscription;
}
case SubscriptionRemoveEvent():
for (final channelId in event.channelIds) {
assert(channels.containsKey(channelId));
assert(channels[channelId] is Subscription);
assert(channelsByName.containsKey(channels[channelId]!.name));
assert(channelsByName[channels[channelId]!.name] is Subscription);
assert(subscriptions.containsKey(channelId));
final subscription = subscriptions.remove(channelId);
if (subscription == null) continue; // TODO(log)
final stream = ZulipStream.fromSubscription(subscription);
channels[channelId] = stream;
channelsByName[subscription.name] = stream;
}
case SubscriptionUpdateEvent():
final subscription = subscriptions[event.channelId];
if (subscription == null) return; // TODO(log)
assert(identical(channels[event.channelId], subscription));
assert(identical(channelsByName[subscription.name], subscription));
switch (event.property) {
case SubscriptionProperty.color:
subscription.color = event.value as int;
case SubscriptionProperty.isMuted:
// TODO(#1255) update [MessageListView] if affected
subscription.isMuted = event.value as bool;
case SubscriptionProperty.pinToTop:
subscription.pinToTop = event.value as bool;
case SubscriptionProperty.desktopNotifications:
subscription.desktopNotifications = event.value as bool;
case SubscriptionProperty.audibleNotifications:
subscription.audibleNotifications = event.value as bool;
case SubscriptionProperty.pushNotifications:
subscription.pushNotifications = event.value as bool;
case SubscriptionProperty.emailNotifications:
subscription.emailNotifications = event.value as bool;
case SubscriptionProperty.wildcardMentionsNotify:
subscription.wildcardMentionsNotify = event.value as bool;
case SubscriptionProperty.unknown:
// unrecognized property; do nothing
return;
}
case SubscriptionPeerAddEvent():
case SubscriptionPeerRemoveEvent():
// We don't currently store the data these would update; that's #374.
}
}
void handleChannelFolderEvent(ChannelFolderEvent event) {
switch (event) {
case ChannelFolderAddEvent():
final newChannelFolder = event.channelFolder;
channelFolders[newChannelFolder.id] = newChannelFolder;
case ChannelFolderUpdateEvent():
final change = event.data;
final channelFolder = channelFolders[event.channelFolderId];
if (channelFolder == null) return; // TODO(log)
if (change.name != null) channelFolder.name = change.name!;
if (change.description != null) channelFolder.description = change.description!;
if (change.renderedDescription != null) channelFolder.renderedDescription = change.renderedDescription!;
if (change.isArchived != null) channelFolder.isArchived = change.isArchived!;
case ChannelFolderReorderEvent():
final order = event.order;
for (int i = 0; i < order.length; i++) {
final id = order[i];
final channelFolder = channelFolders[id];
if (channelFolder == null) continue; // TODO(log)
channelFolder.order = i;
}
}
}
void handleUserTopicEvent(UserTopicEvent event) {
UserTopicVisibilityPolicy visibilityPolicy = event.visibilityPolicy;
if (_warnInvalidVisibilityPolicy(visibilityPolicy)) {
visibilityPolicy = UserTopicVisibilityPolicy.none;
}
if (visibilityPolicy == UserTopicVisibilityPolicy.none) {
// This is the "zero value" for this type, which our data structure
// represents by leaving the topic out entirely.
final forStream = topicVisibility[event.streamId];
if (forStream == null) return;
forStream.remove(event.topicName);
if (forStream.isEmpty) {
topicVisibility.remove(event.streamId);
}
} else {
final forStream = topicVisibility.putIfAbsent(event.streamId, () => makeTopicKeyedMap());
forStream[event.topicName] = visibilityPolicy;
}
}
}
/// A [Map] with [TopicName] keys and [V] values.
///
/// When one of these is created by [makeTopicKeyedMap],
/// key equality is done case-insensitively; see there.
///
/// This type should only be used for maps created by [makeTopicKeyedMap].
/// It would be nice to enforce that.
typedef TopicKeyedMap<V> = Map<TopicName, V>;
/// Make a case-insensitive, case-preserving [TopicName]-keyed [LinkedHashMap].
///
/// The equality function is [TopicName.isSameAs],
/// and the hash code is [String.hashCode] of [TopicName.canonicalize].
TopicKeyedMap<V> makeTopicKeyedMap<V>() => LinkedHashMap<TopicName, V>(
equals: (a, b) => a.isSameAs(b),
hashCode: (k) => k.canonicalize().hashCode,
);