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.
 
 
 
 

233 lines
7.6 KiB

import { ExtendedKind } from '@/constants'
import { extractHashtagsFromContent, normalizeTopic } from '@/lib/discussion-topics'
import { DISCUSSION_TOPICS } from '@/pages/primary/DiscussionsPage/discussionTopics'
import { Event } from 'nostr-tools'
import type { LucideIcon } from 'lucide-react'
import { Hash } from 'lucide-react'
/** Isolates TipTap post cache from the main note composer (see postEditorCache.generateCacheKey). */
export const THREAD_POST_EDITOR_PARENT = { id: '__jumble_thread_post_editor__' } as Event
export type TDiscussionDynamicTopics = {
mainTopics: {
id: string
label: string
count: number
isMainTopic: boolean
isSubtopic: boolean
parentTopic?: string
}[]
subtopics: {
id: string
label: string
count: number
isMainTopic: boolean
isSubtopic: boolean
parentTopic?: string
}[]
allTopics: {
id: string
label: string
count: number
isMainTopic: boolean
isSubtopic: boolean
parentTopic?: string
}[]
}
type TTopicRow = { id: string; label: string; icon: LucideIcon }
type TopicListEntry = { id: string; label: string }
function extractImagesFromContent(content: string): string[] {
const imageRegex = /(https?:\/\/[^\s]+\.(jpg|jpeg|png|gif|webp|svg)(\?[^\s]*)?)/gi
return content.match(imageRegex) || []
}
function generateImetaTagsFromUrls(imageUrls: string[]): string[][] {
return imageUrls.map((url) => ['imeta', 'url', url])
}
function buildDiscussionNsfwTag(): string[] {
return ['content-warning', '']
}
/** Match preset/dynamic list by id or exact label (case-insensitive); otherwise normalize as a new topic slug. */
export function resolveTopicFromInput(raw: string, topics: TopicListEntry[]): string {
const trimmed = raw.trim()
if (!trimmed) return ''
const lower = trimmed.toLowerCase()
const byId = topics.find((x) => x.id === lower)
if (byId) return byId.id
const byLabel = topics.find((x) => x.label.toLowerCase() === lower)
if (byLabel) return byLabel.id
return normalizeTopic(trimmed)
}
export function displayTopicLabel(topicId: string, topics: TopicListEntry[]): string {
const row = topics.find((x) => x.id === topicId)
return row?.label ?? topicId
}
export function buildAllAvailableTopics(dynamicTopics?: TDiscussionDynamicTopics | null): TTopicRow[] {
const combined: TTopicRow[] = [...DISCUSSION_TOPICS]
if (dynamicTopics) {
dynamicTopics.mainTopics.forEach((dynamicTopic) => {
combined.push({
id: dynamicTopic.id,
label: `${dynamicTopic.label} (${dynamicTopic.count}) 🔥`,
icon: Hash
})
})
dynamicTopics.subtopics.forEach((dynamicTopic) => {
const predefinedMainTopic = DISCUSSION_TOPICS.find(
(pt) =>
dynamicTopic.id.toLowerCase().includes(pt.id.toLowerCase()) ||
pt.id.toLowerCase().includes(dynamicTopic.id.toLowerCase())
)
const relatedDynamicMainTopic = dynamicTopics.mainTopics.find(
(dt) =>
dynamicTopic.id.toLowerCase().includes(dt.id.toLowerCase()) ||
dt.id.toLowerCase().includes(dynamicTopic.id.toLowerCase())
)
const parentTopic = predefinedMainTopic?.id || relatedDynamicMainTopic?.id
if (parentTopic) {
const parentIndex = combined.findIndex((topic) => topic.id === parentTopic)
if (parentIndex !== -1) {
combined.splice(parentIndex + 1, 0, {
id: dynamicTopic.id,
label: ` └─ ${dynamicTopic.label} (${dynamicTopic.count}) 📌`,
icon: Hash
})
} else {
combined.push({
id: dynamicTopic.id,
label: `${dynamicTopic.label} (${dynamicTopic.count}) 📌`,
icon: Hash
})
}
} else {
const generalIndex = combined.findIndex((topic) => topic.id === 'general')
if (generalIndex !== -1) {
combined.splice(generalIndex + 1, 0, {
id: dynamicTopic.id,
label: ` └─ ${dynamicTopic.label} (${dynamicTopic.count}) 📌`,
icon: Hash
})
} else {
combined.push({
id: dynamicTopic.id,
label: `${dynamicTopic.label} (${dynamicTopic.count}) 📌`,
icon: Hash
})
}
}
})
}
return combined
}
export function collectDiscussionThreadTags(params: {
processedContent: string
topicForTags: string
title: string
dynamicTopics?: TDiscussionDynamicTopics | null
isReadingGroup: boolean
author: string
subject: string
isNsfw: boolean
}): string[][] {
const { processedContent, topicForTags, title, dynamicTopics, isReadingGroup, author, subject, isNsfw } = params
const images = extractImagesFromContent(processedContent)
const hashtags = extractHashtagsFromContent(processedContent)
const tags: string[][] = [['title', title.trim()], ['-']]
if (topicForTags !== 'all' && topicForTags !== 'general' && topicForTags !== 'groups') {
const selectedDynamicTopic = dynamicTopics?.allTopics.find((dt) => dt.id === topicForTags)
if (selectedDynamicTopic?.isSubtopic) {
const predefinedMainTopic = DISCUSSION_TOPICS.find(
(pt) =>
topicForTags.toLowerCase().includes(pt.id.toLowerCase()) ||
pt.id.toLowerCase().includes(topicForTags.toLowerCase())
)
if (predefinedMainTopic) {
tags.push(['t', normalizeTopic(predefinedMainTopic.id)])
tags.push(['t', normalizeTopic(topicForTags)])
} else {
const relatedDynamicMainTopic = dynamicTopics?.mainTopics.find(
(dt) =>
topicForTags.toLowerCase().includes(dt.id.toLowerCase()) ||
dt.id.toLowerCase().includes(topicForTags.toLowerCase())
)
if (relatedDynamicMainTopic) {
tags.push(['t', normalizeTopic(relatedDynamicMainTopic.id)])
tags.push(['t', normalizeTopic(topicForTags)])
} else {
tags.push(['t', normalizeTopic(topicForTags)])
}
}
} else {
tags.push(['t', normalizeTopic(topicForTags)])
}
}
let uniqueHashtags = hashtags
if (topicForTags !== 'all' && topicForTags !== 'general') {
const selectedDynamicTopic = dynamicTopics?.allTopics.find((dt) => dt.id === topicForTags)
if (selectedDynamicTopic?.isSubtopic) {
const predefinedMainTopic = DISCUSSION_TOPICS.find(
(pt) =>
topicForTags.toLowerCase().includes(pt.id.toLowerCase()) ||
pt.id.toLowerCase().includes(topicForTags.toLowerCase())
)
const relatedDynamicMainTopic = dynamicTopics?.mainTopics.find(
(dt) =>
topicForTags.toLowerCase().includes(dt.id.toLowerCase()) ||
dt.id.toLowerCase().includes(topicForTags.toLowerCase())
)
const parentTopic = predefinedMainTopic?.id || relatedDynamicMainTopic?.id
uniqueHashtags = hashtags.filter(
(hashtag) =>
hashtag !== normalizeTopic(topicForTags) && (parentTopic ? hashtag !== normalizeTopic(parentTopic) : true)
)
} else {
uniqueHashtags = hashtags.filter((hashtag) => hashtag !== normalizeTopic(topicForTags))
}
}
for (const hashtag of uniqueHashtags) {
tags.push(['t', hashtag])
}
if (isReadingGroup) {
if (!uniqueHashtags.includes('readings')) {
tags.push(['t', 'readings'])
}
tags.push(['author', author.trim()])
tags.push(['subject', subject.trim()])
}
if (images && images.length > 0) {
tags.push(...generateImetaTagsFromUrls(images))
}
if (isNsfw) {
tags.push(buildDiscussionNsfwTag())
}
return tags
}
export function discussionThreadDraftKindParams() {
return { kind: ExtendedKind.DISCUSSION, parentEvent: THREAD_POST_EDITOR_PARENT }
}