fix: Fix corrupt database, Add soundcloud and spotify embeds
This commit is contained in:
@@ -100,11 +100,13 @@
|
||||
|
||||
@if (msg.linkMetadata?.length) {
|
||||
@for (meta of msg.linkMetadata; track meta.url) {
|
||||
<app-chat-link-embed
|
||||
[metadata]="meta"
|
||||
[canRemove]="isOwnMessage() || isAdmin()"
|
||||
(removed)="removeEmbed(meta.url)"
|
||||
/>
|
||||
@if (shouldShowLinkEmbed(meta.url)) {
|
||||
<app-chat-link-embed
|
||||
[metadata]="meta"
|
||||
[canRemove]="isOwnMessage() || isAdmin()"
|
||||
(removed)="removeEmbed(meta.url)"
|
||||
/>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
MAX_AUTO_SAVE_SIZE_BYTES
|
||||
} from '../../../../../attachment';
|
||||
import { KlipyService } from '../../../../application/services/klipy.service';
|
||||
import { hasDedicatedChatEmbed } from '../../../../domain/rules/link-embed.rules';
|
||||
import {
|
||||
DELETED_MESSAGE_CONTENT,
|
||||
Message,
|
||||
@@ -278,6 +279,10 @@ export class ChatMessageItemComponent {
|
||||
});
|
||||
}
|
||||
|
||||
shouldShowLinkEmbed(url?: string): boolean {
|
||||
return !hasDedicatedChatEmbed(url);
|
||||
}
|
||||
|
||||
requestReferenceScroll(messageId: string): void {
|
||||
this.referenceRequested.emit(messageId);
|
||||
}
|
||||
|
||||
@@ -45,6 +45,14 @@
|
||||
<div class="block">
|
||||
<app-chat-youtube-embed [url]="node.url" />
|
||||
</div>
|
||||
} @else if (isSpotifyUrl(node.url)) {
|
||||
<div class="block">
|
||||
<app-chat-spotify-embed [url]="node.url" />
|
||||
</div>
|
||||
} @else if (isSoundcloudUrl(node.url)) {
|
||||
<div class="block">
|
||||
<app-chat-soundcloud-embed [url]="node.url" />
|
||||
</div>
|
||||
}
|
||||
</ng-template>
|
||||
</remark>
|
||||
|
||||
@@ -5,8 +5,15 @@ import remarkBreaks from 'remark-breaks';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import remarkParse from 'remark-parse';
|
||||
import { unified } from 'unified';
|
||||
import {
|
||||
isSoundcloudUrl,
|
||||
isSpotifyUrl,
|
||||
isYoutubeUrl
|
||||
} from '../../../../../domain/rules/link-embed.rules';
|
||||
import { ChatImageProxyFallbackDirective } from '../../../../chat-image-proxy-fallback.directive';
|
||||
import { ChatYoutubeEmbedComponent, isYoutubeUrl } from '../chat-youtube-embed/chat-youtube-embed.component';
|
||||
import { ChatSoundcloudEmbedComponent } from '../chat-soundcloud-embed/chat-soundcloud-embed.component';
|
||||
import { ChatSpotifyEmbedComponent } from '../chat-spotify-embed/chat-spotify-embed.component';
|
||||
import { ChatYoutubeEmbedComponent } from '../chat-youtube-embed/chat-youtube-embed.component';
|
||||
|
||||
const PRISM_LANGUAGE_ALIASES: Record<string, string> = {
|
||||
cs: 'csharp',
|
||||
@@ -40,6 +47,8 @@ const REMARK_PROCESSOR = unified()
|
||||
RemarkModule,
|
||||
MermaidComponent,
|
||||
ChatImageProxyFallbackDirective,
|
||||
ChatSpotifyEmbedComponent,
|
||||
ChatSoundcloudEmbedComponent,
|
||||
ChatYoutubeEmbedComponent
|
||||
],
|
||||
templateUrl: './chat-message-markdown.component.html'
|
||||
@@ -63,6 +72,14 @@ export class ChatMessageMarkdownComponent {
|
||||
return isYoutubeUrl(url);
|
||||
}
|
||||
|
||||
isSpotifyUrl(url?: string): boolean {
|
||||
return isSpotifyUrl(url);
|
||||
}
|
||||
|
||||
isSoundcloudUrl(url?: string): boolean {
|
||||
return isSoundcloudUrl(url);
|
||||
}
|
||||
|
||||
isMermaidCodeBlock(lang?: string): boolean {
|
||||
return this.normalizeCodeLanguage(lang) === 'mermaid';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
@if (embedUrl(); as soundcloudEmbedUrl) {
|
||||
<div class="mt-2 w-[480px] max-w-full overflow-hidden rounded-md border border-border/60 bg-secondary/20">
|
||||
<iframe
|
||||
[src]="soundcloudEmbedUrl"
|
||||
[style.height.px]="embedHeight()"
|
||||
class="w-full border-0"
|
||||
loading="lazy"
|
||||
title="SoundCloud player"
|
||||
></iframe>
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
input
|
||||
} from '@angular/core';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
import { extractSoundcloudResource } from '../../../../../domain/rules/link-embed.rules';
|
||||
|
||||
@Component({
|
||||
selector: 'app-chat-soundcloud-embed',
|
||||
standalone: true,
|
||||
templateUrl: './chat-soundcloud-embed.component.html'
|
||||
})
|
||||
export class ChatSoundcloudEmbedComponent {
|
||||
readonly url = input.required<string>();
|
||||
|
||||
readonly resource = computed(() => extractSoundcloudResource(this.url()));
|
||||
|
||||
readonly embedHeight = computed(() => this.resource()?.type === 'playlist' ? 352 : 166);
|
||||
|
||||
readonly embedUrl = computed(() => {
|
||||
const resource = this.resource();
|
||||
|
||||
if (!resource) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const embedUrl = new URL('https://w.soundcloud.com/player/');
|
||||
|
||||
embedUrl.searchParams.set('url', resource.canonicalUrl);
|
||||
embedUrl.searchParams.set('auto_play', 'false');
|
||||
embedUrl.searchParams.set('hide_related', 'false');
|
||||
embedUrl.searchParams.set('show_comments', 'false');
|
||||
embedUrl.searchParams.set('show_user', 'true');
|
||||
embedUrl.searchParams.set('show_reposts', 'false');
|
||||
embedUrl.searchParams.set('show_teaser', 'true');
|
||||
embedUrl.searchParams.set('visual', resource.type === 'playlist' ? 'true' : 'false');
|
||||
|
||||
return this.sanitizer.bypassSecurityTrustResourceUrl(embedUrl.toString());
|
||||
});
|
||||
|
||||
private readonly sanitizer = inject(DomSanitizer);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
@if (embedUrl(); as spotifyEmbedUrl) {
|
||||
<div class="mt-2 w-[480px] max-w-full overflow-hidden rounded-md border border-border/60 bg-secondary/20">
|
||||
<iframe
|
||||
[src]="spotifyEmbedUrl"
|
||||
[style.height.px]="embedHeight()"
|
||||
class="w-full border-0"
|
||||
loading="lazy"
|
||||
title="Spotify player"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import {
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
input
|
||||
} from '@angular/core';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
import { extractSpotifyResource } from '../../../../../domain/rules/link-embed.rules';
|
||||
|
||||
@Component({
|
||||
selector: 'app-chat-spotify-embed',
|
||||
standalone: true,
|
||||
templateUrl: './chat-spotify-embed.component.html'
|
||||
})
|
||||
export class ChatSpotifyEmbedComponent {
|
||||
readonly url = input.required<string>();
|
||||
|
||||
readonly resource = computed(() => extractSpotifyResource(this.url()));
|
||||
|
||||
readonly embedHeight = computed(() => {
|
||||
const resource = this.resource();
|
||||
|
||||
if (!resource) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
switch (resource.type) {
|
||||
case 'track':
|
||||
case 'episode':
|
||||
return 152;
|
||||
default:
|
||||
return 352;
|
||||
}
|
||||
});
|
||||
|
||||
readonly embedUrl = computed(() => {
|
||||
const resource = this.resource();
|
||||
|
||||
if (!resource) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const embedUrl = new URL(`https://open.spotify.com/embed/${resource.type}/${encodeURIComponent(resource.id)}`);
|
||||
|
||||
embedUrl.searchParams.set('utm_source', 'generator');
|
||||
|
||||
return this.sanitizer.bypassSecurityTrustResourceUrl(embedUrl.toString());
|
||||
});
|
||||
|
||||
private readonly sanitizer = inject(DomSanitizer);
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
<iframe
|
||||
[src]="embedUrl()"
|
||||
class="aspect-video w-full"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen
|
||||
loading="lazy"
|
||||
></iframe>
|
||||
|
||||
@@ -5,8 +5,21 @@ import {
|
||||
input
|
||||
} from '@angular/core';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
import { extractYoutubeVideoId } from '../../../../../domain/rules/link-embed.rules';
|
||||
|
||||
const YOUTUBE_URL_PATTERN = /(?:youtube\.com\/(?:watch\?.*v=|embed\/|shorts\/)|youtu\.be\/)([\w-]{11})/;
|
||||
const YOUTUBE_EMBED_FALLBACK_ORIGIN = 'https://toju.app';
|
||||
|
||||
function resolveYoutubeClientOrigin(): string {
|
||||
if (typeof window === 'undefined') {
|
||||
return YOUTUBE_EMBED_FALLBACK_ORIGIN;
|
||||
}
|
||||
|
||||
const origin = window.location.origin;
|
||||
|
||||
return /^https?:\/\//.test(origin)
|
||||
? origin
|
||||
: YOUTUBE_EMBED_FALLBACK_ORIGIN;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-chat-youtube-embed',
|
||||
@@ -16,11 +29,7 @@ const YOUTUBE_URL_PATTERN = /(?:youtube\.com\/(?:watch\?.*v=|embed\/|shorts\/)|y
|
||||
export class ChatYoutubeEmbedComponent {
|
||||
readonly url = input.required<string>();
|
||||
|
||||
readonly videoId = computed(() => {
|
||||
const match = this.url().match(YOUTUBE_URL_PATTERN);
|
||||
|
||||
return match?.[1] ?? null;
|
||||
});
|
||||
readonly videoId = computed(() => extractYoutubeVideoId(this.url()));
|
||||
|
||||
readonly embedUrl = computed(() => {
|
||||
const id = this.videoId();
|
||||
@@ -28,14 +37,16 @@ export class ChatYoutubeEmbedComponent {
|
||||
if (!id)
|
||||
return '';
|
||||
|
||||
const clientOrigin = resolveYoutubeClientOrigin();
|
||||
const embedUrl = new URL(`https://www.youtube-nocookie.com/embed/${encodeURIComponent(id)}`);
|
||||
|
||||
embedUrl.searchParams.set('origin', clientOrigin);
|
||||
embedUrl.searchParams.set('widget_referrer', clientOrigin);
|
||||
|
||||
return this.sanitizer.bypassSecurityTrustResourceUrl(
|
||||
`https://www.youtube-nocookie.com/embed/${encodeURIComponent(id)}`
|
||||
embedUrl.toString()
|
||||
);
|
||||
});
|
||||
|
||||
private readonly sanitizer = inject(DomSanitizer);
|
||||
}
|
||||
|
||||
export function isYoutubeUrl(url?: string): boolean {
|
||||
return !!url && YOUTUBE_URL_PATTERN.test(url);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user