Conversation
3ea07c0 to
c98acb1
Compare
c98acb1 to
c604d9e
Compare
06f335c to
07b4f45
Compare
|
Thank you @alya for the suggestion. Made it one liner.
|
This was missed when we first added the channel link autocomplete feature.
As of this commit, it's not yet possible in the app to initiate a topic link autocomplete interaction. So in the widgets code that would consume the results of such an interaction, we just throw for now, leaving that to be implemented in a later commit.
This will be used in the next commits for generating incomplete syntax in addition to the previous completed syntax. The new incomplete syntax will be inserted in the compose box when a channel is chosen from the channel autocomplete. This way, the topic autocomplete interaction will be readily available. The old completed syntax will be inserted to the compose box when the channel is chosen from the topic autocomplete.
This will be used in the next commits for generating topic link syntax in the compose box when a topic is chosen from the topic autocomplete.
In the next commit(s), we'll need the narrow field in autocomplete intent.
We already cancel out an autocomplete intent whenever the selection is expanding to the left of the intent character (e.g @ or #). To simplify this, we can start the search for the intent character from the selection start, going leftwards. This is basically NFC, but in the new approach if there's an autocomplete intent inside the selection that will be cancelled, it still keeps looking for another autocomplete intent to the left, while previously it would cancel the looking process too. I think simplifying this part of the code is worth the additional looking which is basically harmless.
For this commit we temporarily intercept the query at the AutocompleteField widget, to avoid invoking the widgets that are still unimplemented. That lets us defer those widgets' logic to a separate later commit.
From the web repo; edited in Inkscape to remove the padding around it. https://github.com/zulip/zulip/blob/052655eee/web/icons/corner-down-right.svg
In the next commit(s), this helper will be used in other test groups too.
Adapted from the Figma design and the web app design: https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=7953-30398&t=IeFc4HZDSNwrfGkU-0 https://chat.zulip.org/#narrow/channel/530-mobile-design/topic/topic.20link.20autocomplete/near/2370602 Fixes: zulip#124
07b4f45 to
d9a3b48
Compare
chrisbobbe
left a comment
There was a problem hiding this comment.
Thanks! Sorry again that I've let this sit. I noticed the symptom of #226 when testing this manually, and considered it a blocker—the topic items don't pop up immediately when you tap a channel, which they definitely should. Anyway, I've sent flutter/flutter#183705 upstream for that, and hopefully it'll land soon, giving us new API that we'll use to fix the bug. 🙂
Comments below.
| List<String> normalizedNameWordsForChannel(ZulipStream channel) { | ||
| return _normalizedNameWordsByChannel[channel.streamId] | ||
| ?? normalizedNameForChannel(channel).split(' '); | ||
| ??= normalizedNameForChannel(channel).split(' '); |
There was a problem hiding this comment.
Indeed, thanks for catching this!
| List<String> normalizedNameWordsForChannelTopic(int channelId, String topic) { | ||
| return (_normalizedNameWordsByChannelTopic[channelId] ?? {})[topic] | ||
| ??= normalizedNameForChannelTopic(channelId, topic).split(' '); | ||
| } |
There was a problem hiding this comment.
| List<String> normalizedNameWordsForChannelTopic(int channelId, String topic) { | |
| return (_normalizedNameWordsByChannelTopic[channelId] ?? {})[topic] | |
| ??= normalizedNameForChannelTopic(channelId, topic).split(' '); | |
| } | |
| List<String> normalizedNameWordsForChannelTopic(int channelId, String topic) { | |
| return (_normalizedNameWordsByChannelTopic[channelId] ??= {})[topic] | |
| ??= normalizedNameForChannelTopic(channelId, topic).split(' '); | |
| } |
Right?
| final queryTopicResult = query.testQueryTopic(TopicName(query.raw), | ||
| matchedTopics: unsorted.whereType<TopicLinkAutocompleteTopicResult>() | ||
| .map((r) => r.topic)); | ||
| if (queryTopicResult != null) unsorted.add(queryTopicResult); |
There was a problem hiding this comment.
It should be possible to check whether a topic exists in constant time; let's try to avoid a linear scan through the filtered topics (unsorted) on every keystroke just for that. How about calling store.topics.latestMessageInTopic, where .latestMessageInTopic is a new int?-returning method you add to Topics, which checks _latestMessageIdsByChannelTopic.
| return bucketSort(unsorted, (r) => r.rank, | ||
| numBuckets: TopicLinkAutocompleteQuery._numResultRanks); |
There was a problem hiding this comment.
This bucket-sort step (here and in other autocompletes) is really handling two things:
a) Query relevance i.e. match quality
b) Everything we want to take precedence over query relevance
For example, in @-mention autocomplete, we rank user-group results below user results even when the user-group result would be a better match for the query.
Here, the ranking of TopicLinkAutocompleteChannelResult is clearly (b): its position relative to other list items has nothing to do with how well it matches the query. In fact we're not really "matching" this result to the query at all—we're just implementing the product choice that it should be present, at the top, when the query is empty, and absent otherwise.
How about hard-coding the rank of TopicLinkAutocompleteChannelResult to 0? (Also adding some dartdoc so its meaning is clearer):
+/// A result for just a channel link, after all, not any topic.
+///
+/// Offered at the top of the list when the topic query is empty.
class TopicLinkAutocompleteChannelResult extends TopicLinkAutocompleteResult {
- TopicLinkAutocompleteChannelResult({required this.channelId, required this.rank});
+ TopicLinkAutocompleteChannelResult({required this.channelId});
@override
final int channelId;
+ /// This should always come first in the list of options.
@override
- final int rank;
+ int get rank => 0;
}Then follow the resulting analyzer errors, on paths that should make sense intuitively, which I think means: TopicLinkAutocompleteQuery.testChannel disappears, and its caller is replaced with something like
if (query.raw.isEmpty) {
unsorted.add(TopicLinkAutocompleteChannelResult(channelId: channelId));
}and so on; also update dartdocs appropriately, e.g.:
- Dartdoc on
TopicLinkAutocompleteResult.rankmoves to subclassTopicLinkAutocompleteTopicResult.rank, because it's only about topics _rankChannelResultdisappears_rankTopicResult's dartdoc is updated, e.g.:
/// A measure of a topic result's quality in the context of the query,
- /// from 0 (best) to one less than [_numResultRanks].
+ /// from 1 (best) to one less than [_numResultRanks].
///
- /// See also [_rankChannelResult].
+ /// (Rank 0 is reserved for [TopicLinkAutocompleteChannelResult].)
static int _rankTopicResult({- and
_numResultRanks:
- /// The number of possible values returned by [_rankResult].
+ /// The number of possible values returned by [_rankTopicResult],
+ /// plus one for the [TopicLinkAutocompleteChannelResult].
static const _numResultRanks = 5;| TopicLinkAutocompleteTopicResult? testQueryTopic(TopicName queryTopic, { | ||
| required Iterable<TopicName> matchedTopics, | ||
| }) { | ||
| assert(raw == queryTopic.apiName); | ||
| if (raw.isEmpty) return null; | ||
| final queryTopicExists = matchedTopics.any((t) => t.isSameAs(queryTopic)); | ||
| if (queryTopicExists) return null; | ||
| return TopicLinkAutocompleteTopicResult( | ||
| channelId: channelId, topic: queryTopic, isNew: true, | ||
| rank: TopicLinkAutocompleteQuery._rankTopicResult(isNewTopic: true)); | ||
| } |
There was a problem hiding this comment.
Like the testChannel case, I think this work can be inlined in TopicLinkAutocompleteView.computeResults, for similar reasons. (Also see my previous comment about checking for topic existence.)
I would also probably make a new subclass of TopicLinkAutocompleteResult just for the new-topic item, with rank hard-coded to 1 (similarly to the channel result's hardcoding to 0), making sure to update TopicLinkAutocompleteResult's dartdoc which refers to two subclasses.
| } | ||
| } | ||
| } else if (charAtPos == '[') { | ||
| final channelFallbackLinkMatch = _channelFallbackLinkWithTopicDelimiterRegex.matchAsPrefix(textUntilCursor, pos); |
There was a problem hiding this comment.
autocomplete: Identify when the user intends a topic link autocomplete
Would you separate the "fallback link" edge-case handling into its own commit? There's a lot going on in this commit, and I'd like to focus on the common case first.
| value = value.replaced( | ||
| TextRange(start: pos, end: value.selection.end), | ||
| channelLink(channel, isComplete: false, store: store)); |
There was a problem hiding this comment.
// Replace "#**…** >" with "#**…>" to trigger the topic autocomplete
// interaction for the channel.This seems helpful, but it also seems like its own "coherent idea" that should go in its own commit, for ease of review.
Also: I wouldn't expect side effects (setting value) in the autocompleteIntent method. I think of it as basically a getter, except by convention we write getters as methods when they involve expensive computation, basically as a hint to potential callers that they should think about performance.
Can we change value at a different layer? From Flutter docs, I think a custom TextInputFormatter looks like the right thing for this. It would need to do its own scan backward through the text, but that can be skipped except when there's a ">" to the left of the cursor.
Then this autocompleteIntent pseudo-getter would stay focused on the job of detecting an intent, making it much easier to reason about.
| final store = PerAccountStoreWidget.of(context); | ||
| final channel = store.streams[option.channelId]; | ||
|
|
||
| if (channel == null) return SizedBox.shrink(); |
| icon = Icon(iconDataForStream(channel), size: _iconSize, | ||
| color: colorSwatchFor(context, store.subscriptions[channel.streamId])); |
| ]))), | ||
| ); |
There was a problem hiding this comment.
nit: put parens on same line



Fixes: #124
A follow-up to #1902.
Design discussion: #mobile-design > topic link autocomplete
Screenshots
Screen recording
Topic.link.autocomplete.mov