re-add link preview

This commit is contained in:
Ren Amamiya 2023-09-04 14:35:57 +07:00
parent 3ebcf4a981
commit c74a81cfdb
8 changed files with 246 additions and 41 deletions

129
src-tauri/Cargo.lock generated
View File

@ -1082,6 +1082,36 @@ dependencies = [
"cipher", "cipher",
] ]
[[package]]
name = "curl"
version = "0.4.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "509bd11746c7ac09ebd19f0b17782eae80aadee26237658a6b4808afb5c11a22"
dependencies = [
"curl-sys",
"libc",
"openssl-probe",
"openssl-sys",
"schannel",
"socket2 0.4.9",
"winapi",
]
[[package]]
name = "curl-sys"
version = "0.4.65+curl-8.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "961ba061c9ef2fe34bbd12b807152d96f0badd2bebe7b90ce6c8c8b7572a0986"
dependencies = [
"cc",
"libc",
"libz-sys",
"openssl-sys",
"pkg-config",
"vcpkg",
"winapi",
]
[[package]] [[package]]
name = "curve25519-dalek" name = "curve25519-dalek"
version = "3.2.0" version = "3.2.0"
@ -2098,7 +2128,21 @@ checksum = "e5c13fb08e5d4dfc151ee5e88bae63f7773d61852f3bdc73c9f4b9e1bde03148"
dependencies = [ dependencies = [
"log", "log",
"mac", "mac",
"markup5ever", "markup5ever 0.10.1",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "html5ever"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7"
dependencies = [
"log",
"mac",
"markup5ever 0.11.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 1.0.109", "syn 1.0.109",
@ -2462,7 +2506,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ea8e9c6e031377cff82ee3001dc8026cdf431ed4e2e6b51f98ab8c73484a358" checksum = "1ea8e9c6e031377cff82ee3001dc8026cdf431ed4e2e6b51f98ab8c73484a358"
dependencies = [ dependencies = [
"cssparser", "cssparser",
"html5ever", "html5ever 0.25.2",
"matches", "matches",
"selectors", "selectors",
] ]
@ -2511,6 +2555,18 @@ dependencies = [
"vcpkg", "vcpkg",
] ]
[[package]]
name = "libz-sys"
version = "1.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d97137b25e321a73eef1418d1d5d2eda4d77e12813f8e6dead84bc52c5870a7b"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]] [[package]]
name = "line-wrap" name = "line-wrap"
version = "0.1.1" version = "0.1.1"
@ -2579,6 +2635,7 @@ dependencies = [
"tauri-plugin-store", "tauri-plugin-store",
"tauri-plugin-stronghold", "tauri-plugin-stronghold",
"tauri-plugin-upload", "tauri-plugin-upload",
"webpage",
"window-shadows", "window-shadows",
"window-vibrancy", "window-vibrancy",
] ]
@ -2619,12 +2676,38 @@ checksum = "a24f40fb03852d1cdd84330cddcaf98e9ec08a7b7768e952fad3b4cf048ec8fd"
dependencies = [ dependencies = [
"log", "log",
"phf 0.8.0", "phf 0.8.0",
"phf_codegen", "phf_codegen 0.8.0",
"string_cache", "string_cache",
"string_cache_codegen", "string_cache_codegen",
"tendril", "tendril",
] ]
[[package]]
name = "markup5ever"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016"
dependencies = [
"log",
"phf 0.10.1",
"phf_codegen 0.10.0",
"string_cache",
"string_cache_codegen",
"tendril",
]
[[package]]
name = "markup5ever_rcdom"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9521dd6750f8e80ee6c53d65e2e4656d7de37064f3a7a5d2d11d05df93839c2"
dependencies = [
"html5ever 0.26.0",
"markup5ever 0.11.0",
"tendril",
"xml5ever",
]
[[package]] [[package]]
name = "matchers" name = "matchers"
version = "0.1.0" version = "0.1.0"
@ -3205,6 +3288,16 @@ dependencies = [
"phf_shared 0.8.0", "phf_shared 0.8.0",
] ]
[[package]]
name = "phf_codegen"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd"
dependencies = [
"phf_generator 0.10.0",
"phf_shared 0.10.0",
]
[[package]] [[package]]
name = "phf_generator" name = "phf_generator"
version = "0.8.0" version = "0.8.0"
@ -4022,7 +4115,7 @@ dependencies = [
"log", "log",
"matches", "matches",
"phf 0.8.0", "phf 0.8.0",
"phf_codegen", "phf_codegen 0.8.0",
"precomputed-hash", "precomputed-hash",
"servo_arc", "servo_arc",
"smallvec", "smallvec",
@ -5096,7 +5189,7 @@ dependencies = [
"dunce", "dunce",
"glob", "glob",
"heck 0.4.1", "heck 0.4.1",
"html5ever", "html5ever 0.25.2",
"infer", "infer",
"json-patch", "json-patch",
"kuchiki", "kuchiki",
@ -5754,6 +5847,19 @@ dependencies = [
"system-deps 6.1.1", "system-deps 6.1.1",
] ]
[[package]]
name = "webpage"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8598785beeb5af95abe95e7bb20c7e747d1188347080d6811d5a56d2b9a5f368"
dependencies = [
"curl",
"html5ever 0.26.0",
"markup5ever_rcdom",
"serde",
"serde_json",
]
[[package]] [[package]]
name = "webpki-roots" name = "webpki-roots"
version = "0.24.0" version = "0.24.0"
@ -6232,7 +6338,7 @@ dependencies = [
"gio", "gio",
"glib", "glib",
"gtk", "gtk",
"html5ever", "html5ever 0.25.2",
"http", "http",
"kuchiki", "kuchiki",
"libc", "libc",
@ -6305,6 +6411,17 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "xml5ever"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4034e1d05af98b51ad7214527730626f019682d797ba38b51689212118d8e650"
dependencies = [
"log",
"mac",
"markup5ever 0.11.0",
]
[[package]] [[package]]
name = "zbus" name = "zbus"
version = "3.14.1" version = "3.14.1"

View File

@ -50,6 +50,7 @@ sqlx-cli = { version = "0.7.0", default-features = false, features = [
"sqlite", "sqlite",
] } ] }
rust-argon2 = "1.0" rust-argon2 = "1.0"
webpage = { version = "1.6.0", features = ["serde"] }
[features] [features]
# this feature is used for production builds or when `devPath` points to the filesystem # this feature is used for production builds or when `devPath` points to the filesystem

