-
Notifications
You must be signed in to change notification settings - Fork 270
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
msglist: Follow /with/ links through message moves #1029
Conversation
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.
Looks good overall! Left some comments.
test/model/message_list_test.dart
Outdated
generateMessages: (i) => eg.streamMessage( | ||
stream: someStream, | ||
topic: someTopic)), | ||
]; |
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.
Maybe we can refactor these test cases (and the ones added later) with a helper that takes these arguments. See #870 (comment).
test/example_data.dart
Outdated
const String recentZulipVersion = '8.0'; | ||
const int recentZulipFeatureLevel = 185; | ||
const String recentZulipVersion = '9.0'; | ||
const int recentZulipFeatureLevel = 271; |
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.
We should be able to drop this now.
test/model/message_list_test.dart
Outdated
final fetchFuture = model.fetchInitial(); | ||
check(model).fetched.isFalse(); | ||
|
||
checkNotNotified(); | ||
await fetchFuture; |
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.
Looks like we can use await model.fetchInitial()
here and the test below
lib/model/message_list.dart
Outdated
/// | ||
/// See API doc: | ||
/// https://zulip.com/api/construct-narrow#message-ids | ||
void _adjustTopicPermalinkNarrow(Narrow narrow_, Message? someFetchedMessageOrNull) { |
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.
Looks like the caller passes the same narrow
as the narrow
that lives on the message list. Should we remove narrow_
and just use narrow
instead?
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.
I agree it's odd for this function to have a narrow_
param; let's remove that.
It's convenient in the function's body to not keep using narrow
each time we need it, because then we'd have to repeat things like narrow as TopicNarrow
even in places where it's clear to us that the value can't be anything other than a TopicNarrow
. I think what's happening there is: when we read narrow
, the analyzer sees that as calling a getter, not accessing a variable, and it doesn't assume the getter will return the same value / Narrow
subtype from one call to the next. And while in theory the analyzer could conclude that this class's narrow
getter doesn't have that surprising behavior—it's just a field with the getter defined implicitly—it doesn't want to go and check for subclasses that might override the getter with that surprising behavior.
How about a narrow_
variable at the top of the function body, with a comment.
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.
Yeah, that explanation makes sense. I remember that making narrow
private might fix the issue, but I'm not sure. The proposed change sounds good to me too.
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.
Hmm nevermind, I think the local variable would be a better solution here.
See #808 (comment)
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.
Interesting; I like final narrow = this.narrow
in that example; I think I like it better than final narrow_ = narrow
.
test/widgets/message_list_test.dart
Outdated
}) async { | ||
addTearDown(testBinding.reset); | ||
streams ??= subscriptions ??= [eg.subscription(eg.stream(streamId: eg.defaultStreamMessageStreamId))]; | ||
await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot( | ||
streams: streams, subscriptions: subscriptions, unreadMsgs: unreadMsgs)); | ||
store = await testBinding.globalStore.perAccount(eg.selfAccount.id); | ||
connection = store.connection as FakeApiConnection; | ||
if (zulipFeatureLevel != null) { | ||
connection.zulipFeatureLevel = zulipFeatureLevel; |
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.
Hmm, I think eg.selfAccount
carries a different zulipFeatureLevel
if we only override the feature level on the connection
.
08b8e2f
to
0131961
Compare
Thanks for the review! Revision pushed. |
Thanks! This revision looks good to me. |
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.
Thanks @chrisbobbe, and thanks @PIG208 for the previous reviews!
Generally this looks great. Comments below, most of them small.
..messages.length.equals(30) | ||
..haveOldest.isTrue(); | ||
}); | ||
group('smoke', () { |
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:
msglist test: Refactor fetchInitial smoke test, preparing for more narrows
Not NFC just because the test is identified differently in the
output (under a new "smoke" group, and with the name
"CombinedFeedNarrow").
We've sometimes gone back and forth on this, I think; but I'd rather consider this kind of change NFC. So the names of the test cases, and their grouping, don't count as "functional", even though they are observable with flutter test
.
I think a good definition of what does count as "functional", for test code, is: a functional change is one that could affect whether the tests would pass or fail, on some hypothetical future change to the code under test.
test/widgets/message_list_test.dart
Outdated
}) async { | ||
addTearDown(testBinding.reset); | ||
streams ??= subscriptions ??= [eg.subscription(eg.stream(streamId: eg.defaultStreamMessageStreamId))]; | ||
await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot( | ||
final effectiveFeatureLevel = zulipFeatureLevel ?? eg.recentZulipFeatureLevel; |
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:
final effectiveFeatureLevel = zulipFeatureLevel ?? eg.recentZulipFeatureLevel; | |
zulipFeatureLevel ??= eg.recentZulipFeatureLevel; |
and then subsequent lines get to say zulipFeatureLevel: zulipFeatureLevel
instead of needing a different, longer name
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.
Interesting; I'll keep this pattern in mind. I'd always thought it was a code smell to reassign a function param.
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.
Yeah — just like mutating a local variable, it requires a bit of care to avoid making things confusing. But I think doing so (as here) right at the top of the function body, where it's effectively still parsing its arguments, is one pattern where it's generally pretty clean.
In this case it's also what this function is already doing with two other parameters on the previous line, so this might as well follow the same pattern.
lib/api/model/narrow.dart
Outdated
} | ||
if (!hasDmElement && !hasWithElement) return narrow; |
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.
The goal of the corresponding early return in the existing code is so we don't make a copy of the list in cases where we don't need to. So we should preserve that property.
In this version, I think it will end up making a no-op copy if there's a /with/ element on a recent server.
lib/api/model/narrow.dart
Outdated
}).toList(); | ||
} | ||
if (hasWithElement && !supportsOperatorWith) { | ||
result.removeWhere((element) => element is ApiNarrowWith); |
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.
This mutates result
. If the argument narrow
didn't happen to have an ApiNarrowDm
in it, then that mutates the argument.
lib/api/model/narrow.dart
Outdated
ApiNarrow result = narrow; | ||
if (hasDmElement) { | ||
result = narrow.map((element) => switch (element) { | ||
ApiNarrowDm() => element.resolve(legacy: !supportsOperatorDm), | ||
_ => element, | ||
}).toList(); | ||
} | ||
if (hasWithElement && !supportsOperatorWith) { | ||
result.removeWhere((element) => element is ApiNarrowWith); | ||
} |
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.
I think these are probably cleanest as one loop that builds up result
from empty, iterating through narrow
.
(By the time we reach this point we should already know that the loop is going to end up producing a result that's different from narrow
, so we can't avoid producing a fresh list and don't need further conditions like if (hasDmElement)
as an optimization.)
lib/model/message_list.dart
Outdated
case StreamMessage(:final streamId, :final topic): | ||
this.narrow = TopicNarrow(streamId, topic); |
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:
case StreamMessage(:final streamId, :final topic): | |
this.narrow = TopicNarrow(streamId, topic); | |
case StreamMessage(): | |
this.narrow = TopicNarrow.ofMessage(someFetchedMessageOrNull); |
This feels a bit more direct.
test/model/message_list_test.dart
Outdated
check(model).narrow.isA<TopicNarrow>() | ||
..streamId.equals(otherStream.streamId) | ||
..topic.equals(otherTopic) | ||
..with_.isNull(); |
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:
check(model).narrow.isA<TopicNarrow>() | |
..streamId.equals(otherStream.streamId) | |
..topic.equals(otherTopic) | |
..with_.isNull(); | |
check(model).narrow | |
.equals(TopicNarrow(otherStream.streamId, otherTopic)); |
TopicNarrow has an ==
override, so we may as well use it here.
test/model/message_list_test.dart
Outdated
checkNotNotified(); | ||
await model.fetchInitial(); |
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:
checkNotNotified(); | |
await model.fetchInitial(); | |
await model.fetchInitial(); |
and no blank line above
The checkNotNotified
is redundant because we haven't done anything yet up to that point. (We called prepare
, but it ends by ensuring the notification count is clean; other test cases rely on that too.)
The connection.prepare
belongs right before the fetchInitial
call, because it's preparing for a request that call makes.
test/widgets/message_list_test.dart
Outdated
for (final useLegacy in [false, true]) { | ||
testWidgets('without message move; ${useLegacy ? 'legacy' : 'modern'}', (tester) async { |
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.
The legacy case doesn't really change anything in this test, does it? It seems like all it does is mean we don't send the with
element to the server.
The fact that we don't send that is already covered by the API tests. So we can leave it out here.
lib/api/model/narrow.dart
Outdated
if (!narrow.any((element) => element is ApiNarrowDm)) { | ||
return narrow; | ||
// TODO(server-7) remove [ApiNarrowDm] reference in dartdoc | ||
ApiNarrow resolveApiNarrowElements(ApiNarrow narrow, int zulipFeatureLevel) { |
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.
This name doesn't sound right to me for the expanded scope of this function. (In particular "elements" in the name feels redundant; it doesn't seem to convey any more information than resolveApiNarrow
would.)
Here's a version:
/// Adapt the given narrow to be sent to the given Zulip server version.
///
/// Any elements that take a different name on old vs. new servers
/// will be resolved to the specific name to use.
/// Any elements that are unknown to old servers and can
/// reasonably be omitted will be omitted.
ApiNarrow resolveApiNarrowForServer(ApiNarrow narrow, int zulipFeatureLevel) {
0131961
to
6e195fa
Compare
Thanks for the review and patience! Revision pushed. 🎉 |
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.
Thanks for the revision! I think this is close; comments below.
).toJson()); | ||
final fetchFuture = model.fetchInitial(); | ||
check(model).fetched.isFalse(); | ||
group('fetchInitial', () { |
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:
msglist test: Make groups for fetchInitial and fetchOlder tests
NFC, similarly to #1029 (comment)
lib/api/model/narrow.dart
Outdated
case ApiNarrowWith(): | ||
assert(!supportsOperatorWith); |
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.
What if there's both an ApiNarrowWith and an ApiNarrowDm? Then I think this assertion can fail.
lib/api/model/narrow.dart
Outdated
if (!hasDmElement && (!hasWithElement || supportsOperatorWith)) { | ||
return narrow; |
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.
Related to my previous comment just below: this condition ought to be equivalent to "the loop below will produce a result different from narrow
" — that's the basis for the optimization this represents.
I think this condition is correct but the logic below doesn't match it.
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.
Ah right—thanks for the catch.
lib/model/internal_link.dart
Outdated
ApiNarrowWith? withElement; | ||
ApiNarrowDm? dmElement; |
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: put these, and the cases below, in the same order as each other and as the respective ApiNarrowElement subclasses
lib/model/internal_link.dart
Outdated
continue; | ||
|
||
case _NarrowOperator.with_: | ||
if (withElement != null) return null; | ||
withElement = ApiNarrowWith(int.parse(operand, radix: 10)); |
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.
on parse failure should return null, not throw
// and if it is, it'll appear in the result, making it non-empty.) | ||
this.narrow = narrow.sansWith(); | ||
case StreamMessage(): | ||
this.narrow = TopicNarrow.ofMessage(someFetchedMessageOrNull); |
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.
Over on the widget state there's a comment which should get updated:
void _modelChanged() {
if (model!.narrow != widget.narrow) {
// A message move event occurred, where propagate mode is
// [PropagateMode.changeAll] or [PropagateMode.changeLater].
widget.onNarrowChanged(model!.narrow);
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.
Ooh thanks for the catch.
test/model/message_list_test.dart
Outdated
numAfter: 0, | ||
); | ||
}); | ||
final otherStream = eg.stream(); |
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: otherChannel, to match the neighboring someChannel
test/model/message_list_test.dart
Outdated
final fetchFuture = model.fetchOlder(); | ||
checkNotifiedOnce(); | ||
check(model).fetchingOlder.isTrue(); | ||
test('topic permalink, message was moved', () async { |
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.
I think this test case and the one above it can probably be left out — it doesn't seem like they add anything that isn't covered either by the neighboring tests, or the two test cases added below.
test/widgets/message_list_test.dart
Outdated
'narrow': jsonEncode([ | ||
ApiNarrowStream(someStream.streamId), | ||
ApiNarrowTopic(eg.t(someTopic)), | ||
ApiNarrowWith(1), | ||
]), |
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.
This can use narrow.apiEncode()
like the other test case does, right? Or is there a difference I'm missing?
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.
Oh right! That makes sense.
test/widgets/message_list_test.dart
Outdated
// server sends the /with/<id> message in its current, different location | ||
messages: [eg.streamMessage(id: 1, stream: otherStream, topic: otherTopic)], | ||
streams: [someStream, otherStream], | ||
subscriptions: [eg.subscription(someStream), eg.subscription(otherStream)], |
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: I think the subscriptions aren't needed here
(some other tests need them because they use CombinedFeedNarrow)
6e195fa
to
b17769f
Compare
Thanks for the review! Revision pushed, this time with tests for |
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.
Thanks! Looks good; just a few comments.
test/api/model/narrow_test.dart
Outdated
final zulipFeatureLevel = 176; | ||
doTest('dm (legacy)', zulipFeatureLevel: zulipFeatureLevel, | ||
[dm], [dm.resolve(legacy: true)]); | ||
doTest('topic, not permalink', [stream, topic], [stream, topic]); |
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.
Is this meant to pass zulipFeatureLevel too?
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.
Oops, yes.
lib/api/model/narrow.dart
Outdated
@@ -150,6 +173,22 @@ class ApiNarrowPmWith extends ApiNarrowDm { | |||
ApiNarrowPmWith._(super.operand, {super.negated}); | |||
} | |||
|
|||
/// An [ApiNarrowElement] with the 'with' operator. | |||
/// | |||
/// If part of [ApiNarrow] use [resolveApiNarrowElements]. |
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: update name for revision
lib/model/internal_link.dart
Outdated
@@ -78,6 +78,8 @@ Uri narrowLink(PerAccountStore store, Narrow narrow, {int? nearMessageId}) { | |||
fragment.write('$streamId-$slugifiedName'); | |||
case ApiNarrowTopic(): | |||
fragment.write(_encodeHashComponent(element.operand.apiName)); | |||
case ApiNarrowWith(): |
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: match order of cases
test/api/model/narrow_test.dart
Outdated
import 'model_checks.dart'; | ||
|
||
void main() { | ||
group('resolveApiNarrowForServer', () { |
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.
This should get a comment pointing to the existing set of tests that cover this method, which are in test/api/route/messages_test.dart
, particularly the Narrow.toJson
test.
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.
Ah indeed! Thanks for pointing out that existing coverage—which I see I even added to, since an earlier revision, but forgot about that because I wrote that revision a while ago. 🙂
(Oh and there are some unused imports the analyzer is complaining about.) |
b17769f
to
03c261f
Compare
Thanks for the review! Revision pushed. |
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.
Thanks for the revision! Looks good; a few small comments.
test/api/route/messages_test.dart
Outdated
{'operator': 'pm-with', 'operand': [123, 234]}, | ||
])); | ||
|
||
connection.zulipFeatureLevel = 270; |
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: put this section above the FL 170 section — that way there's more of a monotonic range here from newest to oldish to older
test/api/route/messages_test.dart
Outdated
])); | ||
|
||
// Unlikely to occur in the wild but should still be handled correctly | ||
checkNarrow([ApiNarrowDm([123, 234]), ApiNarrowWith(1)], jsonEncode([ |
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.
])); | |
// Unlikely to occur in the wild but should still be handled correctly | |
checkNarrow([ApiNarrowDm([123, 234]), ApiNarrowWith(1)], jsonEncode([ | |
])); | |
checkNarrow([ApiNarrowDm([123, 234]), ApiNarrowWith(1)], jsonEncode([ |
I think the apology isn't needed 🙂
test/api/route/messages_test.dart
Outdated
])); | ||
|
||
connection.zulipFeatureLevel = 176; | ||
|
||
checkNarrow(eg.topicNarrow(12, 'stuff', with_: 1).apiEncode(), jsonEncode([ |
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: and then without those other comments, the change to the ZFL can be comfortably kept in the same stanza with the checks that rely on it
])); | |
connection.zulipFeatureLevel = 176; | |
checkNarrow(eg.topicNarrow(12, 'stuff', with_: 1).apiEncode(), jsonEncode([ | |
])); | |
connection.zulipFeatureLevel = 176; | |
checkNarrow(eg.topicNarrow(12, 'stuff', with_: 1).apiEncode(), jsonEncode([ |
I think that substantially helps to mitigate the risk of confusion from this use of mutable state, where one otherwise might see one of those later checkNarrow
calls in isolation and wonder why the expected behavior is what it is
lib/api/model/narrow.dart
Outdated
default: | ||
} | ||
} | ||
if (!hasDmElement && (!hasWithElement || supportsOperatorWith)) { |
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: use a De Morgan transform of this:
if (!hasDmElement && (!hasWithElement || supportsOperatorWith)) { | |
if (!hasDmElement && !(hasWithElement && !supportsOperatorWith)) { |
That way it more directly corresponds to the condition that's written in the switch cases below.
(You could even then apply De Morgan to the outer operator too — write "not (A or B)" instead of "not-A and not-B" — and that might be clearer still.)
And refactor the implementation to prepare for a new [ApiNarrow] feature. We're about to add ApiNarrowWith, which new in Zulip Server 9. This function seems like a good place to resolve ApiNarrows into the expected form for servers before 9 (without "with") and 9 or later (with "with"). First we have to make its name and dartdoc more generic, as done here.
We'll use this for some new tests, coming up. Also use it now for one test that's only been checking the topic in the app bar. That test now checks the channel name too, as it was before b1767b2, which dropped that specificity when we split the app bar's channel name and topic onto two rows.
…checks And fix the name of the test.
03c261f
to
c8494a8
Compare
Thanks! Revision pushed. |
Thanks! Looks good; merging. |
Fixes: #1028