Add cashtag support for stock ticker discussions (#9689)

* Add cashtag support for stock ticker discussions

Display cashtags ($TICKER format) as clickable links with dedicated menu actions and search feed, alongside hashtags. Includes composer highlighting and proper formatting.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

* Fix cashtag search query to use # prefix

Cashtags need to be searched as "#$BTC" rather than just "$BTC" to work
with the search API.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Update @atproto/api to 0.18.14 with cashtag support

The updated package includes native CASHTAG_REGEX detection for stock
ticker symbols like $AAPL and $BTC.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Fix todo, remove redundant validation

---------

Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
Co-authored-by: Eric Bailey <git@esb.lol>
This commit is contained in:
Samuel Newman
2026-01-15 03:03:48 +02:00
committed by GitHub
parent 498a09e797
commit 26aa7c0d9a
5 changed files with 228 additions and 140 deletions
@@ -14,7 +14,11 @@
* the facet-set.
*/
import {TAG_REGEX, TRAILING_PUNCTUATION_REGEX} from '@atproto/api'
import {
CASHTAG_REGEX,
TAG_REGEX,
TRAILING_PUNCTUATION_REGEX,
} from '@atproto/api'
import {Mark} from '@tiptap/core'
import {type Node as ProsemirrorNode} from '@tiptap/pm/model'
import {Plugin, PluginKey} from '@tiptap/pm/state'
@@ -28,6 +32,7 @@ function getDecorations(doc: ProsemirrorNode) {
const regex = TAG_REGEX
const textContent = node.textContent
// Detect hashtags
let match
while ((match = regex.exec(textContent))) {
const [matchedString, __, tag] = match
@@ -52,6 +57,27 @@ function getDecorations(doc: ProsemirrorNode) {
}),
)
}
// Detect cashtags
const cashtagRegex = new RegExp(CASHTAG_REGEX.source, 'gu')
while ((match = cashtagRegex.exec(textContent))) {
const [_fullMatch, leading, ticker] = match
if (!ticker) continue
// Calculate positions: leading char + $ + ticker
const matchedFrom = match.index + leading.length
const matchedTo = matchedFrom + 1 + ticker.length // +1 for $
const start = pos + matchedFrom
const end = pos + matchedTo
decorations.push(
Decoration.inline(start, end, {
class: 'autolink',
}),
)
}
}
})