forked from zulip/zulip-flutter
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathchannel.dart
230 lines (211 loc) · 9.17 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
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 {
Map<int, ZulipStream> get streams;
Map<String, ZulipStream> get streamsByName;
Map<int, Subscription> get subscriptions;
UserTopicVisibilityPolicy topicVisibilityPolicy(int streamId, String topic);
/// 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) {
switch (topicVisibilityPolicy(streamId, topic)) {
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) {
switch (topicVisibilityPolicy(streamId, topic)) {
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 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;
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 handleStreamEvent(StreamEvent event) {
switch (event) {
case StreamCreateEvent():
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 StreamDeleteEvent():
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);
}
}
}
void handleSubscriptionEvent(SubscriptionEvent event) {
switch (event) {
case SubscriptionAddEvent():
for (final subscription in event.subscriptions) {
assert(streams.containsKey(subscription.streamId)
&& streams[subscription.streamId] is! Subscription);
assert(streamsByName.containsKey(subscription.name)
&& 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;
}
}
}