feat: Youtube embed support
This commit is contained in:
@@ -32,4 +32,19 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
<ng-template
|
||||||
|
[remarkTemplate]="'link'"
|
||||||
|
let-node
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
[href]="node.url"
|
||||||
|
[title]="node.title ?? ''"
|
||||||
|
[remarkNode]="node"
|
||||||
|
></a>
|
||||||
|
@if (isYoutubeUrl(node.url)) {
|
||||||
|
<div class="block">
|
||||||
|
<app-chat-youtube-embed [url]="node.url" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</ng-template>
|
||||||
</remark>
|
</remark>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import remarkGfm from 'remark-gfm';
|
|||||||
import remarkParse from 'remark-parse';
|
import remarkParse from 'remark-parse';
|
||||||
import { unified } from 'unified';
|
import { unified } from 'unified';
|
||||||
import { ChatImageProxyFallbackDirective } from '../../../chat-image-proxy-fallback.directive';
|
import { ChatImageProxyFallbackDirective } from '../../../chat-image-proxy-fallback.directive';
|
||||||
|
import { ChatYoutubeEmbedComponent, isYoutubeUrl } from './chat-youtube-embed.component';
|
||||||
|
|
||||||
const PRISM_LANGUAGE_ALIASES: Record<string, string> = {
|
const PRISM_LANGUAGE_ALIASES: Record<string, string> = {
|
||||||
cs: 'csharp',
|
cs: 'csharp',
|
||||||
@@ -38,7 +39,8 @@ const REMARK_PROCESSOR = unified()
|
|||||||
CommonModule,
|
CommonModule,
|
||||||
RemarkModule,
|
RemarkModule,
|
||||||
MermaidComponent,
|
MermaidComponent,
|
||||||
ChatImageProxyFallbackDirective
|
ChatImageProxyFallbackDirective,
|
||||||
|
ChatYoutubeEmbedComponent
|
||||||
],
|
],
|
||||||
templateUrl: './chat-message-markdown.component.html'
|
templateUrl: './chat-message-markdown.component.html'
|
||||||
})
|
})
|
||||||
@@ -57,6 +59,10 @@ export class ChatMessageMarkdownComponent {
|
|||||||
return KLIPY_MEDIA_URL_PATTERN.test(url);
|
return KLIPY_MEDIA_URL_PATTERN.test(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isYoutubeUrl(url?: string): boolean {
|
||||||
|
return isYoutubeUrl(url);
|
||||||
|
}
|
||||||
|
|
||||||
isMermaidCodeBlock(lang?: string): boolean {
|
isMermaidCodeBlock(lang?: string): boolean {
|
||||||
return this.normalizeCodeLanguage(lang) === 'mermaid';
|
return this.normalizeCodeLanguage(lang) === 'mermaid';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { Component, computed, input } from '@angular/core';
|
||||||
|
import { DomSanitizer } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
const YOUTUBE_URL_PATTERN = /(?:youtube\.com\/(?:watch\?.*v=|embed\/|shorts\/)|youtu\.be\/)([\w-]{11})/;
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-chat-youtube-embed',
|
||||||
|
standalone: true,
|
||||||
|
template: `
|
||||||
|
@if (videoId()) {
|
||||||
|
<div class="mt-2 w-[480px] max-w-full overflow-hidden rounded-md border border-border/60">
|
||||||
|
<iframe
|
||||||
|
[src]="embedUrl()"
|
||||||
|
class="aspect-video w-full"
|
||||||
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||||
|
allowfullscreen
|
||||||
|
loading="lazy"
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
`
|
||||||
|
})
|
||||||
|
export class ChatYoutubeEmbedComponent {
|
||||||
|
readonly url = input.required<string>();
|
||||||
|
|
||||||
|
readonly videoId = computed(() => {
|
||||||
|
const match = this.url().match(YOUTUBE_URL_PATTERN);
|
||||||
|
|
||||||
|
return match?.[1] ?? null;
|
||||||
|
});
|
||||||
|
|
||||||
|
readonly embedUrl = computed(() => {
|
||||||
|
const id = this.videoId();
|
||||||
|
|
||||||
|
if (!id)
|
||||||
|
return '';
|
||||||
|
|
||||||
|
return this.sanitizer.bypassSecurityTrustResourceUrl(
|
||||||
|
`https://www.youtube-nocookie.com/embed/${encodeURIComponent(id)}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
constructor(private readonly sanitizer: DomSanitizer) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isYoutubeUrl(url?: string): boolean {
|
||||||
|
return !!url && YOUTUBE_URL_PATTERN.test(url);
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
/>
|
/>
|
||||||
<meta
|
<meta
|
||||||
http-equiv="Content-Security-Policy"
|
http-equiv="Content-Security-Policy"
|
||||||
content="default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob:; connect-src 'self' blob: ws: wss: http: https:; media-src 'self' blob:; img-src 'self' data: blob: http: https:;"
|
content="default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob:; connect-src 'self' blob: ws: wss: http: https:; media-src 'self' blob:; img-src 'self' data: blob: http: https:; frame-src https://www.youtube-nocookie.com;"
|
||||||
/>
|
/>
|
||||||
<link
|
<link
|
||||||
rel="icon"
|
rel="icon"
|
||||||
|
|||||||
Reference in New Issue
Block a user