youtube embeds

This commit is contained in:
Moon.eth
2026-07-29 11:53:15 +09:00
parent 2f0c8821f3
commit cd77720a4f
7 changed files with 274 additions and 1 deletions
+6 -1
View File
@@ -15,11 +15,13 @@
import { displayNameOf, formatCount, fullHandle, profilePath } from '$lib/util/profile'
import { renderDisplayName } from '$lib/util/html'
import { isoDate, longDate, stampDate } from '$lib/util/time'
import { extractYouTubeVideoIds } from '$lib/util/youtube'
import Avatar from '../common/Avatar.svelte'
import RichText from '../common/RichText.svelte'
import Attachments from './Attachments.svelte'
import PollView from './PollView.svelte'
import PreviewCardView from './PreviewCardView.svelte'
import YouTubeEmbeds from './YouTubeEmbeds.svelte'
interface Props {
status: Status
@@ -45,6 +47,7 @@
const handle = $derived(fullHandle(author, session.host))
const permalink = $derived(`#/blog/${entry.id}`)
const isMine = $derived(session.me?.id === entry.account.id)
const youtubeVideoIds = $derived(extractYouTubeVideoIds(entry.content))
let busy = $state(false)
let actionError = $state<string | null>(null)
@@ -188,6 +191,7 @@
{#if entry.media_attachments.length > 0}
<Attachments attachments={entry.media_attachments} sensitive={entry.sensitive} />
{/if}
<YouTubeEmbeds videoIds={youtubeVideoIds} sensitive={entry.sensitive} />
</details>
{:else}
<RichText
@@ -200,13 +204,14 @@
{#if entry.media_attachments.length > 0}
<Attachments attachments={entry.media_attachments} sensitive={entry.sensitive} />
{/if}
<YouTubeEmbeds videoIds={youtubeVideoIds} sensitive={entry.sensitive} />
{/if}
{#if entry.poll}
<PollView poll={entry.poll} />
{/if}
{#if entry.card && entry.media_attachments.length === 0}
{#if entry.card && entry.media_attachments.length === 0 && youtubeVideoIds.length === 0}
<PreviewCardView card={entry.card} />
{/if}
+50
View File
@@ -0,0 +1,50 @@
import { fireEvent, render } from '@testing-library/svelte'
import { describe, expect, it } from 'vitest'
import { APP_SERVICES } from '$lib/app-services'
import { status, testServices } from '$test/fixtures'
import BlogEntry from './BlogEntry.svelte'
function renderEntry(overrides: Parameters<typeof status>[0]) {
return render(BlogEntry, {
props: { status: status(overrides) },
context: new Map([[APP_SERVICES, testServices()]]),
})
}
describe('BlogEntry YouTube embeds', () => {
it('turns a YouTube note link into an attachment-style embed', () => {
const view = renderEntry({
content:
'<p>Watch this: <a href="https://www.youtube.com/watch?v=pyNCigSulNs">https://www.youtube.com/watch?v=pyNCigSulNs</a></p>',
card: {
url: 'https://www.youtube.com/watch?v=pyNCigSulNs',
title: 'Server-generated YouTube preview',
description: 'This should be replaced by the player.',
type: 'video',
},
})
const frame = view.getByTitle('YouTube video')
expect(frame).toHaveAttribute(
'src',
'https://www.youtube-nocookie.com/embed/pyNCigSulNs',
)
expect(frame.closest('.attachment')).toHaveAttribute('data-type', 'youtube')
expect(view.getByRole('link', { name: 'Watch on YouTube' })).toHaveAttribute(
'href',
'https://www.youtube.com/watch?v=pyNCigSulNs',
)
expect(view.queryByText('Server-generated YouTube preview')).not.toBeInTheDocument()
})
it('does not load a sensitive player before the reader reveals it', async () => {
const view = renderEntry({
content: '<p><a href="https://youtu.be/pyNCigSulNs">video</a></p>',
sensitive: true,
})
expect(view.queryByTitle('YouTube video')).not.toBeInTheDocument()
await fireEvent.click(view.getByRole('button', { name: 'Show sensitive video' }))
expect(view.getByTitle('YouTube video')).toBeInTheDocument()
})
})
+65
View File
@@ -0,0 +1,65 @@
<script lang="ts">
/** Privacy-enhanced YouTube players presented in the normal attachment grid. */
import { youtubeEmbedUrl, youtubeVideoId, youtubeWatchUrl } from '$lib/util/youtube'
interface Props {
videoIds: string[]
sensitive?: boolean
}
let { videoIds, sensitive = false }: Props = $props()
let revealed = $state<Record<string, boolean>>({})
const validVideoIds = $derived(
[...new Set(videoIds.filter((id) => youtubeVideoId(youtubeWatchUrl(id)) === id))].slice(0, 4),
)
function isRevealed(id: string): boolean {
return !sensitive || revealed[id] === true
}
function toggle(id: string): void {
revealed = { ...revealed, [id]: !revealed[id] }
}
</script>
{#if validVideoIds.length > 0}
<ul class="attachment-list youtube-attachment-list" aria-label="YouTube videos">
{#each validVideoIds as videoId, index (videoId)}
<li
class="attachment youtube-attachment"
data-type="youtube"
data-sensitive={sensitive ? 'true' : 'false'}
data-revealed={isRevealed(videoId) ? 'true' : 'false'}
>
<figure class="attachment-figure">
<div class="youtube-embed">
{#if isRevealed(videoId)}
<iframe
src={youtubeEmbedUrl(videoId)}
title={validVideoIds.length === 1 ? 'YouTube video' : `YouTube video ${index + 1}`}
loading="lazy"
referrerpolicy="strict-origin-when-cross-origin"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen
></iframe>
{:else}
<div class="youtube-embed-placeholder">Sensitive YouTube video</div>
{/if}
</div>
<figcaption class="attachment-caption">
<a href={youtubeWatchUrl(videoId)} target="_blank" rel="noopener noreferrer">
Watch on YouTube
</a>
</figcaption>
</figure>
{#if sensitive}
<button type="button" class="button button--small attachment-reveal" onclick={() => toggle(videoId)}>
{isRevealed(videoId) ? 'Hide' : 'Show'} sensitive video
</button>
{/if}
</li>
{/each}
</ul>
{/if}