View File

@ -7,6 +7,8 @@ use tauri::Manager;
use tauri_plugin_autostart::MacosLauncher; use tauri_plugin_autostart::MacosLauncher;
use tauri_plugin_sql::{Migration, MigrationKind}; use tauri_plugin_sql::{Migration, MigrationKind};
use window_shadows::set_shadow; use window_shadows::set_shadow;
use webpage::{Webpage, WebpageOptions};
use std::time::Duration;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use window_vibrancy::{apply_vibrancy, NSVisualEffectMaterial}; use window_vibrancy::{apply_vibrancy, NSVisualEffectMaterial};
@ -17,6 +19,71 @@ struct Payload {
cwd: String, cwd: String,
} }
#[derive(serde::Serialize)]
struct OpenGraphResponse {
title: String,
description: String,
url: String,
image: String,
}
async fn fetch_opengraph(url: String) -> OpenGraphResponse {
let options = WebpageOptions {
allow_insecure: false,
max_redirections: 3,
timeout: Duration::from_secs(15),
useragent: "lume - desktop app".to_string(),
..Default::default()
};
let result = match Webpage::from_url(&url, options) {
Ok(webpage) => webpage,
Err(_) => {
return OpenGraphResponse {
title: "".to_string(),
description: "".to_string(),
url: "".to_string(),
image: "".to_string(),
}
}
};
let html = result.html;
return OpenGraphResponse {
title: html
.opengraph
.properties
.get("title")
.cloned()
.unwrap_or_default(),
description: html
.opengraph
.properties
.get("description")
.cloned()
.unwrap_or_default(),
url: html
.opengraph
.properties
.get("url")
.cloned()
.unwrap_or_default(),
image: html
.opengraph
.images
.get(0)
.and_then(|i| Some(i.url.clone()))
.unwrap_or_default(),
};
}
#[tauri::command]
async fn opengraph(url: String) -> OpenGraphResponse {
let result = fetch_opengraph(url).await;
return result;
}
#[tauri::command] #[tauri::command]
async fn close_splashscreen(window: tauri::Window) { async fn close_splashscreen(window: tauri::Window) {
// Close splashscreen // Close splashscreen
@ -186,7 +253,7 @@ fn main() {
})) }))
.plugin(tauri_plugin_upload::init()) .plugin(tauri_plugin_upload::init())
.plugin(tauri_plugin_store::Builder::default().build()) .plugin(tauri_plugin_store::Builder::default().build())
.invoke_handler(tauri::generate_handler![close_splashscreen]) .invoke_handler(tauri::generate_handler![close_splashscreen, opengraph])
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
} }

