diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index bcb1aa6e..0e52a00c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -22,6 +22,7 @@ mod irc; mod log; mod providers; mod server; +mod util; const CLIENT_ID: &str = "kimne78kx3ncx6brgo4mv6wki5h1ko"; @@ -194,6 +195,7 @@ fn get_handler() -> impl Fn(Invoke) -> bool { providers::fetch_recent_messages, providers::seventv::connect_seventv, providers::seventv::send_presence, - server::start_server + server::start_server, + util::extract_seo, ] } diff --git a/src-tauri/src/util.rs b/src-tauri/src/util.rs new file mode 100644 index 00000000..7db1cc5a --- /dev/null +++ b/src-tauri/src/util.rs @@ -0,0 +1,49 @@ +use std::sync::LazyLock; + +use regex::Regex; +use serde::Serialize; + +use crate::error::Error; + +#[derive(Serialize)] +pub struct Seo { + title: Option, + description: Option, + image: Option, +} + +static TITLE_RE: LazyLock = + LazyLock::new(|| Regex::new(r#"]*>([^<]+)"#).unwrap()); +static DESC_RE: LazyLock = + LazyLock::new(|| Regex::new(r#"]*name="description"[^>]*content="([^"]+)""#).unwrap()); + +static OG_TITLE_RE: LazyLock = + LazyLock::new(|| Regex::new(r#"]*property="og:title"[^>]*content="([^"]+)""#).unwrap()); +static OG_DESC_RE: LazyLock = LazyLock::new(|| { + Regex::new(r#"]*property="og:description"[^>]*content="([^"]+)""#).unwrap() +}); +static OG_IMAGE_RE: LazyLock = + LazyLock::new(|| Regex::new(r#"]*property="og:image"[^>]*content="([^"]+)""#).unwrap()); + +#[tauri::command] +pub async fn extract_seo(url: String) -> Result, Error> { + let res = reqwest::get(url).await?; + let body = res.text().await?; + + Ok(Some(Seo { + title: TITLE_RE + .captures(&body) + .or_else(|| OG_TITLE_RE.captures(&body)) + .and_then(|cap| cap.get(1).map(|m| m.as_str().to_string())) + .or(None), + description: DESC_RE + .captures(&body) + .or_else(|| OG_DESC_RE.captures(&body)) + .and_then(|cap| cap.get(1).map(|m| m.as_str().to_string())) + .or(None), + image: OG_IMAGE_RE + .captures(&body) + .and_then(|cap| cap.get(1).map(|m| m.as_str().to_string())) + .or(None), + })) +} diff --git a/src/lib/components/message/Link.svelte b/src/lib/components/message/Link.svelte new file mode 100644 index 00000000..ea7c9492 --- /dev/null +++ b/src/lib/components/message/Link.svelte @@ -0,0 +1,70 @@ + + +{#if preview} + {#await extractSeo() then seo} + {#if seo} + + + {#snippet child({ props })} + {@render link(props)} + {/snippet} + + + + {seo.title + +

+ {seo.title} +

+ +

+ {seo.description} +

+
+
+ {:else} + {@render link()} + {/if} + {/await} +{:else} + {@render link()} +{/if} + +{#snippet link(props: Record = {})} + openUrl(node.data.url.toString())} + > + {node.value} + +{/snippet} diff --git a/src/lib/components/message/Message.svelte b/src/lib/components/message/Message.svelte index 3d5c18c8..594f790d 100644 --- a/src/lib/components/message/Message.svelte +++ b/src/lib/components/message/Message.svelte @@ -6,7 +6,6 @@ + + + + diff --git a/src/lib/components/ui/hover-card/hover-card-trigger.svelte b/src/lib/components/ui/hover-card/hover-card-trigger.svelte new file mode 100644 index 00000000..322172bb --- /dev/null +++ b/src/lib/components/ui/hover-card/hover-card-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/components/ui/hover-card/index.ts b/src/lib/components/ui/hover-card/index.ts new file mode 100644 index 00000000..c7f6bc7b --- /dev/null +++ b/src/lib/components/ui/hover-card/index.ts @@ -0,0 +1,14 @@ +import { LinkPreview as HoverCardPrimitive } from "bits-ui"; +import Content from "./hover-card-content.svelte"; +import Trigger from "./hover-card-trigger.svelte"; + +const Root = HoverCardPrimitive.Root; + +export { + Content, + Root as HoverCard, + Content as HoverCardContent, + Trigger as HoverCardTrigger, + Root, + Trigger, +};