mirror of
https://github.com/luminous-devs/lume.git
synced 2024-09-19 11:43:30 +00:00
add link preview
This commit is contained in:
parent
be6de2344e
commit
c4bc67410c
@ -22,7 +22,7 @@
|
|||||||
"http": {
|
"http": {
|
||||||
"all": true,
|
"all": true,
|
||||||
"request": true,
|
"request": true,
|
||||||
"scope": ["https://rbr.bio/*", "https://void.cat/*", "https://metadata.lume.nu/*"]
|
"scope": ["https://void.cat/*", "https://skrape.dev/*"]
|
||||||
},
|
},
|
||||||
"fs": {
|
"fs": {
|
||||||
"all": false,
|
"all": false,
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { LinkPreview } from "./preview/link";
|
||||||
import { MentionNote } from "@app/note/components/mentions/note";
|
import { MentionNote } from "@app/note/components/mentions/note";
|
||||||
import { MentionUser } from "@app/note/components/mentions/user";
|
import { MentionUser } from "@app/note/components/mentions/user";
|
||||||
import { ImagePreview } from "@app/note/components/preview/image";
|
import { ImagePreview } from "@app/note/components/preview/image";
|
||||||
@ -32,6 +33,11 @@ export function Kind1({
|
|||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
)}
|
)}
|
||||||
|
{Array.isArray(content.links) && content.links.length ? (
|
||||||
|
<LinkPreview urls={content.links} />
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
{Array.isArray(content.notes) && content.notes.length ? (
|
{Array.isArray(content.notes) && content.notes.length ? (
|
||||||
content.notes.map((note: string) => (
|
content.notes.map((note: string) => (
|
||||||
<MentionNote key={note} id={note} />
|
<MentionNote key={note} id={note} />
|
||||||
|
@ -3,43 +3,15 @@ import { Kind1063 } from "@app/note/components/kind1063";
|
|||||||
import { NoteSkeleton } from "@app/note/components/skeleton";
|
import { NoteSkeleton } from "@app/note/components/skeleton";
|
||||||
import { NoteQuoteUser } from "@app/note/components/user/quote";
|
import { NoteQuoteUser } from "@app/note/components/user/quote";
|
||||||
import { NoteWrapper } from "@app/note/components/wrapper";
|
import { NoteWrapper } from "@app/note/components/wrapper";
|
||||||
import { RelayContext } from "@shared/relayProvider";
|
import { useEvent } from "@utils/hooks/useEvent";
|
||||||
import { READONLY_RELAYS } from "@stores/constants";
|
|
||||||
import { noteParser } from "@utils/parser";
|
import { noteParser } from "@utils/parser";
|
||||||
import { memo, useContext } from "react";
|
import { memo } from "react";
|
||||||
import useSWRSubscription from "swr/subscription";
|
|
||||||
|
|
||||||
export const MentionNote = memo(function MentionNote({ id }: { id: string }) {
|
export const MentionNote = memo(function MentionNote({ id }: { id: string }) {
|
||||||
const pool: any = useContext(RelayContext);
|
const data = useEvent(id);
|
||||||
|
|
||||||
const { data, error } = useSWRSubscription(
|
const kind1 = data?.kind === 1 ? noteParser(data) : null;
|
||||||
id ? id : null,
|
const kind1063 = data?.kind === 1063 ? data.tags : null;
|
||||||
(key, { next }) => {
|
|
||||||
const unsubscribe = pool.subscribe(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
ids: [key],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
READONLY_RELAYS,
|
|
||||||
(event: any) => {
|
|
||||||
next(null, event);
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
{
|
|
||||||
unsubscribeOnEose: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
unsubscribe();
|
|
||||||
};
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const kind1 = !error && data?.kind === 1 ? noteParser(data) : null;
|
|
||||||
const kind1063 = !error && data?.kind === 1063 ? data.tags : null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NoteWrapper
|
<NoteWrapper
|
||||||
|
@ -3,43 +3,15 @@ import { Kind1063 } from "@app/note/components/kind1063";
|
|||||||
import { NoteMetadata } from "@app/note/components/metadata";
|
import { NoteMetadata } from "@app/note/components/metadata";
|
||||||
import { NoteSkeleton } from "@app/note/components/skeleton";
|
import { NoteSkeleton } from "@app/note/components/skeleton";
|
||||||
import { NoteDefaultUser } from "@app/note/components/user/default";
|
import { NoteDefaultUser } from "@app/note/components/user/default";
|
||||||
import { RelayContext } from "@shared/relayProvider";
|
import { useEvent } from "@utils/hooks/useEvent";
|
||||||
import { READONLY_RELAYS } from "@stores/constants";
|
|
||||||
import { noteParser } from "@utils/parser";
|
import { noteParser } from "@utils/parser";
|
||||||
import { memo, useContext } from "react";
|
import { memo } from "react";
|
||||||
import useSWRSubscription from "swr/subscription";
|
|
||||||
|
|
||||||
export const NoteParent = memo(function NoteParent({ id }: { id: string }) {
|
export const NoteParent = memo(function NoteParent({ id }: { id: string }) {
|
||||||
const pool: any = useContext(RelayContext);
|
const data = useEvent(id);
|
||||||
|
|
||||||
const { data, error } = useSWRSubscription(
|
const kind1 = data?.kind === 1 ? noteParser(data) : null;
|
||||||
id ? id : null,
|
const kind1063 = data?.kind === 1063 ? data.tags : null;
|
||||||
(key, { next }) => {
|
|
||||||
const unsubscribe = pool.subscribe(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
ids: [key],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
READONLY_RELAYS,
|
|
||||||
(event: any) => {
|
|
||||||
next(null, event);
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
{
|
|
||||||
unsubscribeOnEose: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
unsubscribe();
|
|
||||||
};
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const kind1 = !error && data?.kind === 1 ? noteParser(data) : null;
|
|
||||||
const kind1063 = !error && data?.kind === 1063 ? data.tags : null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative overflow-hidden flex flex-col pb-6">
|
<div className="relative overflow-hidden flex flex-col pb-6">
|
||||||
|
@ -1,12 +1,9 @@
|
|||||||
import { Image } from "@shared/image";
|
import { Image } from "@shared/image";
|
||||||
import useEmblaCarousel from "embla-carousel-react";
|
|
||||||
|
|
||||||
export function ImagePreview({ urls }: { urls: string[] }) {
|
export function ImagePreview({ urls }: { urls: string[] }) {
|
||||||
const [emblaRef] = useEmblaCarousel();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={emblaRef} className="mt-3 overflow-hidden">
|
<div className="mt-3 overflow-hidden">
|
||||||
<div className="flex">
|
<div className="flex flex-col gap-2">
|
||||||
{urls.map((url) => (
|
{urls.map((url) => (
|
||||||
<div key={url} className="mr-2 min-w-0 grow-0 shrink-0 basis-full">
|
<div key={url} className="mr-2 min-w-0 grow-0 shrink-0 basis-full">
|
||||||
<Image
|
<Image
|
||||||
|
39
src/app/note/components/preview/link.tsx
Normal file
39
src/app/note/components/preview/link.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { Image } from "@shared/image";
|
||||||
|
import { useOpenGraph } from "@utils/hooks/useOpenGraph";
|
||||||
|
|
||||||
|
export function LinkPreview({ urls }: { urls: string[] }) {
|
||||||
|
const domain = new URL(urls[0]);
|
||||||
|
const { data, error, isLoading } = useOpenGraph(urls[0]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-3 overflow-hidden rounded-lg bg-zinc-800">
|
||||||
|
{error && <p>failed to load</p>}
|
||||||
|
{isLoading && !data ? (
|
||||||
|
<p>Loading...</p>
|
||||||
|
) : (
|
||||||
|
<a href={urls[0]} className="flex flex-col">
|
||||||
|
<Image
|
||||||
|
src={data["og:image"]}
|
||||||
|
alt={urls[0]}
|
||||||
|
className="w-full h-auto border-t-lg object-cover"
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col gap-2 px-3 py-3">
|
||||||
|
<h5 className="leading-none font-medium text-zinc-200">
|
||||||
|
{data["og:title"]}
|
||||||
|
</h5>
|
||||||
|
{data["og:description"] ? (
|
||||||
|
<p className="leading-none text-sm text-zinc-400 line-clamp-3">
|
||||||
|
{data["og:description"]}
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
<span className="mt-2.5 leading-none text-sm text-zinc-500">
|
||||||
|
{domain.hostname}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -15,7 +15,7 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.markdown {
|
.markdown {
|
||||||
@apply prose prose-zinc max-w-none select-text break-words dark:prose-invert prose-p:m-0 prose-p:leading-tight prose-a:text-[15px] prose-a:font-normal prose-a:leading-tight prose-a:text-fuchsia-500 hover:prose-a:text-fuchsia-600 prose-ol:mb-1 prose-ul:mb-1 prose-li:text-[15px] prose-li:leading-tight prose-blockquote:m-0 prose-hr:mx-0 prose-hr:my-2;
|
@apply prose prose-zinc max-w-none select-text break-words dark:prose-invert prose-p:m-0 prose-p:leading-tight prose-a:font-normal prose-a:leading-tight prose-a:text-fuchsia-500 hover:prose-a:text-fuchsia-600 prose-ol:mb-1 prose-ul:mb-1 prose-li:leading-tight prose-blockquote:m-0 prose-hr:mx-0 prose-hr:my-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* For Webkit-based browsers (Chrome, Safari and Opera) */
|
/* For Webkit-based browsers (Chrome, Safari and Opera) */
|
||||||
|
@ -5,6 +5,8 @@ export const DEFAULT_AVATAR = "https://void.cat/d/KmypFh2fBdYCEvyJrPiN89.webp";
|
|||||||
export const DEFAULT_CHANNEL_BANNER =
|
export const DEFAULT_CHANNEL_BANNER =
|
||||||
"https://bafybeiacwit7hjmdefqggxqtgh6ht5dhth7ndptwn2msl5kpkodudsr7py.ipfs.w3s.link/banner-1.jpg";
|
"https://bafybeiacwit7hjmdefqggxqtgh6ht5dhth7ndptwn2msl5kpkodudsr7py.ipfs.w3s.link/banner-1.jpg";
|
||||||
|
|
||||||
|
export const OPENGRAPH_KEY = "9EJG4SY-19Q4M5J-H8R29C9-091XPCC";
|
||||||
|
|
||||||
// read-only relay list
|
// read-only relay list
|
||||||
export const READONLY_RELAYS = [
|
export const READONLY_RELAYS = [
|
||||||
"wss://welcome.nostr.wine",
|
"wss://welcome.nostr.wine",
|
||||||
|
57
src/utils/hooks/useEvent.tsx
Normal file
57
src/utils/hooks/useEvent.tsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { RelayContext } from "@shared/relayProvider";
|
||||||
|
import { useActiveAccount } from "@stores/accounts";
|
||||||
|
import { READONLY_RELAYS } from "@stores/constants";
|
||||||
|
import { createNote, getNoteByID } from "@utils/storage";
|
||||||
|
import { getParentID } from "@utils/transform";
|
||||||
|
import { useContext } from "react";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import useSWRSubscription from "swr/subscription";
|
||||||
|
|
||||||
|
const fetcher = ([, id]) => getNoteByID(id);
|
||||||
|
|
||||||
|
export function useEvent(id: string) {
|
||||||
|
const pool: any = useContext(RelayContext);
|
||||||
|
const account = useActiveAccount((state: any) => state.account);
|
||||||
|
|
||||||
|
const { data: cache } = useSWR(["event", id], fetcher);
|
||||||
|
const { data: newest } = useSWRSubscription(
|
||||||
|
!cache ? id : null,
|
||||||
|
(key, { next }) => {
|
||||||
|
const unsubscribe = pool.subscribe(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
ids: [key],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
READONLY_RELAYS,
|
||||||
|
(event: any) => {
|
||||||
|
const parentID = getParentID(event.tags, event.id);
|
||||||
|
// insert event to local database
|
||||||
|
createNote(
|
||||||
|
event.id,
|
||||||
|
account.id,
|
||||||
|
event.pubkey,
|
||||||
|
event.kind,
|
||||||
|
event.tags,
|
||||||
|
event.content,
|
||||||
|
event.created_at,
|
||||||
|
parentID,
|
||||||
|
);
|
||||||
|
// update state
|
||||||
|
next(null, event);
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
unsubscribeOnEose: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return cache ? cache : newest;
|
||||||
|
}
|
28
src/utils/hooks/useOpenGraph.tsx
Normal file
28
src/utils/hooks/useOpenGraph.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { OPENGRAPH_KEY } from "@stores/constants";
|
||||||
|
import { fetch } from "@tauri-apps/api/http";
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
const fetcher = async (url: string) => {
|
||||||
|
const result = await fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
timeout: 20,
|
||||||
|
});
|
||||||
|
if (result.ok) {
|
||||||
|
return result.data;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useOpenGraph(url: string) {
|
||||||
|
const { data, error, isLoading } = useSWR(
|
||||||
|
`https://skrape.dev/api/opengraph/?url=${url}&key=${OPENGRAPH_KEY}`,
|
||||||
|
fetcher,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: data,
|
||||||
|
error: error,
|
||||||
|
isLoading: isLoading,
|
||||||
|
};
|
||||||
|
}
|
@ -13,12 +13,14 @@ export function noteParser(event: Event) {
|
|||||||
notes: string[];
|
notes: string[];
|
||||||
images: string[];
|
images: string[];
|
||||||
videos: string[];
|
videos: string[];
|
||||||
|
links: string[];
|
||||||
} = {
|
} = {
|
||||||
original: event.content,
|
original: event.content,
|
||||||
parsed: event.content,
|
parsed: event.content,
|
||||||
notes: [],
|
notes: [],
|
||||||
images: [],
|
images: [],
|
||||||
videos: [],
|
videos: [],
|
||||||
|
links: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
// handle media
|
// handle media
|
||||||
@ -36,10 +38,15 @@ export function noteParser(event: Event) {
|
|||||||
content.videos.push(url);
|
content.videos.push(url);
|
||||||
// remove url from original content
|
// remove url from original content
|
||||||
content.parsed = content.parsed.replace(url, "");
|
content.parsed = content.parsed.replace(url, "");
|
||||||
|
} else {
|
||||||
|
// push to store
|
||||||
|
content.links.push(url);
|
||||||
|
// remove url from original content
|
||||||
|
content.parsed = content.parsed.replace(url, "");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// map hashtag to em
|
// map hashtag to link
|
||||||
content.original.match(/#(\w+)(?!:\/\/)/g)?.forEach((item) => {
|
content.original.match(/#(\w+)(?!:\/\/)/g)?.forEach((item) => {
|
||||||
content.parsed = content.parsed.replace(item, `[${item}](/search/${item})`);
|
content.parsed = content.parsed.replace(item, `[${item}](/search/${item})`);
|
||||||
});
|
});
|
||||||
@ -47,13 +54,6 @@ export function noteParser(event: Event) {
|
|||||||
// handle nostr mention
|
// handle nostr mention
|
||||||
references.forEach((item) => {
|
references.forEach((item) => {
|
||||||
const profile = item.profile;
|
const profile = item.profile;
|
||||||
const event = item.event;
|
|
||||||
|
|
||||||
if (event) {
|
|
||||||
content.notes.push(event.id);
|
|
||||||
content.parsed = content.parsed.replace(item.text, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (profile) {
|
if (profile) {
|
||||||
content.parsed = content.parsed.replace(item.text, `*${profile.pubkey}*`);
|
content.parsed = content.parsed.replace(item.text, `*${profile.pubkey}*`);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user