Skip to content

Conversation

chrisbobbe
Copy link
Collaborator

@chrisbobbe chrisbobbe commented Jul 3, 2025

Fixes #252.

Stacked on #1658


Still TODO before shipping:

  • Tests.

Screenshots

Light Dark
image image
image image
image image
image image

@chrisbobbe chrisbobbe requested a review from gnprice July 3, 2025 04:25
@chrisbobbe chrisbobbe added the integration review Added by maintainers when PR may be ready for integration label Jul 3, 2025
Copy link
Member

@gnprice gnprice left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exciting! Here's a review of the current version, omitting the points you mentioned as TODOs above.


@override
Widget build(BuildContext context) {
AppBar _buildAppBar(BuildContext context) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

msglist [nfc]: Pull out _buildAppBar helper

How about making it a separate widget entirely? Seems like that'd make the separation a bit crisper; and it doesn't look like this method is interacting with this widget's state beyond looking at narrow.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be feasible, but the reason I didn't is that Scaffold's appBar param is actually typed as PreferredSizeWidget?, not Widget?. Maybe I can subclass ZulipAppBar (which extends AppBar / PreferredSizeWidget) for this app bar?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I see, yeah. That bit of the Scaffold/AppBar API is kind of a pain.

I don't think there's a great way to do it by subclassing, either. Most of what this build-helper method is doing is computing values that get passed to the AppBar constructor, so those computations would have to happen in the subclass's constructor… and they're using the context, so that's no good.

I have an idea which I'll send as an added commit, with a comment.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ac6c750 msglist [nfc]: Move _buildAppBar to a widget-like class

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then I also went into a bit of a digression and sent a PR upstream that would clean up AppBar's API in this respect:

And I have a small draft commit that would use that to turn _MessageListAppBar into a proper widget class:
3ecadf2 wip use AppBar.preferredSizeFor to clean up _MessageListAppBar

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, thanks for doing that!

Comment on lines -942 to +966
&& narrow.containsMessage(outboxMessage)
&& narrow.containsMessage(outboxMessage) == true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The app-code changes show just small behavior changes for a
message-list page in a search narrow, which we'll implement soon:
- Message events are ignored
- Outbox messages never appear

nit: s/changes/choices/ — before this commit, there was no answer to the question of how these would behave on a null containsMessage result, because the types ruled that out

Comment on lines +621 to +623
/// Set [narrow] to [newNarrow], reset, [notifyListeners], and [fetchInitial].
void renarrowAndFetch(Narrow newNarrow) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: mention this from the [narrow] doc, since it's a lot like a setter for that property

Comment on lines 249 to 250
class ApiNarrowKeywordSearch extends ApiNarrowElement {
@override String get operator => 'search';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: let's call this ApiNarrowSearch, to keep it closely aligned with the underlying API

(unlike the Narrow subclass, which is more about how we organize the UI within the app)

);
}

class ApiNarrowKeywordSearch extends ApiNarrowElement {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: let's put this before with and id; those feel more specific than this because they point to an individual message, and naturally go at the end of a list of filters

(I guess that calls for with to go after is, too, next to id.)

Comment on lines 22 to 25
///
/// Will be null for search narrows.
bool? containsMessage(MessageBase message);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like this to give a bit more of a semantics for what it means for this to be null:

Suggested change
///
/// Will be null for search narrows.
bool? containsMessage(MessageBase message);
///
/// Null when the client is unable to predict whether the message
/// satisfies the filters of this narrow, e.g. when this is a search narrow.
bool? containsMessage(MessageBase message);

controller: _controller,
autocorrect: false,

// Current servers seem to require straight quotes for the "exact match"-
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

smartQuotesType: SmartQuotesType.disabled,

autofocus: true,
onSubmitted: (value) => widget.onSubmitted(_valueToNarrow(value)),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: maybe have one method do both these steps (since they always occur together):

Suggested change
onSubmitted: (value) => widget.onSubmitted(_valueToNarrow(value)),
onSubmitted: _handleSubmitted,

border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide.none),
hintStyle: TextStyle(color: designVariables.labelSearchPrompt)));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: should go next to hintText

Comment on lines 3015 to 3019
case MentionsNarrow():
case StarredMessagesNarrow():
check(model.narrow.containsMessage(message)).isNotNull().isTrue();
case KeywordSearchNarrow():
check(model.narrow.containsMessage(message)).isNull();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's a KeywordSearchNarrow, then there's nothing that needs to be checked here; it's a fact about that narrow type, not about the list of messages, that containsMessage will always be null.

Then I think also this is one place where we don't need to be considering for each new narrow type how it should be handled. We can let the default be that the check is done; if at some point we add another narrow type where the check would fail, then we'll naturally notice at test time, and can fix it then. So this could look like:

    if (model.narrow is! KeywordSearchNarrow) {
      check(model.narrow.containsMessage(message)).isNotNull().isTrue();
    }

Hmm or maybe better yet: this could just check that containsMessage is either null or true. If it's null, we're unable to confirm whether the message really belongs here, so checkInvariants has to let it slide. What it's really looking for is cases where it's false, and therefore MessageListView really should have omitted the message.

@gnprice
Copy link
Member

gnprice commented Jul 3, 2025

(All the comments above are small, except #1662 (comment) about narrowLink; and I believe that bit of code never actually gets exercised in the app, so it'd be fine to have it just throw UnimplementedError or something.

… Yeah, confirmed: the app only ever calls that function for generating a message link, so with a SendableNarrow.)

@alya
Copy link
Collaborator

alya commented Jul 3, 2025

