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
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 } |
|
}
|
|
|