Change hashtags to preserve first-used casing (#11416)
parent
4cc29eb5ad
commit
f371b32137
|
@ -148,12 +148,9 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
def process_hashtag(tag)
|
def process_hashtag(tag)
|
||||||
return if tag['name'].blank?
|
return if tag['name'].blank?
|
||||||
|
|
||||||
hashtag = tag['name'].gsub(/\A#/, '').mb_chars.downcase
|
Tag.find_or_create_by_names(tag['name']) do |hashtag|
|
||||||
hashtag = Tag.where(name: hashtag).first_or_create!(name: hashtag)
|
@tags << hashtag unless @tags.include?(hashtag)
|
||||||
|
end
|
||||||
return if @tags.include?(hashtag)
|
|
||||||
|
|
||||||
@tags << hashtag
|
|
||||||
rescue ActiveRecord::RecordInvalid
|
rescue ActiveRecord::RecordInvalid
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
|
@ -20,7 +20,7 @@ class Tag < ApplicationRecord
|
||||||
HASHTAG_NAME_RE = '([[:word:]_][[:word:]_·]*[[:alpha:]_·][[:word:]_·]*[[:word:]_])|([[:word:]_]*[[:alpha:]][[:word:]_]*)'
|
HASHTAG_NAME_RE = '([[:word:]_][[:word:]_·]*[[:alpha:]_·][[:word:]_·]*[[:word:]_])|([[:word:]_]*[[:alpha:]][[:word:]_]*)'
|
||||||
HASHTAG_RE = /(?:^|[^\/\)\w])#(#{HASHTAG_NAME_RE})/i
|
HASHTAG_RE = /(?:^|[^\/\)\w])#(#{HASHTAG_NAME_RE})/i
|
||||||
|
|
||||||
validates :name, presence: true, uniqueness: true, format: { with: /\A(#{HASHTAG_NAME_RE})\z/i }
|
validates :name, presence: true, format: { with: /\A(#{HASHTAG_NAME_RE})\z/i }
|
||||||
|
|
||||||
scope :discoverable, -> { joins(:account_tag_stat).where(AccountTagStat.arel_table[:accounts_count].gt(0)).where(account_tag_stats: { hidden: false }).order(Arel.sql('account_tag_stats.accounts_count desc')) }
|
scope :discoverable, -> { joins(:account_tag_stat).where(AccountTagStat.arel_table[:accounts_count].gt(0)).where(account_tag_stats: { hidden: false }).order(Arel.sql('account_tag_stats.accounts_count desc')) }
|
||||||
scope :hidden, -> { where(account_tag_stats: { hidden: true }) }
|
scope :hidden, -> { where(account_tag_stats: { hidden: true }) }
|
||||||
|
@ -64,22 +64,48 @@ class Tag < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
def search_for(term, limit = 5, offset = 0)
|
def find_or_create_by_names(name_or_names)
|
||||||
pattern = sanitize_sql_like(term.strip) + '%'
|
Array(name_or_names).map(&method(:normalize)).uniq.map do |normalized_name|
|
||||||
|
tag = matching_name(normalized_name).first || create(name: normalized_name)
|
||||||
|
|
||||||
Tag.where('lower(name) like lower(?)', pattern)
|
yield tag if block_given?
|
||||||
|
|
||||||
|
tag
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def search_for(term, limit = 5, offset = 0)
|
||||||
|
pattern = sanitize_sql_like(normalize(term.strip)) + '%'
|
||||||
|
|
||||||
|
Tag.where(arel_table[:name].lower.matches(pattern.downcase))
|
||||||
.order(:name)
|
.order(:name)
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.offset(offset)
|
.offset(offset)
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_normalized(name)
|
def find_normalized(name)
|
||||||
find_by(name: name.mb_chars.downcase.to_s)
|
matching_name(name).first
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_normalized!(name)
|
def find_normalized!(name)
|
||||||
find_normalized(name) || raise(ActiveRecord::RecordNotFound)
|
find_normalized(name) || raise(ActiveRecord::RecordNotFound)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def matching_name(name_or_names)
|
||||||
|
names = Array(name_or_names).map { |name| normalize(name).downcase }
|
||||||
|
|
||||||
|
if names.size == 1
|
||||||
|
where(arel_table[:name].lower.eq(names.first))
|
||||||
|
else
|
||||||
|
where(arel_table[:name].lower.in(names))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def normalize(str)
|
||||||
|
str.gsub(/\A#/, '').mb_chars.to_s
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -14,7 +14,7 @@ class HashtagQueryService < BaseService
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def tags_for(tags)
|
def tags_for(names)
|
||||||
Tag.where(name: tags.map(&:downcase)) if tags.presence
|
Tag.matching_name(names) if names.presence
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,9 +5,7 @@ class ProcessHashtagsService < BaseService
|
||||||
tags = Extractor.extract_hashtags(status.text) if status.local?
|
tags = Extractor.extract_hashtags(status.text) if status.local?
|
||||||
records = []
|
records = []
|
||||||
|
|
||||||
tags.map { |str| str.mb_chars.downcase }.uniq(&:to_s).each do |name|
|
Tag.find_or_create_by_names(tags) do |tag|
|
||||||
tag = Tag.where(name: name).first_or_create(name: name)
|
|
||||||
|
|
||||||
status.tags << tag
|
status.tags << tag
|
||||||
records << tag
|
records << tag
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
class AddCaseInsensitiveIndexToTags < ActiveRecord::Migration[5.2]
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
def up
|
||||||
|
safety_assured { execute 'CREATE UNIQUE INDEX CONCURRENTLY index_tags_on_name_lower ON tags (lower(name))' }
|
||||||
|
remove_index :tags, name: 'index_tags_on_name'
|
||||||
|
remove_index :tags, name: 'hashtag_search_index'
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
add_index :tags, :name, unique: true, algorithm: :concurrently
|
||||||
|
safety_assured { execute 'CREATE INDEX CONCURRENTLY hashtag_search_index ON tags (name text_pattern_ops)' }
|
||||||
|
remove_index :tags, name: 'index_tags_on_name_lower'
|
||||||
|
end
|
||||||
|
end
|
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 2019_07_15_164535) do
|
ActiveRecord::Schema.define(version: 2019_07_26_175042) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
@ -652,8 +652,7 @@ ActiveRecord::Schema.define(version: 2019_07_15_164535) do
|
||||||
t.string "name", default: "", null: false
|
t.string "name", default: "", null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.index "lower((name)::text) text_pattern_ops", name: "hashtag_search_index"
|
t.index "lower((name)::text)", name: "index_tags_on_name_lower", unique: true
|
||||||
t.index ["name"], name: "index_tags_on_name", unique: true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "tombstones", force: :cascade do |t|
|
create_table "tombstones", force: :cascade do |t|
|
||||||
|
|
Reference in New Issue