-
Notifications
You must be signed in to change notification settings - Fork 309
unreads: Track total and per-stream unread counts, in addition to IDs #345
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
a7f7fff
cc76f2a
0ebaef1
aeb570f
ad074b2
848d233
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 | ||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -17,12 +17,13 @@ import 'narrow.dart'; | |||||||||||||||||||||||||||||
/// Callers should do their own filtering based on other state, like muting, | ||||||||||||||||||||||||||||||
/// as desired. | ||||||||||||||||||||||||||||||
/// | ||||||||||||||||||||||||||||||
/// In each component of this model ([streams], [dms], [mentions]), | ||||||||||||||||||||||||||||||
/// In each component of this model ([_totalCount], [streams], [dms], [mentions]), | ||||||||||||||||||||||||||||||
/// if a message is not represented, its status is either read | ||||||||||||||||||||||||||||||
/// or unknown to the component. In all components, | ||||||||||||||||||||||||||||||
/// a message's status will be unknown if, at /register time, | ||||||||||||||||||||||||||||||
/// it was very old by the server's reckoning. See [oldUnreadsMissing]. | ||||||||||||||||||||||||||||||
/// It may also be unknown for other reasons that differ by component; see each. | ||||||||||||||||||||||||||||||
/// In [mentions], there's another more complex reason | ||||||||||||||||||||||||||||||
/// the state might be unknown; see there. | ||||||||||||||||||||||||||||||
/// | ||||||||||||||||||||||||||||||
/// Messages in unsubscribed streams, and messages sent by muted users, | ||||||||||||||||||||||||||||||
/// are generally deemed read by the server and shouldn't be expected to appear. | ||||||||||||||||||||||||||||||
|
@@ -39,28 +40,37 @@ import 'narrow.dart'; | |||||||||||||||||||||||||||||
// messages and refresh [mentions] (see [mentions] dartdoc). | ||||||||||||||||||||||||||||||
class Unreads extends ChangeNotifier { | ||||||||||||||||||||||||||||||
factory Unreads({required UnreadMessagesSnapshot initial, required selfUserId}) { | ||||||||||||||||||||||||||||||
final streams = <int, Map<String, QueueList<int>>>{}; | ||||||||||||||||||||||||||||||
int totalCount = 0; | ||||||||||||||||||||||||||||||
final streams = <int, StreamUnreads>{}; | ||||||||||||||||||||||||||||||
final dms = <DmNarrow, QueueList<int>>{}; | ||||||||||||||||||||||||||||||
final mentions = Set.of(initial.mentions); | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
for (final unreadStreamSnapshot in initial.streams) { | ||||||||||||||||||||||||||||||
final streamId = unreadStreamSnapshot.streamId; | ||||||||||||||||||||||||||||||
final topic = unreadStreamSnapshot.topic; | ||||||||||||||||||||||||||||||
(streams[streamId] ??= {})[topic] = QueueList.from(unreadStreamSnapshot.unreadMessageIds); | ||||||||||||||||||||||||||||||
final streamUnreads = streams[streamId] ??= StreamUnreads.empty(); | ||||||||||||||||||||||||||||||
final unreadInTopic = unreadStreamSnapshot.unreadMessageIds.length; | ||||||||||||||||||||||||||||||
streamUnreads | ||||||||||||||||||||||||||||||
..topics[topic] = QueueList.from(unreadStreamSnapshot.unreadMessageIds) | ||||||||||||||||||||||||||||||
..count += unreadInTopic; | ||||||||||||||||||||||||||||||
totalCount += unreadInTopic; | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
for (final unreadDmSnapshot in initial.dms) { | ||||||||||||||||||||||||||||||
final otherUserId = unreadDmSnapshot.otherUserId; | ||||||||||||||||||||||||||||||
final narrow = DmNarrow.withUser(otherUserId, selfUserId: selfUserId); | ||||||||||||||||||||||||||||||
dms[narrow] = QueueList.from(unreadDmSnapshot.unreadMessageIds); | ||||||||||||||||||||||||||||||
totalCount += unreadDmSnapshot.unreadMessageIds.length; | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
for (final unreadHuddleSnapshot in initial.huddles) { | ||||||||||||||||||||||||||||||
final narrow = DmNarrow.ofUnreadHuddleSnapshot(unreadHuddleSnapshot, selfUserId: selfUserId); | ||||||||||||||||||||||||||||||
dms[narrow] = QueueList.from(unreadHuddleSnapshot.unreadMessageIds); | ||||||||||||||||||||||||||||||
totalCount += unreadHuddleSnapshot.unreadMessageIds.length; | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
return Unreads._( | ||||||||||||||||||||||||||||||
totalCount: totalCount, | ||||||||||||||||||||||||||||||
streams: streams, | ||||||||||||||||||||||||||||||
dms: dms, | ||||||||||||||||||||||||||||||
mentions: mentions, | ||||||||||||||||||||||||||||||
|
@@ -70,18 +80,27 @@ class Unreads extends ChangeNotifier { | |||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
Unreads._({ | ||||||||||||||||||||||||||||||
required totalCount, | ||||||||||||||||||||||||||||||
required this.streams, | ||||||||||||||||||||||||||||||
required this.dms, | ||||||||||||||||||||||||||||||
required this.mentions, | ||||||||||||||||||||||||||||||
required this.oldUnreadsMissing, | ||||||||||||||||||||||||||||||
required this.selfUserId, | ||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||
}) : _totalCount = totalCount; | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
// TODO excluded for now; would need to handle nuances around muting etc. | ||||||||||||||||||||||||||||||
// int count; | ||||||||||||||||||||||||||||||
/// Total unread messages. | ||||||||||||||||||||||||||||||
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. Hmm, but do we have a use case for this? I don't immediately see one — I feel like any place that we might want a count like this one, we'd actually want the version that accounts for muting. If we don't have a use case where it'd be valid to use, then given that it looks appealingly like something one might want in situations where in fact it'd be buggy to use it, it's probably best not to have it around. (If we did have reason to keep it around, I'd want the doc to clearly warn that it's not the "total number of unreads" one wants in the UI.) |
||||||||||||||||||||||||||||||
/// | ||||||||||||||||||||||||||||||
/// Prefer this when possible over traversing the model to make a sum. | ||||||||||||||||||||||||||||||
/// | ||||||||||||||||||||||||||||||
/// We initialize and maintain this ourselves, | ||||||||||||||||||||||||||||||
/// ignoring [UnreadMessagesSnapshot.count] because it has information gaps | ||||||||||||||||||||||||||||||
/// beyond the ones explained in [oldUnreadsMissing]: | ||||||||||||||||||||||||||||||
/// https://chat.zulip.org/#narrow/stream/378-api-design/topic/register.3A.20maintaining.20.60unread_msgs.2Ementions.60.20correctly/near/1668449 | ||||||||||||||||||||||||||||||
int get totalCount => _totalCount; | ||||||||||||||||||||||||||||||
int _totalCount; | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
/// Unread stream messages, as: stream ID → topic → message ID. | ||||||||||||||||||||||||||||||
final Map<int, Map<String, QueueList<int>>> streams; | ||||||||||||||||||||||||||||||
final Map<int, StreamUnreads> streams; | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
/// Unread DM messages, as: DM narrow → message ID. | ||||||||||||||||||||||||||||||
final Map<DmNarrow, QueueList<int>> dms; | ||||||||||||||||||||||||||||||
|
@@ -250,6 +269,7 @@ class Unreads extends ChangeNotifier { | |||||||||||||||||||||||||||||
switch (event) { | ||||||||||||||||||||||||||||||
case UpdateMessageFlagsAddEvent(): | ||||||||||||||||||||||||||||||
if (event.all) { | ||||||||||||||||||||||||||||||
_totalCount = 0; | ||||||||||||||||||||||||||||||
streams.clear(); | ||||||||||||||||||||||||||||||
dms.clear(); | ||||||||||||||||||||||||||||||
mentions.clear(); | ||||||||||||||||||||||||||||||
|
@@ -286,14 +306,23 @@ class Unreads extends ChangeNotifier { | |||||||||||||||||||||||||||||
.add(messageId); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
newlyUnreadInStreams.forEach((incomingStreamId, incomingTopics) { | ||||||||||||||||||||||||||||||
incomingTopics.forEach((incomingTopic, incomingMessageIds) { | ||||||||||||||||||||||||||||||
for ( | ||||||||||||||||||||||||||||||
final MapEntry(key: incomingStreamId, value: incomingTopics) | ||||||||||||||||||||||||||||||
in newlyUnreadInStreams.entries | ||||||||||||||||||||||||||||||
) { | ||||||||||||||||||||||||||||||
for ( | ||||||||||||||||||||||||||||||
final MapEntry(key: incomingTopic, value: incomingMessageIds) | ||||||||||||||||||||||||||||||
in incomingTopics.entries | ||||||||||||||||||||||||||||||
) { | ||||||||||||||||||||||||||||||
_addAllInStreamTopic(incomingMessageIds..sort(), incomingStreamId, incomingTopic); | ||||||||||||||||||||||||||||||
Comment on lines
+309
to
317
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. I wish I had a good idea for making these more concise, but I don't. :-/ Short of, like, an extension Can at least make them more compact vertically, though:
Suggested change
|
||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||
newlyUnreadInDms.forEach((incomingDmNarrow, incomingMessageIds) { | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
for ( | ||||||||||||||||||||||||||||||
final MapEntry(key: incomingDmNarrow, value: incomingMessageIds) | ||||||||||||||||||||||||||||||
in newlyUnreadInDms.entries | ||||||||||||||||||||||||||||||
) { | ||||||||||||||||||||||||||||||
_addAllInDm(incomingMessageIds..sort(), incomingDmNarrow); | ||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
notifyListeners(); | ||||||||||||||||||||||||||||||
|
@@ -302,62 +331,87 @@ class Unreads extends ChangeNotifier { | |||||||||||||||||||||||||||||
// TODO use efficient lookups | ||||||||||||||||||||||||||||||
bool _slowIsPresentInStreams(int messageId) { | ||||||||||||||||||||||||||||||
return streams.values.any( | ||||||||||||||||||||||||||||||
(topics) => topics.values.any( | ||||||||||||||||||||||||||||||
(streamUnreads) => streamUnreads.topics.values.any( | ||||||||||||||||||||||||||||||
(messageIds) => messageIds.contains(messageId), | ||||||||||||||||||||||||||||||
), | ||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
void _addLastInStreamTopic(int messageId, int streamId, String topic) { | ||||||||||||||||||||||||||||||
((streams[streamId] ??= {})[topic] ??= QueueList()).addLast(messageId); | ||||||||||||||||||||||||||||||
final streamUnreads = streams[streamId] ??= StreamUnreads.empty(); | ||||||||||||||||||||||||||||||
(streamUnreads.topics[topic] ??= QueueList()).addLast(messageId); | ||||||||||||||||||||||||||||||
streamUnreads.count++; | ||||||||||||||||||||||||||||||
_totalCount += 1; | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
// [messageIds] must be sorted ascending and without duplicates. | ||||||||||||||||||||||||||||||
void _addAllInStreamTopic(QueueList<int> messageIds, int streamId, String topic) { | ||||||||||||||||||||||||||||||
final topics = streams[streamId] ??= {}; | ||||||||||||||||||||||||||||||
topics.update(topic, | ||||||||||||||||||||||||||||||
ifAbsent: () => messageIds, | ||||||||||||||||||||||||||||||
int numAdded = 0; | ||||||||||||||||||||||||||||||
final streamUnreads = streams[streamId] ??= StreamUnreads.empty(); | ||||||||||||||||||||||||||||||
streamUnreads.topics.update(topic, | ||||||||||||||||||||||||||||||
ifAbsent: () { | ||||||||||||||||||||||||||||||
numAdded = messageIds.length; | ||||||||||||||||||||||||||||||
return messageIds; | ||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||
// setUnion dedupes existing and incoming unread IDs, | ||||||||||||||||||||||||||||||
// so we tolerate zulip/zulip#22164, fixed in 6.0 | ||||||||||||||||||||||||||||||
// TODO(server-6) remove 6.0 comment | ||||||||||||||||||||||||||||||
(existing) => setUnion(existing, messageIds), | ||||||||||||||||||||||||||||||
(existing) { | ||||||||||||||||||||||||||||||
final result = setUnion(existing, messageIds); | ||||||||||||||||||||||||||||||
numAdded = result.length - messageIds.length; | ||||||||||||||||||||||||||||||
return result; | ||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||
streamUnreads.count += numAdded; | ||||||||||||||||||||||||||||||
_totalCount += numAdded; | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
// TODO use efficient model lookups | ||||||||||||||||||||||||||||||
void _slowRemoveAllInStreams(Set<int> idsToRemove) { | ||||||||||||||||||||||||||||||
int totalRemoved = 0; | ||||||||||||||||||||||||||||||
final newlyEmptyStreams = []; | ||||||||||||||||||||||||||||||
streams.forEach((streamId, topics) { | ||||||||||||||||||||||||||||||
for (final MapEntry(key: streamId, value: streamUnreads) in streams.entries) { | ||||||||||||||||||||||||||||||
int removedInStream = 0; | ||||||||||||||||||||||||||||||
final newlyEmptyTopics = []; | ||||||||||||||||||||||||||||||
topics.forEach((topic, messageIds) { | ||||||||||||||||||||||||||||||
for (final MapEntry(key: topic, value: messageIds) in streamUnreads.topics.entries) { | ||||||||||||||||||||||||||||||
final lengthBefore = messageIds.length; | ||||||||||||||||||||||||||||||
messageIds.removeWhere((id) => idsToRemove.contains(id)); | ||||||||||||||||||||||||||||||
final removedInTopic = lengthBefore - messageIds.length; | ||||||||||||||||||||||||||||||
removedInStream += removedInTopic; | ||||||||||||||||||||||||||||||
totalRemoved += removedInTopic; | ||||||||||||||||||||||||||||||
if (messageIds.isEmpty) { | ||||||||||||||||||||||||||||||
newlyEmptyTopics.add(topic); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
for (final topic in newlyEmptyTopics) { | ||||||||||||||||||||||||||||||
topics.remove(topic); | ||||||||||||||||||||||||||||||
streamUnreads.topics.remove(topic); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
if (topics.isEmpty) { | ||||||||||||||||||||||||||||||
if (streamUnreads.topics.isEmpty) { | ||||||||||||||||||||||||||||||
newlyEmptyStreams.add(streamId); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||
streamUnreads.count -= removedInStream; | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
for (final streamId in newlyEmptyStreams) { | ||||||||||||||||||||||||||||||
streams.remove(streamId); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
_totalCount -= totalRemoved; | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
void _removeAllInStreamTopic(Set<int> incomingMessageIds, int streamId, String topic) { | ||||||||||||||||||||||||||||||
final topics = streams[streamId]; | ||||||||||||||||||||||||||||||
if (topics == null) return; | ||||||||||||||||||||||||||||||
final messageIds = topics[topic]; | ||||||||||||||||||||||||||||||
final streamUnreads = streams[streamId]; | ||||||||||||||||||||||||||||||
if (streamUnreads == null) return; | ||||||||||||||||||||||||||||||
final messageIds = streamUnreads.topics[topic]; | ||||||||||||||||||||||||||||||
if (messageIds == null) return; | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
// ([QueueList] doesn't have a `removeAll`) | ||||||||||||||||||||||||||||||
final lengthBefore = messageIds.length; | ||||||||||||||||||||||||||||||
messageIds.removeWhere((id) => incomingMessageIds.contains(id)); | ||||||||||||||||||||||||||||||
final numRemoved = lengthBefore - messageIds.length; | ||||||||||||||||||||||||||||||
streamUnreads.count -= numRemoved; | ||||||||||||||||||||||||||||||
_totalCount -= numRemoved; | ||||||||||||||||||||||||||||||
if (messageIds.isEmpty) { | ||||||||||||||||||||||||||||||
topics.remove(topic); | ||||||||||||||||||||||||||||||
if (topics.isEmpty) { | ||||||||||||||||||||||||||||||
streamUnreads.topics.remove(topic); | ||||||||||||||||||||||||||||||
if (streamUnreads.topics.isEmpty) { | ||||||||||||||||||||||||||||||
streams.remove(streamId); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
@@ -370,30 +424,61 @@ class Unreads extends ChangeNotifier { | |||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
void _addLastInDm(int messageId, DmNarrow narrow) { | ||||||||||||||||||||||||||||||
(dms[narrow] ??= QueueList()).addLast(messageId); | ||||||||||||||||||||||||||||||
_totalCount += 1; | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
// [messageIds] must be sorted ascending and without duplicates. | ||||||||||||||||||||||||||||||
void _addAllInDm(QueueList<int> messageIds, DmNarrow dmNarrow) { | ||||||||||||||||||||||||||||||
int numAdded = 0; | ||||||||||||||||||||||||||||||
dms.update(dmNarrow, | ||||||||||||||||||||||||||||||
ifAbsent: () => messageIds, | ||||||||||||||||||||||||||||||
ifAbsent: () { | ||||||||||||||||||||||||||||||
numAdded = messageIds.length; | ||||||||||||||||||||||||||||||
return messageIds; | ||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||
// setUnion dedupes existing and incoming unread IDs, | ||||||||||||||||||||||||||||||
// so we tolerate zulip/zulip#22164, fixed in 6.0 | ||||||||||||||||||||||||||||||
// TODO(server-6) remove 6.0 comment | ||||||||||||||||||||||||||||||
(existing) => setUnion(existing, messageIds), | ||||||||||||||||||||||||||||||
(existing) { | ||||||||||||||||||||||||||||||
final result = setUnion(existing, messageIds); | ||||||||||||||||||||||||||||||
numAdded = result.length - messageIds.length; | ||||||||||||||||||||||||||||||
return result; | ||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||
_totalCount += numAdded; | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
// TODO use efficient model lookups | ||||||||||||||||||||||||||||||
void _slowRemoveAllInDms(Set<int> idsToRemove) { | ||||||||||||||||||||||||||||||
int numRemoved = 0; | ||||||||||||||||||||||||||||||
final newlyEmptyDms = []; | ||||||||||||||||||||||||||||||
dms.forEach((dmNarrow, messageIds) { | ||||||||||||||||||||||||||||||
for (final MapEntry(key: dmNarrow, value: messageIds) in dms.entries) { | ||||||||||||||||||||||||||||||
final lengthBefore = messageIds.length; | ||||||||||||||||||||||||||||||
messageIds.removeWhere((id) => idsToRemove.contains(id)); | ||||||||||||||||||||||||||||||
numRemoved += lengthBefore - messageIds.length; | ||||||||||||||||||||||||||||||
if (messageIds.isEmpty) { | ||||||||||||||||||||||||||||||
newlyEmptyDms.add(dmNarrow); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
for (final dmNarrow in newlyEmptyDms) { | ||||||||||||||||||||||||||||||
dms.remove(dmNarrow); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
_totalCount -= numRemoved; | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
class StreamUnreads { | ||||||||||||||||||||||||||||||
StreamUnreads({required this.count, required this.topics}); | ||||||||||||||||||||||||||||||
StreamUnreads.empty() | ||||||||||||||||||||||||||||||
: count = 0, | ||||||||||||||||||||||||||||||
topics = {}; | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
/// Total unread messages in this stream. | ||||||||||||||||||||||||||||||
/// | ||||||||||||||||||||||||||||||
/// Prefer this when possible over traversing [topics] to make a sum. | ||||||||||||||||||||||||||||||
int count; | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
Map<String, QueueList<int>> topics; | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
@visibleForTesting | ||||||||||||||||||||||||||||||
Map<String, dynamic> toJson() => {'count': count, 'topics': topics}; | ||||||||||||||||||||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -63,6 +63,8 @@ Object? deepToJson(Object? object) { | |
result = object.map((x) => deepToJson(x)).toList(); | ||
case Map() when object.keys.every((k) => k is String): | ||
result = object.map((k, v) => MapEntry<String, dynamic>(k, deepToJson(v))); | ||
case Map() when object.keys.every((k) => k is int): | ||
result = object.map((k, v) => MapEntry<String, dynamic>(k.toString(), deepToJson(v))); | ||
Comment on lines
+66
to
+67
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. Hmm interesting. The idea in the existing code here is that if you have a Map whose keys aren't strings, that is not a valid JSON-like value — not a value that a E.g. see the
Does If not, but 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. In DartPad, this fails: import 'dart:convert';
main() {
print(jsonEncode({ 1: 123 }));
} with
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. Compare that with the output of this test on test('', () {
check({1: 1}).jsonEquals({1: 1});
});
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. That's a final Object? shallowlyConverted;
try {
shallowlyConverted = (object as dynamic).toJson();
} catch (e) {
throw JsonUnsupportedObjectError(object, cause: e);
} Inspecting its
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. Maybe it would help to make that 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. It looks like we give the same error as Which isn't a coincidence, because as the implementation comment says: Object? deepToJson(Object? object) {
// Implementation is based on the recursion underlying [jsonEncode],
// at [_JsonStringifier.writeObject] in the stdlib's convert/json.dart .
// (We leave out the cycle-checking, for simplicity / out of laziness.) and that includes largely throwing the same exceptions. I'm inclined to not try to optimize the error messages beyond what
because that doesn't seem to have been the scenario. |
||
default: | ||
return (null, false); | ||
} | ||
|
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.
nit: commit message can mark as NFC