View File

@ -15,27 +15,6 @@ import { useEvent } from '@utils/hooks/useEvent';
export function ChildNote({ id, root }: { id: string; root?: string }) { export function ChildNote({ id, root }: { id: string; root?: string }) {
const { status, data } = useEvent(id); const { status, data } = useEvent(id);
if (status === 'loading') {
return (
<div className="relative mb-5 overflow-hidden rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<NoteSkeleton />
</div>
);
}
if (status === 'error') {
return (
<div className="mb-5 flex flex-col gap-1.5 overflow-hidden rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<p className="text-sm font-bold text-white">
Lume cannot found the event with ID
</p>
<div className="inline-flex items-center justify-center rounded-xl border border-dashed border-red-400 bg-red-200/10 p-2">
<p className="select-text break-all text-sm font-medium text-red-400">{id}</p>
</div>
</div>
);
}
const renderKind = (event: NDKEvent) => { const renderKind = (event: NDKEvent) => {
switch (event.kind) { switch (event.kind) {
case NDKKind.Text: case NDKKind.Text:
@ -49,6 +28,40 @@ export function ChildNote({ id, root }: { id: string; root?: string }) {
} }
}; };
if (status === 'loading') {
return (
<>
<div className="absolute bottom-0 left-[18px] h-[calc(100%-3.4rem)] w-0.5 bg-gradient-to-t from-white/20 to-white/10" />
<div className="relative mb-5 overflow-hidden">
<NoteSkeleton />
</div>
</>
);
}
if (status === 'error') {
return (
<>
<div className="absolute bottom-0 left-[18px] h-[calc(100%-3.4rem)] w-0.5 bg-gradient-to-t from-white/20 to-white/10" />
<div className="relative mb-5 flex flex-col">
<div className="relative z-10 flex items-start gap-3">
<div className="h-11 w-11 rounded-lg bg-black" />
<h5 className="truncate font-semibold leading-none text-white">
Lume (System)
</h5>
</div>
<div className="-mt-6 flex items-start gap-3">
<div className="w-11 shrink-0" />
<div className="markdown relative z-20 flex-1 select-text">
<p>Event not found, click to open this note via nostr.com</p>
<p>{id}</p>
</div>
</div>
</div>
</>
);
}
return ( return (
<> <>
<div className="absolute bottom-0 left-[18px] h-[calc(100%-3.4rem)] w-0.5 bg-gradient-to-t from-white/20 to-white/10" /> <div className="absolute bottom-0 left-[18px] h-[calc(100%-3.4rem)] w-0.5 bg-gradient-to-t from-white/20 to-white/10" />

View File

@ -46,7 +46,7 @@ export const MentionNote = memo(function MentionNote({ id }: { id: string }) {
if (status === 'loading') { if (status === 'loading') {
return ( return (
<div className="mb-2 mt-3 cursor-default rounded-lg bg-white/10 px-3 py-3 backdrop-blur-xl"> <div className="mt-3 cursor-default rounded-lg bg-white/10 px-3 py-3 backdrop-blur-xl">
<NoteSkeleton /> <NoteSkeleton />
</div> </div>
); );
@ -54,8 +54,11 @@ export const MentionNote = memo(function MentionNote({ id }: { id: string }) {
if (status === 'error') { if (status === 'error') {
return ( return (
<div className="mb-2 mt-3 cursor-default rounded-lg bg-white/10 px-3 py-3 backdrop-blur-xl"> <div className="mt-3 cursor-default rounded-lg bg-white/10 px-3 py-3 backdrop-blur-xl">
<p>Can&apos;t get event from relay, ID: {id}</p> <h5 className="mb-1 text-sm font-medium text-red-500">Event not found</h5>
<div className="select-text break-all rounded border border-dashed border-red-500 p-2 text-red-500">
{id}
</div>
</div> </div>
); );
} }

View File

@ -1,3 +1,5 @@
import { Link } from 'react-router-dom';
import { Image } from '@shared/image'; import { Image } from '@shared/image';
import { useOpenGraph } from '@utils/hooks/useOpenGraph'; import { useOpenGraph } from '@utils/hooks/useOpenGraph';
@ -20,11 +22,11 @@ export function LinkPreview({ urls }: { urls: string[] }) {
</div> </div>
</div> </div>
) : ( ) : (
<a <Link
className="flex flex-col rounded-lg" to={urls[0]}
href={urls[0]}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className="flex flex-col rounded-lg"
> >
{error ? ( {error ? (
<div className="flex flex-col gap-2 px-3 py-3"> <div className="flex flex-col gap-2 px-3 py-3">
@ -42,22 +44,22 @@ export function LinkPreview({ urls }: { urls: string[] }) {
className="h-44 w-full rounded-t-lg object-cover" className="h-44 w-full rounded-t-lg object-cover"
/> />
)} )}
<div className="flex flex-col gap-2 px-3 py-3"> <div className="flex flex-col gap-1 border-t border-white/5 px-3 py-3">
<h5 className="line-clamp-1 font-medium leading-none text-white"> <h5 className="line-clamp-1 font-semibold leading-none text-white">
{data.title} {data.title}
</h5> </h5>
{data.description && ( {data.description && (
<p className="line-clamp-3 break-all text-sm text-white/50"> <p className="line-clamp-3 break-words text-sm text-white/50">
{data.description} {data.description}
</p> </p>
)} )}
<span className="mt-2.5 text-sm leading-none text-white/50"> <span className="mt-2.5 text-sm leading-none text-white/80">
{domain.hostname} {domain.hostname}
</span> </span>
</div> </div>
</> </>
)} )}
</a> </Link>
)} )}
</div> </div>
); );

View File

@ -5,7 +5,7 @@ import { Opengraph } from '@utils/types';
export function useOpenGraph(url: string) { export function useOpenGraph(url: string) {
const { status, data, error } = useQuery( const { status, data, error } = useQuery(
['preview', url], ['opg', url],
async () => { async () => {
const res: Opengraph = await invoke('opengraph', { url }); const res: Opengraph = await invoke('opengraph', { url });
if (!res) { if (!res) {
@ -14,10 +14,10 @@ export function useOpenGraph(url: string) {
return res; return res;
}, },
{ {
staleTime: Infinity,
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
refetchOnMount: false, refetchOnMount: false,
refetchOnReconnect: false, refetchOnReconnect: false,
staleTime: Infinity,
} }
); );

View File

@ -45,6 +45,8 @@ export function parser(eventContent: string) {
// remove url from original content // remove url from original content
word = word.replace(word, ''); word = word.replace(word, '');
} }
content.links.push(url.toString());
} }
// hashtag // hashtag