FEATURE: Allow showing hashtag autocomplete results without term (#19219)

This commit allows us to type # in the UI and present autocomplete
results immediately with the following logic for the topic composer,
and reversed for the chat composer:

* Categories the user can access and has not muted sorted by `topic_count`
* Tags the user can access and has not muted sorted by `topic_count`
* Chat channels the user is a member of sorted by `messages_count`

So in effect, we allow searching for hashtags without a search term.
To do this we add a new `search_without_term` to each data source so
each one can define how it wants to handle this logic.
This commit is contained in:
Martin Brennan
2022-12-08 13:47:59 +10:00
committed by GitHub
parent fde9e6bc25
commit 3fdb8ffb57
12 changed files with 292 additions and 36 deletions

View File

@@ -0,0 +1,38 @@
# frozen_string_literal: true
RSpec.describe CategoryHashtagDataSource do
fab!(:category1) { Fabricate(:category, slug: "random", topic_count: 12) }
fab!(:category2) { Fabricate(:category, slug: "books", topic_count: 566) }
fab!(:category3) { Fabricate(:category, slug: "movies", topic_count: 245) }
fab!(:group) { Fabricate(:group) }
fab!(:category4) { Fabricate(:private_category, slug: "secret", group: group, topic_count: 40) }
fab!(:category5) { Fabricate(:category, slug: "casual", topic_count: 99) }
fab!(:user) { Fabricate(:user) }
let(:guardian) { Guardian.new(user) }
let(:uncategorized_category) { Category.find(SiteSetting.uncategorized_category_id) }
describe "#search_without_term" do
it "returns distinct categories ordered by topic_count" do
expect(described_class.search_without_term(guardian, 5).map(&:slug)).to eq(
["books", "movies", "casual", "random", "#{uncategorized_category.slug}"],
)
end
it "does not return categories the user does not have permission to view" do
expect(described_class.search_without_term(guardian, 5).map(&:slug)).not_to include("secret")
group.add(user)
expect(described_class.search_without_term(Guardian.new(user), 5).map(&:slug)).to include(
"secret",
)
end
it "does not return categories the user has muted" do
CategoryUser.create!(
user: user,
category: category1,
notification_level: CategoryUser.notification_levels[:muted],
)
expect(described_class.search_without_term(guardian, 5).map(&:slug)).not_to include("random")
end
end
end

View File

@@ -3,7 +3,8 @@
RSpec.describe HashtagAutocompleteService do
fab!(:user) { Fabricate(:user) }
fab!(:category1) { Fabricate(:category, name: "Book Club", slug: "book-club") }
fab!(:tag1) { Fabricate(:tag, name: "great-books") }
fab!(:tag1) { Fabricate(:tag, name: "great-books", topic_count: 22) }
fab!(:topic1) { Fabricate(:topic) }
let(:guardian) { Guardian.new(user) }
subject { described_class.new(guardian) }
@@ -71,19 +72,19 @@ RSpec.describe HashtagAutocompleteService do
describe "#search" do
it "returns search results for tags and categories by default" do
expect(subject.search("book", %w[category tag]).map(&:text)).to eq(
["Book Club", "great-books x 0"],
["Book Club", "great-books x 22"],
)
end
it "respects the types_in_priority_order param" do
expect(subject.search("book", %w[tag category]).map(&:text)).to eq(
["great-books x 0", "Book Club"],
["great-books x 22", "Book Club"],
)
end
it "respects the limit param" do
expect(subject.search("book", %w[tag category], limit: 1).map(&:text)).to eq(
["great-books x 0"],
["great-books x 22"],
)
end
@@ -111,10 +112,10 @@ RSpec.describe HashtagAutocompleteService do
it "does case-insensitive search" do
expect(subject.search("book", %w[category tag]).map(&:text)).to eq(
["Book Club", "great-books x 0"],
["Book Club", "great-books x 22"],
)
expect(subject.search("bOOk", %w[category tag]).map(&:text)).to eq(
["Book Club", "great-books x 0"],
["Book Club", "great-books x 22"],
)
end
@@ -125,7 +126,7 @@ RSpec.describe HashtagAutocompleteService do
it "does not include categories the user cannot access" do
category1.update!(read_restricted: true)
expect(subject.search("book", %w[tag category]).map(&:text)).to eq(["great-books x 0"])
expect(subject.search("book", %w[tag category]).map(&:text)).to eq(["great-books x 22"])
end
it "does not include tags the user cannot access" do
@@ -141,7 +142,7 @@ RSpec.describe HashtagAutocompleteService do
HashtagAutocompleteService.register_data_source("bookmark", BookmarkDataSource)
expect(subject.search("book", %w[category tag bookmark]).map(&:text)).to eq(
["Book Club", "great-books x 0", "read review of this fantasy book"],
["Book Club", "great-books x 22", "read review of this fantasy book"],
)
end
@@ -235,6 +236,38 @@ RSpec.describe HashtagAutocompleteService do
expect(subject.search("book", %w[category tag]).map(&:text)).to eq(["Book Club"])
end
end
context "when no term is provided (default results) triggered by a # with no characters in the UI" do
fab!(:category2) do
Fabricate(:category, name: "Book Zone", slug: "book-zone", topic_count: 546)
end
fab!(:category3) do
Fabricate(:category, name: "Book Dome", slug: "book-dome", topic_count: 987)
end
fab!(:category4) { Fabricate(:category, name: "Bookworld", slug: "book", topic_count: 56) }
fab!(:category5) { Fabricate(:category, name: "Media", slug: "media", topic_count: 446) }
fab!(:tag2) { Fabricate(:tag, name: "mid-books", topic_count: 33) }
fab!(:tag3) { Fabricate(:tag, name: "terrible-books", topic_count: 2) }
fab!(:tag4) { Fabricate(:tag, name: "book", topic_count: 1) }
it "returns the 'most polular' categories and tags (based on topic_count) that the user can access" do
category1.update!(read_restricted: true)
Fabricate(:tag_group, permissions: { "staff" => 1 }, tag_names: ["terrible-books"])
expect(subject.search(nil, %w[category tag]).map(&:text)).to eq(
[
"Book Dome",
"Book Zone",
"Media",
"Bookworld",
Category.find(SiteSetting.uncategorized_category_id).name,
"mid-books x 33",
"great-books x 22",
"book x 1",
],
)
end
end
end
describe "#lookup_old" do

View File

@@ -1,11 +1,11 @@
# frozen_string_literal: true
RSpec.describe TagHashtagDataSource do
fab!(:tag1) { Fabricate(:tag, name: "fact") }
fab!(:tag1) { Fabricate(:tag, name: "fact", topic_count: 0) }
fab!(:tag2) { Fabricate(:tag, name: "factor", topic_count: 5) }
fab!(:tag3) { Fabricate(:tag, name: "factory", topic_count: 1) }
fab!(:tag4) { Fabricate(:tag, name: "factorio") }
fab!(:tag5) { Fabricate(:tag, name: "factz") }
fab!(:tag3) { Fabricate(:tag, name: "factory", topic_count: 4) }
fab!(:tag4) { Fabricate(:tag, name: "factorio", topic_count: 3) }
fab!(:tag5) { Fabricate(:tag, name: "factz", topic_count: 1) }
fab!(:user) { Fabricate(:user) }
let(:guardian) { Guardian.new(user) }
@@ -33,7 +33,7 @@ RSpec.describe TagHashtagDataSource do
it "includes the topic count for the text of the tag" do
expect(described_class.search(guardian, "fact", 5).map(&:text)).to eq(
["fact x 0", "factor x 5", "factory x 1", "factorio x 0", "factz x 0"],
["fact x 0", "factor x 5", "factory x 4", "factorio x 3", "factz x 1"],
)
end
@@ -42,4 +42,22 @@ RSpec.describe TagHashtagDataSource do
expect(described_class.search(guardian, "fact", 5)).to be_empty
end
end
describe "#search_without_term" do
it "returns distinct tags sorted by topic_count" do
expect(described_class.search_without_term(guardian, 5).map(&:slug)).to eq(
%w[factor factory factorio factz fact],
)
end
it "does not return tags the user does not have permission to view" do
Fabricate(:tag_group, permissions: { "staff" => 1 }, tag_names: ["factor"])
expect(described_class.search_without_term(guardian, 5).map(&:slug)).not_to include("factor")
end
it "does not return tags the user has muted" do
TagUser.create(user: user, tag: tag2, notification_level: TagUser.notification_levels[:muted])
expect(described_class.search_without_term(guardian, 5).map(&:slug)).not_to include("factor")
end
end
end