-
Notifications
You must be signed in to change notification settings - Fork 255
/
Copy pathchannel.dart
366 lines (330 loc) · 14.7 KB
/
channel.dart
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
import 'package:flutter/foundation.dart';
import '../api/model/events.dart';
import '../api/model/initial_snapshot.dart';
import '../api/model/model.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 {
/// All known channels/streams, indexed by [ZulipStream.streamId].
///
/// The same [ZulipStream] objects also appear in [streamsByName].
///
/// 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 streams;
/// All known channels/streams, indexed by [ZulipStream.name].
///
/// The same [ZulipStream] objects also appear in [streams].
///
/// 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 streamsByName;
/// 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 [streams]
/// and [streamsByName].
Map<int, Subscription> get subscriptions;
/// 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].
UserTopicVisibilityPolicy topicVisibilityPolicy(int streamId, String 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<String, 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, String 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.
VisibilityEffect willChangeIfTopicVisibleInStream(UserTopicEvent event) {
final streamId = event.streamId;
final topic = event.topicName;
return VisibilityEffect._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, String 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.
VisibilityEffect willChangeIfTopicVisible(UserTopicEvent event) {
final streamId = event.streamId;
final topic = event.topicName;
return VisibilityEffect._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;
}
}
}
/// Whether and how a given [UserTopicEvent] will affect the results
/// that [ChannelStore.isTopicVisible] or [ChannelStore.isTopicVisibleInStream]
/// would give for some messages.
enum VisibilityEffect {
/// 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 VisibilityEffect._fromBeforeAfter(bool before, bool after) {
return switch ((before, after)) {
(false, true) => VisibilityEffect.unmuted,
(true, false) => VisibilityEffect.muted,
_ => VisibilityEffect.none,
};
}
}
/// 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 with ChannelStore {
factory ChannelStoreImpl({required InitialSnapshot initialSnapshot}) {
final subscriptions = Map.fromEntries(initialSnapshot.subscriptions.map(
(subscription) => MapEntry(subscription.streamId, subscription)));
final streams = Map<int, ZulipStream>.of(subscriptions);
for (final stream in initialSnapshot.streams) {
streams.putIfAbsent(stream.streamId, () => stream);
}
final topicVisibility = <int, Map<String, UserTopicVisibilityPolicy>>{};
for (final item in initialSnapshot.userTopics ?? const <UserTopicItem>[]) {
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, () => {});
forStream[item.topicName] = item.visibilityPolicy;
}
return ChannelStoreImpl._(
streams: streams,
streamsByName: streams.map((_, stream) => MapEntry(stream.name, stream)),
subscriptions: subscriptions,
topicVisibility: topicVisibility,
);
}
ChannelStoreImpl._({
required this.streams,
required this.streamsByName,
required this.subscriptions,
required this.topicVisibility,
});
@override
final Map<int, ZulipStream> streams;
@override
final Map<String, ZulipStream> streamsByName;
@override
final Map<int, Subscription> subscriptions;
@override
Map<int, Map<String, UserTopicVisibilityPolicy>> get debugTopicVisibility => topicVisibility;
final Map<int, Map<String, UserTopicVisibilityPolicy>> topicVisibility;
@override
UserTopicVisibilityPolicy topicVisibilityPolicy(int streamId, String 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) =>
!streams.containsKey(stream.streamId)
&& !streamsByName.containsKey(stream.name)));
streams.addEntries(event.streams.map((stream) => MapEntry(stream.streamId, stream)));
streamsByName.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 stream in event.streams) {
assert(identical(streams[stream.streamId], streamsByName[stream.name]));
assert(subscriptions[stream.streamId] == null
|| identical(subscriptions[stream.streamId], streams[stream.streamId]));
streams.remove(stream.streamId);
streamsByName.remove(stream.name);
subscriptions.remove(stream.streamId);
}
case ChannelUpdateEvent():
final stream = streams[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(streams[stream.streamId], streamsByName[streamName]));
stream.name = event.value as String;
streamsByName.remove(streamName);
streamsByName[stream.name] = stream;
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.canRemoveSubscribersGroup:
case ChannelPropertyName.canRemoveSubscribersGroupId:
stream.canRemoveSubscribersGroup = event.value as int?;
case ChannelPropertyName.streamWeeklyTraffic:
stream.streamWeeklyTraffic = event.value as int?;
}
}
}
void handleSubscriptionEvent(SubscriptionEvent event) {
switch (event) {
case SubscriptionAddEvent():
for (final subscription in event.subscriptions) {
assert(streams.containsKey(subscription.streamId));
assert(streams[subscription.streamId] is! Subscription);
assert(streamsByName.containsKey(subscription.name));
assert(streamsByName[subscription.name] is! Subscription);
assert(!subscriptions.containsKey(subscription.streamId));
streams[subscription.streamId] = subscription;
streamsByName[subscription.name] = subscription;
subscriptions[subscription.streamId] = subscription;
}
case SubscriptionRemoveEvent():
for (final streamId in event.streamIds) {
subscriptions.remove(streamId);
}
case SubscriptionUpdateEvent():
final subscription = subscriptions[event.streamId];
if (subscription == null) return; // TODO(log)
assert(identical(streams[event.streamId], subscription));
assert(identical(streamsByName[subscription.name], subscription));
switch (event.property) {
case SubscriptionProperty.color:
subscription.color = event.value as int;
case SubscriptionProperty.isMuted:
// TODO(#421) update [MessageListView] if affected
subscription.isMuted = event.value as bool;
case SubscriptionProperty.inHomeView:
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 handleUserTopicEvent(UserTopicEvent event) {
UserTopicVisibilityPolicy visibilityPolicy = event.visibilityPolicy;
if (_warnInvalidVisibilityPolicy(visibilityPolicy)) {
visibilityPolicy = UserTopicVisibilityPolicy.none;
}
// TODO(#421) update [MessageListView] if affected
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, () => {});
forStream[event.topicName] = visibilityPolicy;
}
}
}