Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion api/payIn/lib/item.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,20 @@ export async function getMentions (tx, { text, userId }) {
}
}
})
return users.map(user => ({ userId: user.id }))
const userMap = new Map(users.map(u => [u.name.toLowerCase(), u]))
const seen = new Set()
const orderedMentions = []
for (const name of names) {
const lowerName = name.toLowerCase()
if (!seen.has(lowerName)) {
const user = userMap.get(lowerName)
if (user) {
orderedMentions.push({ userId: user.id })
seen.add(lowerName)
}
}
}
return orderedMentions
}
return []
}
Expand Down
8 changes: 8 additions & 0 deletions api/resolvers/item.js
Original file line number Diff line number Diff line change
Expand Up @@ -1299,6 +1299,14 @@ export default {
}
})
},
mentions: async (item, args, { models }) => {
const mentions = await models.mention.findMany({
where: { itemId: item.id },
include: { user: true },
orderBy: { id: 'asc' }
})
return mentions.map(m => m.user)
},
comments: async (item, { sort, cursor }, { me, models }) => {
if (typeof item.comments !== 'undefined') {
if (Array.isArray(item.comments)) {
Expand Down
1 change: 1 addition & 0 deletions api/typeDefs/item.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ export default gql`
cost: Int!
payIn: PayIn
meCommentsViewedAt: Date
mentions: [User!]!
}

input ItemForwardInput {
Expand Down
2 changes: 1 addition & 1 deletion components/comment.js
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ export default function Comment ({
{item.searchText
? <SearchText text={item.searchText} />
: (
<Text itemId={item.id} topLevel={topLevel} rel={item.rel ?? UNKNOWN_LINK_REL} outlawed={item.outlawed} imgproxyUrls={item.imgproxyUrls}>
<Text itemId={item.id} topLevel={topLevel} rel={item.rel ?? UNKNOWN_LINK_REL} outlawed={item.outlawed} imgproxyUrls={item.imgproxyUrls} mentions={item.mentions}>
{item.outlawed && !me?.privates?.wildWestMode
? '*stackers have outlawed this. turn on wild west mode in your [settings](/settings) to see outlawed content.*'
: truncate ? truncateString(item.text) : item.text}
Expand Down
2 changes: 1 addition & 1 deletion components/item-full.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ function TopLevelItem ({ item, noReply, ...props }) {
function ItemText ({ item }) {
return item.searchText
? <SearchText text={item.searchText} />
: <Text itemId={item.id} topLevel rel={item.rel ?? UNKNOWN_LINK_REL} outlawed={item.outlawed} imgproxyUrls={item.imgproxyUrls}>{item.text}</Text>
: <Text itemId={item.id} topLevel rel={item.rel ?? UNKNOWN_LINK_REL} outlawed={item.outlawed} imgproxyUrls={item.imgproxyUrls} mentions={item.mentions}>{item.text}</Text>
}

export default function ItemFull ({ item, fetchMoreComments, bio, rank, ...props }) {
Expand Down
29 changes: 15 additions & 14 deletions components/text.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,6 @@ import Embed from './embed'
import remarkMath from 'remark-math'
import remarkToc from '@/lib/remark-toc'

const rehypeSNStyled = () => rehypeSN({
stylers: [{
startTag: '<sup>',
endTag: '</sup>',
className: styles.superscript
}, {
startTag: '<sub>',
endTag: '</sub>',
className: styles.subscript
}]
})

const baseRemarkPlugins = [
gfm,
remarkUnicode,
Expand All @@ -53,7 +41,7 @@ export function SearchText ({ text }) {
}

// this is one of the slowest components to render
export default memo(function Text ({ rel = UNKNOWN_LINK_REL, imgproxyUrls, children, tab, itemId, outlawed, topLevel }) {
export default memo(function Text ({ rel = UNKNOWN_LINK_REL, imgproxyUrls, children, tab, itemId, outlawed, topLevel, mentions }) {
// include remarkToc if topLevel
const remarkPlugins = topLevel ? [...baseRemarkPlugins, remarkToc] : baseRemarkPlugins

Expand Down Expand Up @@ -151,6 +139,19 @@ export default memo(function Text ({ rel = UNKNOWN_LINK_REL, imgproxyUrls, child

const carousel = useCarousel()

const rehypeSNStyled = useMemo(() => () => rehypeSN({
mentions,
stylers: [{
startTag: '<sup>',
endTag: '</sup>',
className: styles.superscript
}, {
startTag: '<sub>',
endTag: '</sub>',
className: styles.subscript
}]
}), [mentions])

const markdownContent = useMemo(() => (
<ReactMarkdown
components={components}
Expand All @@ -160,7 +161,7 @@ export default memo(function Text ({ rel = UNKNOWN_LINK_REL, imgproxyUrls, child
>
{children}
</ReactMarkdown>
), [components, remarkPlugins, mathJaxPlugin, children, itemId])
), [components, remarkPlugins, mathJaxPlugin, children, itemId, rehypeSNStyled])

const showOverflow = useCallback(() => setShow(true), [setShow])

Expand Down
3 changes: 3 additions & 0 deletions fragments/items.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ export const ITEM_FIELDS = gql`
apiKey
cost
meCommentsViewedAt
mentions {
name
}
}`

export const ITEM_FULL_FIELDS = gql`
Expand Down
64 changes: 61 additions & 3 deletions lib/rehype-sn.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,64 @@ const subRegex = new RegExp('~(' + subGroup + '(?:\\/' + subGroup + ')?)', 'gi')
const nostrIdRegex = /\b((npub1|nevent1|nprofile1|note1|naddr1)[02-9ac-hj-np-z]+)\b/g

export default function rehypeSN (options = {}) {
const { stylers = [] } = options
const { stylers = [], mentions } = options

return function transformer (tree) {
const mentionMap = {}

if (mentions?.length) {
const textMentions = []
visit(tree, 'text', (node, index, parent) => {
if (parent && parent.tagName === 'code') return
if (parent && parent.tagName === 'a') return

let text = toString(node)
if (['@', '~'].includes(node.value) && parent.children[index + 1]?.tagName === 'strong' && parent.children[index + 1].children[0]?.type === 'text') {
text = node.value + '__' + toString(parent.children[index + 1]) + '__'
}

let match
while ((match = mentionRegex.exec(text)) !== null) {
textMentions.push({ name: match[1], position: textMentions.length })
}
mentionRegex.lastIndex = 0
})

// Step 1: Try exact name matching first
const matched = new Set()
const unmatchedText = []

textMentions.forEach((tm) => {
const matchingDbMention = mentions.find(m =>
m.name.toLowerCase() === tm.name.toLowerCase() && !matched.has(m.name.toLowerCase())
)

if (matchingDbMention) {
if (tm.name !== matchingDbMention.name) {
mentionMap[tm.name] = matchingDbMention.name
}
matched.add(matchingDbMention.name.toLowerCase())
} else {
unmatchedText.push(tm)
}
})

// Step 2: Position-based mapping for unmatched mentions
if (unmatchedText.length > 0) {
const unmatchedDb = mentions.filter(m => !matched.has(m.name.toLowerCase()))

if (unmatchedText.length === unmatchedDb.length && unmatchedText.length > 0) {
unmatchedText.forEach((tm, idx) => {
if (unmatchedDb[idx]) {
mentionMap[tm.name] = unmatchedDb[idx].name
}
})
} else if (unmatchedText.length === 1 && unmatchedDb.length === 1) {
mentionMap[unmatchedText[0].name] = unmatchedDb[0].name
}
}
}

try {
visit(tree, (node, index, parent) => {
if (parent?.tagName === 'code') {
Expand Down Expand Up @@ -117,7 +172,7 @@ export default function rehypeSN (options = {}) {

const [fullMatch, mentionMatch, subMatch] = match

const replacement = mentionMatch ? replaceMention(fullMatch, mentionMatch) : replaceSub(fullMatch, subMatch)
const replacement = mentionMatch ? replaceMention(fullMatch, mentionMatch, mentionMap) : replaceSub(fullMatch, subMatch)
if (replacement) {
newChildren.push(replacement)
} else {
Expand Down Expand Up @@ -237,7 +292,10 @@ export default function rehypeSN (options = {}) {
)
}

function replaceMention (value, username) {
function replaceMention (value, username, mentionMap) {
if (mentionMap && mentionMap[username]) {
username = mentionMap[username]
}
// split the name by / to allow user paths and still show the user
return {
type: 'element',
Expand Down