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|