Summary Update.

This commit is contained in:
2026-06-06 23:38:50 -04:00
parent dd75ca4b06
commit 2916d17868
30 changed files with 1231 additions and 21 deletions

View File

@@ -22,12 +22,18 @@ public static class EmbedHelper
private static readonly HashSet<string> ImageExtensions =
[".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".avif"];
/// <summary>Extracts every distinct http/https URL from message text. De-duped so multiple occurrences don't double-embed.</summary>
public static List<string> DetectUrls(string text)
{
if (string.IsNullOrWhiteSpace(text)) return [];
return UrlPattern.Matches(text).Select(m => m.Value).Distinct().ToList();
}
/// <summary>
/// Dispatcher: classifies each URL and delegates to the appropriate Build* method.
/// Order matters — jump links and YouTube/Vimeo IDs are checked before the generic
/// image-extension and link-card paths so the more specific embed wins.
/// </summary>
public static List<View> BuildEmbeds(string text)
{
var views = new List<View>();
@@ -51,6 +57,10 @@ public static class EmbedHelper
return views;
}
/// <summary>
/// Decodes a base64 attachment to bytes and renders it as an inline Image. Used by
/// MainPage.BuildBubbleContent when a message has an image attachment.
/// </summary>
public static View BuildBase64ImageEmbed(string base64, string fileName)
{
try
@@ -86,6 +96,10 @@ public static class EmbedHelper
}
}
/// <summary>
/// Renders a non-image attachment as a tappable card. Tap → writes the bytes to a temp
/// file and hands off to the system handler via Launcher.OpenAsync.
/// </summary>
public static View BuildFileCard(string base64, string fileName, string mimeType)
{
var label = new Label
@@ -122,6 +136,7 @@ public static class EmbedHelper
};
}
/// <summary>Direct image URL → inline Image (loaded async by MAUI from the URI). Tap opens in browser.</summary>
private static View BuildImageEmbed(string url)
{
var image = new Image
@@ -146,6 +161,11 @@ public static class EmbedHelper
};
}
/// <summary>
/// Builds the "💬 Jump to linked message" card for relay://jump URLs. The actual tap
/// handler is wired by MainPage.WireJumpLinks because it needs access to the message
/// bubble dictionary that EmbedHelper doesn't know about.
/// </summary>
private static View BuildJumpCard(string relayUrl)
{
var label = new Label
@@ -167,9 +187,15 @@ public static class EmbedHelper
};
}
/// <summary>Attached property that stores the relay:// URL on the jump label so MainPage.WireJumpLinks can find it.</summary>
public static readonly BindableProperty JumpUrlProperty =
BindableProperty.CreateAttached("JumpUrl", typeof(string), typeof(EmbedHelper), null);
/// <summary>
/// Generic URL card. Starts with just the URL itself; spawns a background task to fetch
/// OG meta tags from the page and append a title/description/preview-image when the
/// response arrives. The whole card is tappable to open the URL in the browser.
/// </summary>
private static View BuildLinkCard(string url)
{
var displayUrl = url.Length > 55 ? url[..52] + "…" : url;
@@ -246,6 +272,10 @@ public static class EmbedHelper
private sealed record OgData(string? Title, string? Description, string? ImageUrl);
/// <summary>
/// 4-second-budget HTTP GET + regex extract of og:title, og:description, og:image meta
/// tags from a page's HTML. Returns null on any failure (so the link card just stays bare).
/// </summary>
private static async Task<OgData?> FetchOgTagsAsync(string url)
{
try
@@ -283,6 +313,7 @@ public static class EmbedHelper
return m.Success ? System.Net.WebUtility.HtmlDecode(m.Groups[1].Value.Trim()) : null;
}
/// <summary>True if the URL's path ends with a known image extension. Used to choose between BuildImageEmbed and BuildLinkCard.</summary>
private static bool IsImageUrl(string url)
{
try
@@ -298,6 +329,7 @@ public static class EmbedHelper
@"(?:youtube\.com/(?:watch\?(?:.*&)?v=|embed/|shorts/|v/)|youtu\.be/)([A-Za-z0-9_-]{6,})",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
/// <summary>Extracts the 11-char video ID from any YouTube URL form (watch, youtu.be, embed, shorts, /v/).</summary>
private static bool TryGetYouTubeId(string url, out string id)
{
var match = YouTubePattern.Match(url);
@@ -314,6 +346,7 @@ public static class EmbedHelper
@"vimeo\.com/(?:video/|channels/[^/]+/|groups/[^/]+/videos/)?(\d{6,})",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
/// <summary>Extracts the numeric video ID from Vimeo URLs. Handles vimeo.com/{id}, /video/{id}, channels/x/{id}, groups/x/videos/{id}.</summary>
private static bool TryGetVimeoId(string url, out string id)
{
var match = VimeoPattern.Match(url);
@@ -326,6 +359,7 @@ public static class EmbedHelper
return false;
}
/// <summary>YouTube embed card. Thumbnail comes from img.youtube.com; player swaps to the youtube.com/embed/ URL on tap.</summary>
private static View BuildYouTubeCard(string url, string videoId) =>
BuildVideoCardWithEmbed(
providerLabel: "🎬 YouTube",
@@ -334,6 +368,7 @@ public static class EmbedHelper
thumbnailUrl: $"https://img.youtube.com/vi/{videoId}/hqdefault.jpg",
embedUrl: $"https://www.youtube.com/embed/{videoId}?autoplay=1&rel=0");
/// <summary>Vimeo embed card. No thumbnail (Vimeo's API requires OAuth); placeholder stays black with a play badge until tap.</summary>
private static View BuildVimeoCard(string url, string videoId) =>
BuildVideoCardWithEmbed(
providerLabel: "🎬 Vimeo",
@@ -342,6 +377,11 @@ public static class EmbedHelper
thumbnailUrl: null, // Vimeo thumbs require an API call; skip and show a black placeholder
embedUrl: $"https://player.vimeo.com/video/{videoId}?autoplay=1");
/// <summary>
/// The lazy-swap player. Default content is BuildThumbnailPlaceholder (cheap — no WebView
/// spawned). On tap, the ContentView's content swaps to a WebView pointing at embedUrl.
/// Means 50 videos in scrollback = 50 thumbnails, not 50 WebViews.
/// </summary>
private static View BuildVideoCardWithEmbed(
string providerLabel,
Color providerColor,
@@ -399,6 +439,10 @@ public static class EmbedHelper
};
}
/// <summary>
/// 16:9 thumbnail (or solid black if no thumb URL) with a translucent black play-badge
/// overlay. Calling onPlay swaps the parent ContentView's content to the real WebView.
/// </summary>
private static View BuildThumbnailPlaceholder(string? thumbnailUrl, Action onPlay)
{
var grid = new Grid
@@ -440,6 +484,7 @@ public static class EmbedHelper
return grid;
}
/// <summary>The actual in-client video player. WebView2 (Windows) and WebKit (mobile) both handle YouTube/Vimeo embed pages.</summary>
private static View BuildEmbeddedPlayer(string embedUrl)
{
return new WebView