From 6db1cf421becc5d001d8ead6d8734bbdfa505d0f Mon Sep 17 00:00:00 2001 From: mgabdev <> Date: Fri, 30 Oct 2020 14:01:55 -0500 Subject: [PATCH] Added cashtag support for statuses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Added: - cashtag support for statuses Ref: https://github.com/tootsuite/mastodon/commit/e23931b2550e8ab48d4d6212e50d825293f5b014 --- .../gabsocial/components/composer.js | 9 +++ .../gabsocial/components/status_content.js | 4 +- app/lib/extractor.rb | 56 ++++++++++++++----- app/lib/formatter.rb | 11 ++++ app/models/tag.rb | 3 +- app/services/process_hashtags_service.rb | 2 +- 6 files changed, 67 insertions(+), 18 deletions(-) diff --git a/app/javascript/gabsocial/components/composer.js b/app/javascript/gabsocial/components/composer.js index a7f7e769..1b6a0ae7 100644 --- a/app/javascript/gabsocial/components/composer.js +++ b/app/javascript/gabsocial/components/composer.js @@ -54,6 +54,10 @@ function hashtagStrategy(contentBlock, callback, contentState) { findWithRegex(HASHTAG_REGEX, contentBlock, callback) } +function cashtagStrategy(contentBlock, callback, contentState) { + findWithRegex(CASHTAG_REGEX, contentBlock, callback) +} + function urlStrategy(contentBlock, callback, contentState) { findWithRegex(urlRegex, contentBlock, callback) } @@ -87,6 +91,10 @@ const compositeDecorator = new CompositeDecorator([ strategy: hashtagStrategy, component: HighlightedSpan, }, + { + strategy: cashtagStrategy, + component: HighlightedSpan, + }, { strategy: urlStrategy, component: HighlightedSpan, @@ -109,6 +117,7 @@ const styleMap = { const GROUP_HANDLE_REGEX = /\g\/[\w]+/g const HANDLE_REGEX = /\@[\w]+/g const HASHTAG_REGEX = /\#[\w\u0590-\u05ff]+/g +const CASHTAG_REGEX = /\$[\w\u0590-\u05ff]+/g class Composer extends React.PureComponent { diff --git a/app/javascript/gabsocial/components/status_content.js b/app/javascript/gabsocial/components/status_content.js index 83e3ea8c..84929f36 100644 --- a/app/javascript/gabsocial/components/status_content.js +++ b/app/javascript/gabsocial/components/status_content.js @@ -50,7 +50,7 @@ class StatusContent extends ImmutablePureComponent { link.addEventListener('click', this.onMentionClick.bind(this, mention), false) link.setAttribute('title', mention.get('acct')) link.removeAttribute('target') - } else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) { + } else if (['#', '$'].includes(link.textContent[0]) || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) { link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false) link.removeAttribute('target') } else { @@ -85,7 +85,7 @@ class StatusContent extends ImmutablePureComponent { } onHashtagClick = (hashtag, e) => { - hashtag = hashtag.replace(/^#/, '').toLowerCase() + hashtag = hashtag.replace(/^(#|\$)/, '').toLowerCase() if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) { e.preventDefault() diff --git a/app/lib/extractor.rb b/app/lib/extractor.rb index 479689d6..1b7609a0 100644 --- a/app/lib/extractor.rb +++ b/app/lib/extractor.rb @@ -13,10 +13,12 @@ module Extractor text.to_s.scan(Account::MENTION_RE) do |screen_name, _| match_data = $LAST_MATCH_INFO - after = $' + after = $' + unless after =~ Twitter::Regex[:end_mention_match] start_position = match_data.char_begin(1) - 1 - end_position = match_data.char_end(1) + end_position = match_data.char_end(1) + possible_entries << { screen_name: screen_name, indices: [start_position, end_position], @@ -24,26 +26,25 @@ module Extractor end end - if block_given? - possible_entries.each do |mention| - yield mention[:screen_name], mention[:indices].first, mention[:indices].last - end - end + possible_entries.each { |mention| yield mention[:screen_name], mention[:indices].first, mention[:indices].last } if block_given? possible_entries end + # :yields: hashtag, start, end def extract_hashtags_with_indices(text, **) return [] unless text =~ /#/ tags = [] + text.scan(Tag::HASHTAG_RE) do |hash_text, _| - match_data = $LAST_MATCH_INFO + match_data = $LAST_MATCH_INFO start_position = match_data.char_begin(1) - 1 - end_position = match_data.char_end(1) - after = $' + end_position = match_data.char_end(1) + after = $' + if after =~ %r{\A://} hash_text.match(/(.+)(https?\Z)/) do |matched| - hash_text = matched[1] + hash_text = matched[1] end_position -= matched[2].char_length end end @@ -58,7 +59,34 @@ module Extractor tags end - def extract_cashtags_with_indices(_text) - [] # always returns empty array + # :yields: cashtag, start, end + def extract_cashtags_with_indices(text) + return [] unless text =~ /\$/ + + tags = [] + + text.scan(Tag::CASHTAG_RE) do |cash_text, _| + match_data = $LAST_MATCH_INFO + start_position = match_data.char_begin(1) - 1 + end_position = match_data.char_end(1) + after = $' + + next if cash_text.size > 5 + + if after =~ %r{\A://} + cash_text.match(/(.+)(https?\Z)/) do |matched| + cash_text = matched[1] + end_position -= matched[2].char_length + end + end + + tags << { + cashtag: cash_text, + indices: [start_position, end_position], + } + end + + tags.each { |tag| yield tag[:cashtag], tag[:indices].first, tag[:indices].last } if block_given? + tags end -end +end \ No newline at end of file diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index 855e51bf..8dcd82d0 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -258,6 +258,8 @@ class Formatter link_to_url(entity, options) elsif entity[:hashtag] link_to_hashtag(entity) + elsif entity[:cashtag] + link_to_cashtag(entity) elsif entity[:screen_name] link_to_mention(entity, accounts) end @@ -396,6 +398,7 @@ class Formatter end entities = Extractor.extract_hashtags_with_indices(escaped, :check_url_overlap => false) + + Extractor.extract_cashtags_with_indices(escaped, :check_url_overlap => false) + Extractor.extract_mentions_or_lists_with_indices(escaped) Extractor.remove_overlapping_entities(entities).map do |extract| pos = extract[:indices].first @@ -440,6 +443,10 @@ class Formatter hashtag_html(entity[:hashtag]) end + def link_to_cashtag(entity) + cashtag_html(entity[:cashtag]) + end + def link_html(url) url = Addressable::URI.parse(url).to_s prefix = url.match(/\Ahttps?:\/\/(www\.)?/).to_s @@ -454,6 +461,10 @@ class Formatter "##{encode(tag)}" end + def cashtag_html(tag) + "$#{encode(tag)}" + end + def mention_html(account) return "@#{encode(account.acct)}" unless account.local? "@#{encode(account.acct)}" diff --git a/app/models/tag.rb b/app/models/tag.rb index 7db76d15..d94df924 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -18,7 +18,8 @@ class Tag < ApplicationRecord has_one :account_tag_stat, dependent: :destroy HASHTAG_NAME_RE = '[[:word:]_]*[[:alpha:]_·][[:word:]_]*' - HASHTAG_RE = /(?:^|[^\/\)\w])#(#{HASHTAG_NAME_RE})/i + HASHTAG_RE = /(?:^|[^\/\)\w])#(#{HASHTAG_NAME_RE})/i.freeze + CASHTAG_RE = /(?:^|[^\/\)\w])\$([a-zA-Z]{2,5})/.freeze validates :name, presence: true, uniqueness: true, format: { with: /\A#{HASHTAG_NAME_RE}\z/i } diff --git a/app/services/process_hashtags_service.rb b/app/services/process_hashtags_service.rb index c7db4288..23d1cf15 100644 --- a/app/services/process_hashtags_service.rb +++ b/app/services/process_hashtags_service.rb @@ -2,7 +2,7 @@ class ProcessHashtagsService < BaseService def call(status, tags = []) - tags = Extractor.extract_hashtags(status.text) if status.local? + tags = Extractor.extract_hashtags(status.text) + Extractor.extract_cashtags(status.text) if status.local? records = [] tags.map { |str| str.mb_chars.downcase }.uniq(&:to_s).each do |name|