From 35352923a5860f5336b08b321dfc842156050f85 Mon Sep 17 00:00:00 2001 From: Myx Date: Sat, 4 Apr 2026 03:30:21 +0200 Subject: [PATCH] feat: Youtube embed support --- .../chat-message-markdown.component.html | 15 ++++++ .../chat-message-markdown.component.ts | 8 +++- .../chat-youtube-embed.component.ts | 48 +++++++++++++++++++ toju-app/src/index.html | 2 +- 4 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 toju-app/src/app/domains/chat/feature/chat-messages/components/message-item/chat-youtube-embed.component.ts diff --git a/toju-app/src/app/domains/chat/feature/chat-messages/components/message-item/chat-message-markdown.component.html b/toju-app/src/app/domains/chat/feature/chat-messages/components/message-item/chat-message-markdown.component.html index c4342f0..b97474b 100644 --- a/toju-app/src/app/domains/chat/feature/chat-messages/components/message-item/chat-message-markdown.component.html +++ b/toju-app/src/app/domains/chat/feature/chat-messages/components/message-item/chat-message-markdown.component.html @@ -32,4 +32,19 @@ } + + + @if (isYoutubeUrl(node.url)) { +
+ +
+ } +
diff --git a/toju-app/src/app/domains/chat/feature/chat-messages/components/message-item/chat-message-markdown.component.ts b/toju-app/src/app/domains/chat/feature/chat-messages/components/message-item/chat-message-markdown.component.ts index 53cd9f7..e929c11 100644 --- a/toju-app/src/app/domains/chat/feature/chat-messages/components/message-item/chat-message-markdown.component.ts +++ b/toju-app/src/app/domains/chat/feature/chat-messages/components/message-item/chat-message-markdown.component.ts @@ -6,6 +6,7 @@ import remarkGfm from 'remark-gfm'; import remarkParse from 'remark-parse'; import { unified } from 'unified'; import { ChatImageProxyFallbackDirective } from '../../../chat-image-proxy-fallback.directive'; +import { ChatYoutubeEmbedComponent, isYoutubeUrl } from './chat-youtube-embed.component'; const PRISM_LANGUAGE_ALIASES: Record = { cs: 'csharp', @@ -38,7 +39,8 @@ const REMARK_PROCESSOR = unified() CommonModule, RemarkModule, MermaidComponent, - ChatImageProxyFallbackDirective + ChatImageProxyFallbackDirective, + ChatYoutubeEmbedComponent ], templateUrl: './chat-message-markdown.component.html' }) @@ -57,6 +59,10 @@ export class ChatMessageMarkdownComponent { return KLIPY_MEDIA_URL_PATTERN.test(url); } + isYoutubeUrl(url?: string): boolean { + return isYoutubeUrl(url); + } + isMermaidCodeBlock(lang?: string): boolean { return this.normalizeCodeLanguage(lang) === 'mermaid'; } diff --git a/toju-app/src/app/domains/chat/feature/chat-messages/components/message-item/chat-youtube-embed.component.ts b/toju-app/src/app/domains/chat/feature/chat-messages/components/message-item/chat-youtube-embed.component.ts new file mode 100644 index 0000000..1380352 --- /dev/null +++ b/toju-app/src/app/domains/chat/feature/chat-messages/components/message-item/chat-youtube-embed.component.ts @@ -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()) { +
+ +
+ } + ` +}) +export class ChatYoutubeEmbedComponent { + readonly url = input.required(); + + 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); +} diff --git a/toju-app/src/index.html b/toju-app/src/index.html index b252b3f..5c11f47 100644 --- a/toju-app/src/index.html +++ b/toju-app/src/index.html @@ -10,7 +10,7 @@ />