-
Notifications
You must be signed in to change notification settings - Fork 244
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
Mark as resolved/unresolved prep 1 #1242
base: main
Are you sure you want to change the base?
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.
Thanks! Good idea.
Comments below from an initial review.
lib/api/route/messages.dart
Outdated
String stripResolvePrefixIfPresent(String topic) => | ||
topic.startsWith(kResolvedTopicPrefix) | ||
? topic.substring(kResolvedTopicPrefix.length) | ||
: 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.
How does this end up getting used later in your branch?
Its behavior differs from unresolve_name
in the shared code:
https://github.com/zulip/zulip/blob/1fac99733/web/shared/src/resolved_topic.ts#L22-L29
which I'm guessing (haven't checked) is what gets applied when the user unresolves a topic in web or the legacy mobile app.
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.
Updating to match web's behavior.
/// When built from server data, the action sheet ignores changes in that data; | ||
/// we intentionally don't live-update the buttons on events. | ||
/// If a button's label, action, or position changes suddenly, | ||
/// it can be confusing and make the on-tap behavior unexpected. | ||
/// Better to let the user decide to tap |
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, yeah.
Looking for PerAccountStoreWidget.of
calls in this file, I think there's one place we don't currently follow this: the hasSelfVote
in ReactionButtons
.
That one shouldn't cause this problem in practice, though, because it changes by the user's own action. (Or by the message getting deleted, but then it doesn't matter whether the user tries to add a reaction or remove one.)
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.
In a quick manual test, I don't even see the hasSelfVote
live-updating in the message action sheet as I add or remove one of the popular emojis from the web app.
hasSelfVote
uses PerAccountStoreWidget.of
to get the self-user's ID. My user ID doesn't change when I add/remove a reaction. And although we're still signing up to refresh whenever the PerAccountStore
notifies listeners, it doesn't notify listeners on ReactionEvent
s. (Any MessageListView
s do notify their listeners.)
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.
In a quick manual test, I don't even see the
hasSelfVote
live-updating in the message action sheet as I add or remove one of the popular emojis from the web app.
Right, but then it's still your own action — you know that you just made that change (even though via a different client). So there isn't a risk of confusion.
/// Better to let the user decide to tap | ||
/// based on information that's comfortably in their working memory, | ||
/// even if we sometimes have to explain (where we handle the tap) | ||
/// that that information has changed and they need to decide again. |
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.
A key point here is that these races are always possible anyway — the change may have happened on the server but not reached us yet.
lib/api/model/model.dart
Outdated
@@ -1,5 +1,6 @@ | |||
import 'package:json_annotation/json_annotation.dart'; | |||
|
|||
import '../route/messages.dart'; |
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 don't like having lib/api/model/ import from lib/api/route/. If it's needed for the model, that feels like a sign it's part of the model and not part of how particular routes interact with the model.
I think these can just stay in this file.
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.
(When this file gets to feel unwieldy, I think the thing to do is to split pieces out by subject area: like a lib/api/model/message.dart
, akin to the narrow.dart
and submessage.dart
and so on that we already have.)
/// As in [UpdateMessageEvent.propagateMode]. | ||
@JsonEnum(fieldRename: FieldRename.snake) | ||
enum PropagateMode { |
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.
And this can move from lib/api/model/events.dart to lib/api/model/model.dart, since it's no longer specific to an event.
fe11d69
to
68d7cd1
Compare
Thanks for the review! Revision pushed. |
lib/api/model/model.dart
Outdated
if (topic.apiName.startsWith(_resolvedTopicPrefix) | ||
&& topic.apiName.substring(_resolvedTopicPrefix.length) == prevTopic.apiName) { | ||
return true; | ||
} | ||
|
||
if (prevTopic.apiName.startsWith(_resolvedTopicPrefix) | ||
&& prevTopic.apiName.substring(_resolvedTopicPrefix.length) == topic.apiName) { | ||
if (topic.isSameAs(prevTopic, ignoringResolvePrefix: false)) { // TODO(log) | ||
// Not a topic move after all… Return `true` | ||
// just because `false` would put a marker for `MessageEditState.moved` | ||
// on a message in the message list, which would be conspicuously wrong. | ||
return true; | ||
} | ||
|
||
return false; | ||
return topic.isSameAs(prevTopic, ignoringResolvePrefix: true); |
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.
Because this operation is something we do when decoding one of the high-volume types of data we get from the server (namely messages), it's an area where I'd like to stay somewhat paranoid about performance. (Similar to #1156 (comment) and other comments I've made in some PRs about parsing message content.)
Concretely, that means:
- Let's have this function call
canonicalize
only once on each of the topics, not twice.- (Even then the loop in
_readFromMessage
will end up calling it twice on each of the topics in the middle of the history, once by passing it asprevTopic
here and once astopic
. Ideally we'd fix that too, but that might be more annoying and I'm content to let it be. That issue compounds with this one, though — it means that in this revision it callscanonicalize
four times on each intermediate topic.)
- (Even then the loop in
- It's a bit regrettable this needs a regexp instead of the
startsWith
/substring
check. We do want to get the semantics right, though. Is that definitely what web does?
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.
It's a bit regrettable this needs a regexp instead of the startsWith/substring check. We do want to get the semantics right, though. Is that definitely what web does?
Oh! Actually this isn't what web does. The code before matches what web does. Compare (zulip-flutter before vs. current web):
static bool topicMoveWasResolveOrUnresolve(TopicName topic, TopicName prevTopic) {
if (topic.apiName.startsWith(TopicName.resolvedTopicPrefix)
&& topic.apiName.substring(TopicName.resolvedTopicPrefix.length) == prevTopic.apiName) {
return true;
}
if (prevTopic.apiName.startsWith(TopicName.resolvedTopicPrefix)
&& prevTopic.apiName.substring(TopicName.resolvedTopicPrefix.length) == topic.apiName) {
return true;
}
return false;
}
// We know it has a topic edit. Now we need to determine if
// it was a true move or a resolve/unresolve.
if (
resolved_topic.is_resolved(edit_history_event.topic) &&
edit_history_event.topic.slice(2) === edit_history_event.prev_topic
) {
// Resolved.
resolve_toggled = true;
continue;
}
if (
resolved_topic.is_resolved(edit_history_event.prev_topic) &&
edit_history_event.prev_topic.slice(2) === edit_history_event.topic
) {
// Unresolved.
resolve_toggled = true;
continue;
}
// Otherwise, it is a real topic rename/move.
moved = true;
For this "MOVED"-marker logic, there are pros and cons of following web:
- Pro: performance, as you mentioned
- Pro (I guess): You get a "MOVED" marker if the topic changes from like "✔ ✔ ✔ some topic" to "✔ ✔ some topic"
- Con: You still get a "MOVED" marker on an actual resolve/unresolve, if the resolved-topic prefix is weird: "✔ ✔ some topic" to "some topic"
- Con: Case-folding is not done
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.
It looks like web uses the regex in just one place: to make an unresolve-topic API request. That seems sensible; as you mentioned in the office, the output of "unresolve topic" shouldn't be a resolved topic or look like one.
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.
Cool. Yeah, let's stick with the simpler existing behavior which matches web, then.
The job of this "was resolve or unresolve" function is really just to paper over the fact that our "resolve topic" feature is implemented under the hood by renaming the topic. So it's fine that if someone edits the topic name only by fixing the letter case, then that will be displayed with a "MOVED" marker — the user who made that change indeed did so through a "move this topic" UI, just like for other times we show a "MOVED" marker.
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 I see, yeah, that makes sense.
lib/api/model/model.dart
Outdated
if (topic.isSameAs(prevTopic, ignoringResolvePrefix: false)) { // TODO(log) | ||
// Not a topic move after all… Return `true` | ||
// just because `false` would put a marker for `MessageEditState.moved` | ||
// on a message in the message list, which would be conspicuously wrong. | ||
return true; |
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 guess further on the point of performance paranoia: is this a situation that the server is actually expected to produce?
I think not — https://zulip.com/api/get-messages at prev_topic
says:
Only present if message's topic was edited.
So we don't need to go out of our way trying to detect it.
lib/api/model/model.dart
Outdated
/// | ||
/// If [ignoringResolvePrefix] is true, | ||
/// the [unresolve]d forms of [this] and [other] are compared instead. | ||
bool isSameAs(TopicName other, {required bool ignoringResolvePrefix}) { |
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 feels to me more like two different methods than one method with a boolean flag.
And then I think the "ignoring" case is basically doing much the same job as its one caller topicMoveWasResolveOrUnresolve
, so can be inlined into there.
/// When built from server data, the action sheet ignores changes in that data; | ||
/// we intentionally don't live-update the buttons on events. | ||
/// If a button's label, action, or position changes suddenly, | ||
/// it can be confusing and make the on-tap behavior unexpected. | ||
/// Better to let the user decide to tap |
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.
In a quick manual test, I don't even see the
hasSelfVote
live-updating in the message action sheet as I add or remove one of the popular emojis from the web app.
Right, but then it's still your own action — you know that you just made that change (even though via a different client). So there isn't a risk of confusion.
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! Generally this all looks good. A few comments above, and a couple more below.
lib/api/model/model.dart
Outdated
changeLater, | ||
changeAll; | ||
|
||
String toJson() =>_$PropagateModeEnumMap[this]!; |
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:
String toJson() =>_$PropagateModeEnumMap[this]!; | |
String toJson() => _$PropagateModeEnumMap[this]!; |
lib/api/route/messages.dart
Outdated
Future<UpdateMessageResult> updateMessage( | ||
ApiConnection connection, { | ||
required int messageId, | ||
String? 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.
String? topic, | |
TopicName? topic, |
🙂
68d7cd1
to
ee4f762
Compare
Thanks for the review! Revision pushed, and please see #1242 (comment). |
Thanks! All looks good except that open subthread above. |
Instead of passing its fields separately. (We'd like to start using lastUnreadId from the data too.)
This will make it available in a long-press handler in the app bar for a topic narrow. That long-press handler brings up the topic action sheet, which will need a message ID to pass in the API request for resolve/unresolve topic. (Not sure the API request should *need* to require a message ID, but it does; shrug.)
And move the PropagateMode definition from events.dart to model.dart, since it's no longer specific to an event.
ee4f762
to
3ef535f
Compare
Thanks for the review! Revision pushed, atop #1290 for the sake of CI. |
My local branch for #744 has gotten pretty long and I think it'll benefit from being split into multiple PRs. 🙂 Here's one.