Files
BRETT/app/components/CommentContent.vue
T
2026-10-03 23:45:04 +02:00

150 lines
5.0 KiB
Vue

<script setup lang="ts">
import { mentionParts } from '~~/shared/utils/mentions'
import { getYoutubeId, youtubeEmbedUrl } from '~~/shared/utils/youtube'
import { getTwitchTarget, serializeTwitchTarget, twitchEmbedUrl } from '~~/shared/utils/twitch'
interface Part {
type: 'text' | 'link' | 'youtube' | 'twitch' | 'image'
text: string
href?: string
videoId?: string
twitchTarget?: string
}
const props = defineProps<{
content: string
compact?: boolean
}>()
// Twitch-Embeds brauchen den Hostname als parent-Parameter
const requestHost = useRequestURL().hostname
const URL_RE = /https?:\/\/[^\s<>"']+/g
const TRAILING_PUNCT = /[.,;:!?)\]]+$/
const IMAGE_EXT = /\.(avif|bmp|gif|jpe?g|png|svg|webp)$/i
function isImageUrl(href: string): boolean {
try {
return IMAGE_EXT.test(new URL(href).pathname)
}
catch {
return false
}
}
function hostnameFor(part: Part): string {
const raw = part.href ?? part.text
try {
return new URL(raw).hostname.replace(/^www\./, '')
}
catch {
return raw
}
}
const { data: mentionUsers } = useMentionUsers()
const parts = computed<Part[]>(() => {
const result: Part[] = []
let last = 0
for (const match of props.content.matchAll(URL_RE)) {
const raw = match[0]
const href = raw.replace(TRAILING_PUNCT, '')
const trailing = raw.slice(href.length)
if (match.index > last) {
result.push({ type: 'text', text: props.content.slice(last, match.index) })
}
const videoId = getYoutubeId(href)
const twitchTarget = videoId ? null : serializeTwitchTarget(getTwitchTarget(href))
if (videoId) {
result.push({ type: 'youtube', text: href, videoId })
}
else if (twitchTarget) {
result.push({ type: 'twitch', text: href, twitchTarget })
}
else if (isImageUrl(href)) {
result.push({ type: 'image', text: href, href })
}
else {
result.push({ type: 'link', text: href, href })
}
if (trailing) {
result.push({ type: 'text', text: trailing })
}
last = match.index + raw.length
}
if (last < props.content.length) {
result.push({ type: 'text', text: props.content.slice(last) })
}
return result
})
</script>
<template>
<div class="whitespace-pre-wrap break-words text-sm">
<template v-for="(part, index) in parts" :key="index">
<template v-if="part.type === 'text'">
<template v-for="(segment, segmentIndex) in mentionParts(part.text, mentionUsers)" :key="segmentIndex">
<span v-if="segment.user && compact" class="rounded-sm bg-accent px-0.5 font-medium text-accent-foreground">{{ segment.text }}</span>
<UserHoverCard v-else-if="segment.user" :user-id="segment.user.id" :user-name="segment.user.name" :user-color="segment.user.color">
<NuxtLink :to="`/user/${segment.user.id}`" class="rounded-sm bg-accent px-0.5 font-medium text-accent-foreground hover:underline" @click.stop>{{ segment.text }}</NuxtLink>
</UserHoverCard>
<template v-else>{{ segment.text }}</template>
</template>
</template>
<!-- Kompakt-Modus (Sidebar): Icon + Hostname statt Embed/Card -->
<span
v-else-if="compact"
class="mx-0.5 inline-flex items-center gap-1 rounded-sm bg-muted px-1.5 py-0.5 align-middle text-xs text-muted-foreground"
>
<Icon v-if="part.type === 'youtube'" name="simple-icons:youtube" class="size-3.5" />
<Icon v-else-if="part.type === 'twitch'" name="simple-icons:twitch" class="size-3.5" />
<Icon name="ph:image-fill" v-else-if="part.type === 'image'" class="size-3.5" />
<Icon name="ph:link-fill" v-else class="size-3.5" />
{{ hostnameFor(part) }}
</span>
<template v-else-if="!compact">
<LinkPreviewCard
v-if="part.type === 'link'"
:url="part.href!"
/>
<div v-else-if="part.type === 'youtube'" class="mt-2 aspect-video overflow-hidden rounded-md">
<iframe
:src="youtubeEmbedUrl(part.videoId!)"
:title="part.text"
loading="lazy"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
class="h-full w-full"
/>
</div>
<div v-else-if="part.type === 'twitch'" class="mt-2 aspect-video overflow-hidden rounded-md">
<iframe
:src="twitchEmbedUrl(part.twitchTarget!, requestHost) ?? ''"
:title="part.text"
loading="lazy"
allowfullscreen
class="h-full w-full"
/>
</div>
<a
v-else-if="part.type === 'image'"
:href="part.href"
target="_blank"
rel="noopener noreferrer"
class="mt-2 block"
>
<NuxtImg
:src="part.href"
:alt="part.text"
loading="lazy"
class="max-h-60 rounded-md"
/>
</a>
<template v-else>{{ part.text }}</template>
</template>
<template v-else>{{ part.text }}</template>
</template>
</div>
</template>