refactor: url parser
This commit is contained in:
@@ -3,13 +3,10 @@ import {
|
|||||||
EmbeddedEmojiParser,
|
EmbeddedEmojiParser,
|
||||||
EmbeddedEventParser,
|
EmbeddedEventParser,
|
||||||
EmbeddedHashtagParser,
|
EmbeddedHashtagParser,
|
||||||
EmbeddedImageParser,
|
|
||||||
EmbeddedLNInvoiceParser,
|
EmbeddedLNInvoiceParser,
|
||||||
EmbeddedMediaParser,
|
|
||||||
EmbeddedMentionParser,
|
EmbeddedMentionParser,
|
||||||
EmbeddedNormalUrlParser,
|
EmbeddedUrlParser,
|
||||||
EmbeddedWebsocketUrlParser,
|
EmbeddedWebsocketUrlParser,
|
||||||
EmbeddedYoutubeParser,
|
|
||||||
parseContent
|
parseContent
|
||||||
} from '@/lib/content-parser'
|
} from '@/lib/content-parser'
|
||||||
import { getImageInfosFromEvent } from '@/lib/event'
|
import { getImageInfosFromEvent } from '@/lib/event'
|
||||||
@@ -40,10 +37,7 @@ const Content = memo(
|
|||||||
if (!_content) return null
|
if (!_content) return null
|
||||||
|
|
||||||
const nodes = parseContent(_content, [
|
const nodes = parseContent(_content, [
|
||||||
EmbeddedYoutubeParser,
|
EmbeddedUrlParser,
|
||||||
EmbeddedImageParser,
|
|
||||||
EmbeddedMediaParser,
|
|
||||||
EmbeddedNormalUrlParser,
|
|
||||||
EmbeddedLNInvoiceParser,
|
EmbeddedLNInvoiceParser,
|
||||||
EmbeddedWebsocketUrlParser,
|
EmbeddedWebsocketUrlParser,
|
||||||
EmbeddedEventParser,
|
EmbeddedEventParser,
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
EmbeddedEmojiParser,
|
EmbeddedEmojiParser,
|
||||||
EmbeddedEventParser,
|
EmbeddedEventParser,
|
||||||
EmbeddedImageParser,
|
|
||||||
EmbeddedMentionParser,
|
EmbeddedMentionParser,
|
||||||
EmbeddedMediaParser,
|
EmbeddedUrlParser,
|
||||||
parseContent
|
parseContent
|
||||||
} from '@/lib/content-parser'
|
} from '@/lib/content-parser'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
@@ -25,8 +24,7 @@ export default function Content({
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const nodes = useMemo(() => {
|
const nodes = useMemo(() => {
|
||||||
return parseContent(content, [
|
return parseContent(content, [
|
||||||
EmbeddedImageParser,
|
EmbeddedUrlParser,
|
||||||
EmbeddedMediaParser,
|
|
||||||
EmbeddedEventParser,
|
EmbeddedEventParser,
|
||||||
EmbeddedMentionParser,
|
EmbeddedMentionParser,
|
||||||
EmbeddedEmojiParser
|
EmbeddedEmojiParser
|
||||||
@@ -36,9 +34,6 @@ export default function Content({
|
|||||||
return (
|
return (
|
||||||
<span className={cn('pointer-events-none', className)}>
|
<span className={cn('pointer-events-none', className)}>
|
||||||
{nodes.map((node, index) => {
|
{nodes.map((node, index) => {
|
||||||
if (node.type === 'text') {
|
|
||||||
return node.data
|
|
||||||
}
|
|
||||||
if (node.type === 'image' || node.type === 'images') {
|
if (node.type === 'image' || node.type === 'images') {
|
||||||
return index > 0 ? ` [${t('image')}]` : `[${t('image')}]`
|
return index > 0 ? ` [${t('image')}]` : `[${t('image')}]`
|
||||||
}
|
}
|
||||||
@@ -57,6 +52,7 @@ export default function Content({
|
|||||||
if (!emoji) return node.data
|
if (!emoji) return node.data
|
||||||
return <Emoji key={index} emoji={emoji} classNames={{ img: 'mb-1' }} />
|
return <Emoji key={index} emoji={emoji} classNames={{ img: 'mb-1' }} />
|
||||||
}
|
}
|
||||||
|
return node.data
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
import {
|
import {
|
||||||
EmbeddedHashtagParser,
|
EmbeddedHashtagParser,
|
||||||
EmbeddedMentionParser,
|
EmbeddedMentionParser,
|
||||||
EmbeddedNormalUrlParser,
|
EmbeddedUrlParser,
|
||||||
EmbeddedWebsocketUrlParser,
|
EmbeddedWebsocketUrlParser,
|
||||||
parseContent
|
parseContent
|
||||||
} from '@/lib/content-parser'
|
} from '@/lib/content-parser'
|
||||||
import { detectLanguage } from '@/lib/utils'
|
import { detectLanguage } from '@/lib/utils'
|
||||||
|
import { useTranslationService } from '@/providers/TranslationServiceProvider'
|
||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { toast } from 'sonner'
|
||||||
import {
|
import {
|
||||||
EmbeddedHashtag,
|
EmbeddedHashtag,
|
||||||
EmbeddedMention,
|
EmbeddedMention,
|
||||||
EmbeddedNormalUrl,
|
EmbeddedNormalUrl,
|
||||||
EmbeddedWebsocketUrl
|
EmbeddedWebsocketUrl
|
||||||
} from '../Embedded'
|
} from '../Embedded'
|
||||||
import { useTranslationService } from '@/providers/TranslationServiceProvider'
|
|
||||||
import { toast } from 'sonner'
|
|
||||||
|
|
||||||
export default function ProfileAbout({ about, className }: { about?: string; className?: string }) {
|
export default function ProfileAbout({ about, className }: { about?: string; className?: string }) {
|
||||||
const { t, i18n } = useTranslation()
|
const { t, i18n } = useTranslation()
|
||||||
@@ -33,14 +33,11 @@ export default function ProfileAbout({ about, className }: { about?: string; cla
|
|||||||
|
|
||||||
const nodes = parseContent(translatedAbout ?? about, [
|
const nodes = parseContent(translatedAbout ?? about, [
|
||||||
EmbeddedWebsocketUrlParser,
|
EmbeddedWebsocketUrlParser,
|
||||||
EmbeddedNormalUrlParser,
|
EmbeddedUrlParser,
|
||||||
EmbeddedHashtagParser,
|
EmbeddedHashtagParser,
|
||||||
EmbeddedMentionParser
|
EmbeddedMentionParser
|
||||||
])
|
])
|
||||||
return nodes.map((node, index) => {
|
return nodes.map((node, index) => {
|
||||||
if (node.type === 'text') {
|
|
||||||
return node.data
|
|
||||||
}
|
|
||||||
if (node.type === 'url') {
|
if (node.type === 'url') {
|
||||||
return <EmbeddedNormalUrl key={index} url={node.data} />
|
return <EmbeddedNormalUrl key={index} url={node.data} />
|
||||||
}
|
}
|
||||||
@@ -53,6 +50,7 @@ export default function ProfileAbout({ about, className }: { about?: string; cla
|
|||||||
if (node.type === 'mention') {
|
if (node.type === 'mention') {
|
||||||
return <EmbeddedMention key={index} userId={node.data.split(':')[1]} />
|
return <EmbeddedMention key={index} userId={node.data.split(':')[1]} />
|
||||||
}
|
}
|
||||||
|
return node.data
|
||||||
})
|
})
|
||||||
}, [about, translatedAbout])
|
}, [about, translatedAbout])
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,12 @@ import {
|
|||||||
EMBEDDED_MENTION_REGEX,
|
EMBEDDED_MENTION_REGEX,
|
||||||
EMOJI_SHORT_CODE_REGEX,
|
EMOJI_SHORT_CODE_REGEX,
|
||||||
HASHTAG_REGEX,
|
HASHTAG_REGEX,
|
||||||
IMAGE_REGEX,
|
|
||||||
LN_INVOICE_REGEX,
|
LN_INVOICE_REGEX,
|
||||||
URL_REGEX,
|
URL_REGEX,
|
||||||
MEDIA_REGEX,
|
|
||||||
WS_URL_REGEX,
|
WS_URL_REGEX,
|
||||||
YOUTUBE_URL_REGEX
|
YOUTUBE_URL_REGEX
|
||||||
} from '@/constants'
|
} from '@/constants'
|
||||||
|
import { isImage, isMedia } from './url'
|
||||||
|
|
||||||
export type TEmbeddedNodeType =
|
export type TEmbeddedNodeType =
|
||||||
| 'text'
|
| 'text'
|
||||||
@@ -36,7 +35,9 @@ export type TEmbeddedNode =
|
|||||||
data: string[]
|
data: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
type TContentParser = { type: Exclude<TEmbeddedNodeType, 'images'>; regex: RegExp }
|
type TContentParser =
|
||||||
|
| { type: Exclude<TEmbeddedNodeType, 'images'>; regex: RegExp }
|
||||||
|
| ((content: string) => TEmbeddedNode[])
|
||||||
|
|
||||||
export const EmbeddedHashtagParser: TContentParser = {
|
export const EmbeddedHashtagParser: TContentParser = {
|
||||||
type: 'hashtag',
|
type: 'hashtag',
|
||||||
@@ -58,31 +59,11 @@ export const EmbeddedEventParser: TContentParser = {
|
|||||||
regex: EMBEDDED_EVENT_REGEX
|
regex: EMBEDDED_EVENT_REGEX
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EmbeddedImageParser: TContentParser = {
|
|
||||||
type: 'image',
|
|
||||||
regex: IMAGE_REGEX
|
|
||||||
}
|
|
||||||
|
|
||||||
export const EmbeddedMediaParser: TContentParser = {
|
|
||||||
type: 'media',
|
|
||||||
regex: MEDIA_REGEX
|
|
||||||
}
|
|
||||||
|
|
||||||
export const EmbeddedWebsocketUrlParser: TContentParser = {
|
export const EmbeddedWebsocketUrlParser: TContentParser = {
|
||||||
type: 'websocket-url',
|
type: 'websocket-url',
|
||||||
regex: WS_URL_REGEX
|
regex: WS_URL_REGEX
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EmbeddedNormalUrlParser: TContentParser = {
|
|
||||||
type: 'url',
|
|
||||||
regex: URL_REGEX
|
|
||||||
}
|
|
||||||
|
|
||||||
export const EmbeddedYoutubeParser: TContentParser = {
|
|
||||||
type: 'youtube',
|
|
||||||
regex: YOUTUBE_URL_REGEX
|
|
||||||
}
|
|
||||||
|
|
||||||
export const EmbeddedEmojiParser: TContentParser = {
|
export const EmbeddedEmojiParser: TContentParser = {
|
||||||
type: 'emoji',
|
type: 'emoji',
|
||||||
regex: EMOJI_SHORT_CODE_REGEX
|
regex: EMOJI_SHORT_CODE_REGEX
|
||||||
@@ -93,6 +74,48 @@ export const EmbeddedLNInvoiceParser: TContentParser = {
|
|||||||
regex: LN_INVOICE_REGEX
|
regex: LN_INVOICE_REGEX
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const EmbeddedUrlParser: TContentParser = (content: string) => {
|
||||||
|
const matches = content.matchAll(URL_REGEX)
|
||||||
|
const result: TEmbeddedNode[] = []
|
||||||
|
let lastIndex = 0
|
||||||
|
for (const match of matches) {
|
||||||
|
const matchStart = match.index!
|
||||||
|
// Add text before the match
|
||||||
|
if (matchStart > lastIndex) {
|
||||||
|
result.push({
|
||||||
|
type: 'text',
|
||||||
|
data: content.slice(lastIndex, matchStart)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = match[0]
|
||||||
|
let type: TEmbeddedNodeType = 'url'
|
||||||
|
if (isImage(url)) {
|
||||||
|
type = 'image'
|
||||||
|
} else if (isMedia(url)) {
|
||||||
|
type = 'media'
|
||||||
|
} else if (YOUTUBE_URL_REGEX.test(url)) {
|
||||||
|
type = 'youtube'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the match as specific type
|
||||||
|
result.push({
|
||||||
|
type,
|
||||||
|
data: url
|
||||||
|
})
|
||||||
|
|
||||||
|
lastIndex = matchStart + url.length
|
||||||
|
}
|
||||||
|
// Add text after the last match
|
||||||
|
if (lastIndex < content.length) {
|
||||||
|
result.push({
|
||||||
|
type: 'text',
|
||||||
|
data: content.slice(lastIndex)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
export function parseContent(content: string, parsers: TContentParser[]) {
|
export function parseContent(content: string, parsers: TContentParser[]) {
|
||||||
let nodes: TEmbeddedNode[] = [{ type: 'text', data: content.trim() }]
|
let nodes: TEmbeddedNode[] = [{ type: 'text', data: content.trim() }]
|
||||||
|
|
||||||
@@ -100,6 +123,11 @@ export function parseContent(content: string, parsers: TContentParser[]) {
|
|||||||
nodes = nodes
|
nodes = nodes
|
||||||
.flatMap((node) => {
|
.flatMap((node) => {
|
||||||
if (node.type !== 'text') return [node]
|
if (node.type !== 'text') return [node]
|
||||||
|
|
||||||
|
if (typeof parser === 'function') {
|
||||||
|
return parser(node.data)
|
||||||
|
}
|
||||||
|
|
||||||
const matches = node.data.matchAll(parser.regex)
|
const matches = node.data.matchAll(parser.regex)
|
||||||
const result: TEmbeddedNode[] = []
|
const result: TEmbeddedNode[] = []
|
||||||
let lastIndex = 0
|
let lastIndex = 0
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export function getProfileFromEvent(event: Event) {
|
|||||||
created_at: event.created_at
|
created_at: event.created_at
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(event.content, err)
|
||||||
return {
|
return {
|
||||||
pubkey: event.pubkey,
|
pubkey: event.pubkey,
|
||||||
npub: pubkeyToNpub(event.pubkey) ?? '',
|
npub: pubkeyToNpub(event.pubkey) ?? '',
|
||||||
|
|||||||
@@ -117,10 +117,22 @@ export function isImage(url: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isVideo(url: string) {
|
export function isMedia(url: string) {
|
||||||
try {
|
try {
|
||||||
const videoExtensions = ['.mp4', '.webm', '.ogg', '.mov']
|
const mediaExtensions = [
|
||||||
return videoExtensions.some((ext) => new URL(url).pathname.toLowerCase().endsWith(ext))
|
'.mp4',
|
||||||
|
'.webm',
|
||||||
|
'.ogg',
|
||||||
|
'.mov',
|
||||||
|
'.mp3',
|
||||||
|
'.wav',
|
||||||
|
'.flac',
|
||||||
|
'.aac',
|
||||||
|
'.m4a',
|
||||||
|
'.opus',
|
||||||
|
'.wma'
|
||||||
|
]
|
||||||
|
return mediaExtensions.some((ext) => new URL(url).pathname.toLowerCase().endsWith(ext))
|
||||||
} catch {
|
} catch {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user