You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
179 lines
5.1 KiB
179 lines
5.1 KiB
import { HASHTAG_REGEX } from '@/constants' |
|
import { NostrEvent } from 'nostr-tools' |
|
|
|
/** |
|
* Normalize a hashtag/topic string |
|
* @param text The text to normalize |
|
* @param replaceSpaces Whether to replace spaces with hyphens (true for t-tags, false for content hashtags) |
|
* @returns Normalized string (lowercase, filtered characters, singular form) |
|
*/ |
|
export function normalizeHashtag(text: string, replaceSpaces: boolean = true): string { |
|
// Convert to lowercase and optionally replace spaces with hyphens |
|
let normalized = text.toLowerCase() |
|
if (replaceSpaces) { |
|
normalized = normalized.replace(/\s+/g, '-') |
|
} |
|
|
|
// Only allow letters, numbers, hyphens, and underscores |
|
normalized = normalized.replace(/[^a-z0-9_-]/g, '') |
|
|
|
// Clean up multiple consecutive hyphens/underscores |
|
normalized = normalized.replace(/[-_]+/g, '-') |
|
|
|
// Remove leading/trailing hyphens/underscores |
|
normalized = normalized.replace(/^[-_]+|[-_]+$/g, '') |
|
|
|
// Reject hashtags that are only numbers |
|
if (/^[0-9]+$/.test(normalized)) { |
|
return '' |
|
} |
|
|
|
// Reject empty strings |
|
if (!normalized) { |
|
return '' |
|
} |
|
|
|
// Convert plural to singular (simple English plurals) |
|
// Handle common cases: -ies -> -y, -es -> (sometimes), -s -> remove |
|
if (normalized.endsWith('ies') && normalized.length > 4) { |
|
// cities -> city, berries -> berry |
|
normalized = normalized.slice(0, -3) + 'y' |
|
} else if (normalized.endsWith('ses') && normalized.length > 4) { |
|
// classes -> class, bosses -> boss |
|
normalized = normalized.slice(0, -2) |
|
} else if (normalized.endsWith('xes') && normalized.length > 4) { |
|
// boxes -> box |
|
normalized = normalized.slice(0, -2) |
|
} else if (normalized.endsWith('ches') && normalized.length > 5) { |
|
// churches -> church |
|
normalized = normalized.slice(0, -2) |
|
} else if (normalized.endsWith('s') && normalized.length > 2) { |
|
// Simple plural: cats -> cat, bitcoins -> bitcoin, Christians -> Christian |
|
// But avoid removing 's' from words that naturally end in 's' |
|
// Check if second-to-last character is not 's' to avoid "ss" words |
|
const secondLast = normalized[normalized.length - 2] |
|
if (secondLast !== 's') { |
|
normalized = normalized.slice(0, -1) |
|
} |
|
} |
|
|
|
return normalized |
|
} |
|
|
|
/** |
|
* Normalize a topic string (t-tags) - replaces spaces with hyphens |
|
* Alias for normalizeHashtag with replaceSpaces=true |
|
*/ |
|
export function normalizeTopic(topic: string): string { |
|
return normalizeHashtag(topic, true) |
|
} |
|
|
|
/** |
|
* Extract hashtags from content |
|
*/ |
|
export function extractHashtagsFromContent(content: string): string[] { |
|
const matches = content.matchAll(HASHTAG_REGEX) |
|
const hashtags: string[] = [] |
|
|
|
for (const match of matches) { |
|
// Remove the # prefix and normalize |
|
const tag = match[0].substring(1) |
|
hashtags.push(normalizeTopic(tag)) |
|
} |
|
|
|
return hashtags |
|
} |
|
|
|
/** |
|
* Extract h-tag (group ID) from event tags |
|
*/ |
|
function extractHTagFromEvent(event: NostrEvent): string | null { |
|
const hTag = event.tags.find(tag => tag[0] === 'h' && tag[1]) |
|
return hTag ? hTag[1] : null |
|
} |
|
|
|
/** |
|
* Parse group identifier from h-tag and relay sources |
|
* Supports both "relay'group-id" format and bare group IDs |
|
*/ |
|
function parseGroupIdentifier( |
|
hTag: string, |
|
relaySources: string[] |
|
): { groupId: string; groupRelay: string | null; fullIdentifier: string } { |
|
// Check if h-tag already contains relay'group-id format |
|
if (hTag.includes("'")) { |
|
const [relay, groupId] = hTag.split("'", 2) |
|
return { |
|
groupId, |
|
groupRelay: relay, |
|
fullIdentifier: hTag |
|
} |
|
} |
|
|
|
// For bare group IDs, use the first relay source |
|
const groupRelay = relaySources.length > 0 ? relaySources[0] : null |
|
const fullIdentifier = groupRelay ? `${groupRelay}'${hTag}` : hTag |
|
|
|
return { |
|
groupId: hTag, |
|
groupRelay, |
|
fullIdentifier |
|
} |
|
} |
|
|
|
/** |
|
* Build display name for a group |
|
*/ |
|
function buildGroupDisplayName( |
|
groupId: string, |
|
groupRelay: string | null |
|
): string { |
|
let displayName: string |
|
|
|
if (!groupRelay) { |
|
displayName = groupId |
|
} else { |
|
// Extract hostname from relay URL for cleaner display |
|
try { |
|
const url = new URL(groupRelay) |
|
const hostname = url.hostname |
|
displayName = `${hostname}'${groupId}` |
|
} catch { |
|
// Fallback to full relay URL if parsing fails |
|
displayName = `${groupRelay}'${groupId}` |
|
} |
|
} |
|
|
|
// Truncate to 20 characters |
|
return displayName.length > 20 ? displayName.substring(0, 20) : displayName |
|
} |
|
|
|
/** |
|
* Extract group information from event |
|
*/ |
|
export function extractGroupInfo( |
|
event: NostrEvent, |
|
relaySources: string[] |
|
): { groupId: string | null; groupRelay: string | null; groupDisplayName: string | null; isGroupDiscussion: boolean } { |
|
const hTag = extractHTagFromEvent(event) |
|
|
|
if (!hTag) { |
|
return { |
|
groupId: null, |
|
groupRelay: null, |
|
groupDisplayName: null, |
|
isGroupDiscussion: false |
|
} |
|
} |
|
|
|
const { groupId, groupRelay } = parseGroupIdentifier(hTag, relaySources) |
|
const groupDisplayName = buildGroupDisplayName(groupId, groupRelay) |
|
|
|
return { |
|
groupId, |
|
groupRelay, |
|
groupDisplayName, |
|
isGroupDiscussion: true |
|
} |
|
} |
|
|
|
|