Just a simple "No search results." is the default web app message for no results. We also show stopwords that were excluded, but I don't know if we exclude stopwords in mobile searches?

@gnprice
Copy link
Member

gnprice commented Jul 3, 2025

The stopwords behavior is server-side (I believe it happens within the database, even), so it'll be the same for mobile as web.

@chrisbobbe
Copy link
Collaborator Author

Thanks for the reviews! Revision pushed. I still intend to write some tests today, but I can do that while you're reviewing this revision. Also @alya I put screenshots in the issue description.

Just a simple "No search results." is the default web app message for no results.

Works for me for now, but I think ideally we'd show a different message (or no message?) if you haven't requested results yet, i.e. you haven't submitted a search query.

Copy link
Member

@gnprice gnprice left a 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! All looks good, modulo nits below, and it'd be good to have those tests you're working on.

I also haven't yet read the commit at the start which is #1658. I'll do that next.

Comment on lines 285 to -290
Widget build(BuildContext context) {
final store = PerAccountStoreWidget.of(context);
final messageListTheme = MessageListTheme.of(context);
final zulipLocalizations = ZulipLocalizations.of(context);

final Color? appBarBackgroundColor;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: commit message needs edit after squashing

msglist [nfc]: Move _buildAppBar to a widget-like class

Conceptually this should be a widget, but can't quite be, for the
reasons explained in the comment.  Still we can organize the code
in nearly the same way as if it were a widget.

// The messages were and remain in this narrow.
// TODO(#421): … except they may have become muted or not.
// The messages didn't enter or leave this narrow.
// TODO(#1255): … except they may have become muted or not.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

msglist [nfc]: Renumber a TODO

The UserTopic part of this is done, but handling channel
muting/unmuting is still TODO.

Looks like there's a couple other TODO comments for this issue that should get the same update:

lib/model/channel.dart
325:            // TODO(#421) update [MessageListView] if affected
357:    // TODO(#421) update [MessageListView] if affected

… Or actually I guess only the first of those two is about channel mutes; the other is topic mutes. Is the latter resolved, then?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also nit: make the summary line a bit more specific, like "a TODO about muting"

Comment on lines +1212 to +1216
/// Page title for the 'Search' message view.
///
/// In en, this message translates to:
/// **'Search'**
String get searchMessagesPageTitle;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: these look to be happening in the wrong commit

final String? matchContent;
@JsonKey(name: 'match_subject')
final String? matchTopic;
// TODO(#1663) matchContent and matchTopic
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's leave some kind of breadcrumb in this TODO comment to say there's a commit we'll want to revert to restore these. (It has some useful bits in it beyond what one gets mechanically from the API docs: the rename, and the test for that.)

Comment on lines 1160 to 1162
_messagesMovedInternally(messageIds);


case ChannelNarrow(:final streamId):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: double blank line

fillColor: designVariables.bgSearchInput,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide.none),));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
borderSide: BorderSide.none),));
borderSide: BorderSide.none),
));

Comment on lines 986 to 988
final String message;

if (widget.narrow is KeywordSearchNarrow) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: group declaration with initializing code

Suggested change
final String message;
if (widget.narrow is KeywordSearchNarrow) {
final String message;
if (widget.narrow is KeywordSearchNarrow) {

chrisbobbe and others added 12 commits July 3, 2025 16:48
Conceptually this should be a widget, but can't quite be, for the
reasons explained in the comment.  Still we can organize the code
in nearly the same way as if it were a widget.

Co-authored-by: Greg Price <[email protected]>
Done as preparation to implement a new Narrow subclass for keyword
search.

We can't really implement a client-side "contains-message" predicate
for search; it's up to the server to decide whether a message
matches the search query.

A simple path to representing that, done here, is to just change
Narrow.containsMessage's contract to say that it always returns null
if the narrow is a search narrow.

The app-code changes show just small behavior choices for a
message-list page in a search narrow, which we'll implement soon:
- Message events are ignored
- Outbox messages never appear

Various TODOs in tests; we'll handle all of those as part of adding
the keyword-search Narrow subclass.
…ning

Seeing that the tests were expecting a notifyListeners call already,
we found that one is already done, in
MessageStore.handleUpdateMessageEvent, so this is redundant.

But because the new call is done synchronously with the existing
call, it doesn't trigger any extra rebuilds, and it helps the reader
by making it clear locally that listeners do indeed get notified.
We'll use this in keyword search, coming up, for renarrowing when
the user submits a new keyword.
…ions

It looks like this comment was written for CombinedFeedNarrow and
not updated when the other two cases were added. (Unlike with
CombinedFeedNarrow, it's possible that some of `messageIds` weren't
starred or mentioned in the first place, so it was inaccurate to say
they "were" in the narrow.)
We already handle muted topics, but we still need to update the
message list on muting/unmuting a channel, which is zulip#1255.
Fixes zulip#252.

For timeliness, tests are left as a followup: zulip#1667
@chrisbobbe chrisbobbe marked this pull request as ready for review July 3, 2025 23:52
@chrisbobbe
Copy link
Collaborator Author

Thanks! Revision pushed, and filed #1667 for tests as a followup, for timeliness.

@gnprice
Copy link
Member

gnprice commented Jul 4, 2025

Thanks! Looks good; merging.

@gnprice gnprice merged commit a6104aa into zulip:main Jul 4, 2025
1 check passed
@chrisbobbe chrisbobbe deleted the pr-basic-search branch July 4, 2025 01:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

integration review Added by maintainers when PR may be ready for integration

Projects

None yet

Development

Successfully merging this pull request may close these issues.

msglist: Offer search narrows

3 participants