polish
This commit is contained in:
@@ -24,16 +24,12 @@ export function OnboardStep3Screen() {
|
||||
|
||||
const { publish } = useNostr();
|
||||
const { account } = useAccount();
|
||||
const { fetcher, relayUrls } = useNDK();
|
||||
const { ndk } = useNDK();
|
||||
const { status, data } = useQuery(
|
||||
['relays'],
|
||||
async () => {
|
||||
const tmp = new Map<string, string>();
|
||||
const events = await fetcher.fetchAllEvents(
|
||||
relayUrls,
|
||||
{ kinds: [10002], authors: account.follows },
|
||||
{ since: 0 }
|
||||
);
|
||||
const events = await ndk.fetchEvents({ kinds: [10002], authors: account.follows });
|
||||
|
||||
if (events) {
|
||||
events.forEach((event) => {
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { useDecryptMessage } from '@app/chats/hooks/useDecryptMessage';
|
||||
|
||||
import { MentionNote } from '@shared/notes/mentions/note';
|
||||
import { ImagePreview } from '@shared/notes/preview/image';
|
||||
import { LinkPreview } from '@shared/notes/preview/link';
|
||||
import { VideoPreview } from '@shared/notes/preview/video';
|
||||
import { NoteContent } from '@shared/notes';
|
||||
import { User } from '@shared/user';
|
||||
|
||||
import { parser } from '@utils/parser';
|
||||
@@ -22,8 +19,6 @@ export function ChatMessageItem({
|
||||
if (decryptedContent) {
|
||||
data['content'] = decryptedContent;
|
||||
}
|
||||
// parse the note content
|
||||
const content = parser(data);
|
||||
|
||||
return (
|
||||
<div className="flex h-min min-h-min w-full select-text flex-col px-5 py-3 hover:bg-white/10">
|
||||
@@ -31,13 +26,8 @@ export function ChatMessageItem({
|
||||
<User pubkey={data.sender_pubkey} time={data.created_at} isChat={true} />
|
||||
<div className="-mt-[20px] pl-[49px]">
|
||||
<p className="select-text whitespace-pre-line break-words text-base text-white">
|
||||
{content.parsed}
|
||||
{data.content}
|
||||
</p>
|
||||
{content.images.length > 0 && <ImagePreview urls={content.images} />}
|
||||
{content.videos.length > 0 && <VideoPreview urls={content.videos} />}
|
||||
{content.links.length > 0 && <LinkPreview urls={content.links} />}
|
||||
{content.notes.length > 0 &&
|
||||
content.notes.map((note: string) => <MentionNote key={note} id={note} />)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,9 +15,7 @@ export function ChatsListSelfItem({ data }: { data: { pubkey: string } }) {
|
||||
return (
|
||||
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2">
|
||||
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-white/10" />
|
||||
<div>
|
||||
<div className="h-2.5 w-full animate-pulse truncate rounded bg-white/10 text-base font-medium" />
|
||||
</div>
|
||||
<div className="h-2.5 w-2/3 animate-pulse rounded bg-white/10" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,14 +11,14 @@ import { nHoursAgo } from '@utils/date';
|
||||
import { LumeEvent, Widget } from '@utils/types';
|
||||
|
||||
export function HashtagBlock({ params }: { params: Widget }) {
|
||||
const { relayUrls, fetcher } = useNDK();
|
||||
const { ndk } = useNDK();
|
||||
const { status, data } = useQuery(['hashtag', params.content], async () => {
|
||||
const events = (await fetcher.fetchAllEvents(
|
||||
relayUrls,
|
||||
{ kinds: [1], '#t': [params.content] },
|
||||
{ since: nHoursAgo(24) }
|
||||
)) as unknown as LumeEvent[];
|
||||
return events;
|
||||
const events = await ndk.fetchEvents({
|
||||
kinds: [1],
|
||||
'#t': [params.content],
|
||||
since: nHoursAgo(24),
|
||||
});
|
||||
return [...events] as unknown as LumeEvent[];
|
||||
});
|
||||
|
||||
const parentRef = useRef();
|
||||
|
||||
@@ -14,15 +14,14 @@ import { LumeEvent, Widget } from '@utils/types';
|
||||
export function UserBlock({ params }: { params: Widget }) {
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { fetcher, relayUrls } = useNDK();
|
||||
const { ndk } = useNDK();
|
||||
const { status, data } = useQuery(['user-feed', params.content], async () => {
|
||||
const events = await fetcher.fetchAllEvents(
|
||||
relayUrls,
|
||||
{ kinds: [1], authors: [params.content] },
|
||||
{ since: nHoursAgo(48) },
|
||||
{ sort: true }
|
||||
);
|
||||
return events as unknown as LumeEvent[];
|
||||
const events = await ndk.fetchEvents({
|
||||
kinds: [1],
|
||||
authors: [params.content],
|
||||
since: nHoursAgo(48),
|
||||
});
|
||||
return [...events] as unknown as LumeEvent[];
|
||||
});
|
||||
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
@@ -42,9 +41,7 @@ export function UserBlock({ params }: { params: Widget }) {
|
||||
<UserProfile pubkey={params.content} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="mt-4 px-3 text-lg font-semibold text-white">
|
||||
Latest activities
|
||||
</h3>
|
||||
<h3 className="mt-4 px-3 text-lg font-semibold text-white">Latest postrs</h3>
|
||||
<div className="flex h-full w-full flex-col justify-between gap-1.5 pb-10">
|
||||
{status === 'loading' ? (
|
||||
<div className="px-3 py-1.5">
|
||||
@@ -57,7 +54,7 @@ export function UserBlock({ params }: { params: Widget }) {
|
||||
<div className="rounded-xl bg-white/10 px-3 py-6">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<p className="text-center text-sm text-white">
|
||||
No new posts about this hashtag in 48 hours ago
|
||||
No new posts from this user in 48 hours ago
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { fetch } from '@tauri-apps/plugin-http';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { FollowIcon, LoaderIcon, UnfollowIcon } from '@shared/icons';
|
||||
|
||||
@@ -8,21 +8,19 @@ import { TitleBar } from '@shared/titleBar';
|
||||
import { LumeEvent } from '@utils/types';
|
||||
|
||||
interface Response {
|
||||
ok: boolean;
|
||||
data: {
|
||||
notes: Array<{ event: LumeEvent }>;
|
||||
};
|
||||
notes: Array<{ event: LumeEvent }>;
|
||||
}
|
||||
|
||||
export function TrendingNotes() {
|
||||
const { status, data, error } = useQuery(
|
||||
['trending-notes'],
|
||||
async () => {
|
||||
const res: Response = await fetch('https://api.nostr.band/v0/trending/notes');
|
||||
const res = await fetch('https://api.nostr.band/v0/trending/notes');
|
||||
if (!res.ok) {
|
||||
throw new Error('Error');
|
||||
}
|
||||
return res.data?.notes;
|
||||
const json: Response = await res.json();
|
||||
return json.notes;
|
||||
},
|
||||
{
|
||||
refetchOnMount: false,
|
||||
@@ -32,6 +30,8 @@ export function TrendingNotes() {
|
||||
}
|
||||
);
|
||||
|
||||
console.log('notes: ', data);
|
||||
|
||||
return (
|
||||
<div className="scrollbar-hide relative h-full w-[400px] shrink-0 overflow-y-auto bg-white/10 pb-20">
|
||||
<TitleBar title="Trending Posts" />
|
||||
|
||||
@@ -7,21 +7,19 @@ import { NoteSkeleton } from '@shared/notes/skeleton';
|
||||
import { TitleBar } from '@shared/titleBar';
|
||||
|
||||
interface Response {
|
||||
ok: boolean;
|
||||
data: {
|
||||
profiles: Array<{ pubkey: string }>;
|
||||
};
|
||||
profiles: Array<{ pubkey: string }>;
|
||||
}
|
||||
|
||||
export function TrendingProfiles() {
|
||||
const { status, data, error } = useQuery(
|
||||
['trending-profiles'],
|
||||
async () => {
|
||||
const res: Response = await fetch('https://api.nostr.band/v0/trending/profiles');
|
||||
const res = await fetch('https://api.nostr.band/v0/trending/profiles');
|
||||
if (!res.ok) {
|
||||
throw new Error('Error');
|
||||
}
|
||||
return res.data?.profiles;
|
||||
const json: Response = await res.json();
|
||||
return json.profiles;
|
||||
},
|
||||
{
|
||||
refetchOnMount: false,
|
||||
@@ -31,6 +29,8 @@ export function TrendingProfiles() {
|
||||
}
|
||||
);
|
||||
|
||||
console.log('profiles: ', data);
|
||||
|
||||
return (
|
||||
<div className="scrollbar-hide relative h-full w-[400px] shrink-0 overflow-y-auto bg-white/10 pb-20">
|
||||
<TitleBar title="Trending Profiles" />
|
||||
@@ -44,7 +44,7 @@ export function TrendingProfiles() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative flex w-full flex-col gap-3 px-3 pt-1.5">
|
||||
{data.map((item) => (
|
||||
{data?.map((item) => (
|
||||
<Profile key={item.pubkey} data={item} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -12,15 +12,14 @@ import { LumeEvent } from '@utils/types';
|
||||
export function UserFeed({ pubkey }: { pubkey: string }) {
|
||||
const parentRef = useRef();
|
||||
|
||||
const { fetcher, relayUrls } = useNDK();
|
||||
const { ndk } = useNDK();
|
||||
const { status, data } = useQuery(['user-feed', pubkey], async () => {
|
||||
const events = await fetcher.fetchAllEvents(
|
||||
relayUrls,
|
||||
{ kinds: [1], authors: [pubkey] },
|
||||
{ since: nHoursAgo(48) },
|
||||
{ sort: true }
|
||||
);
|
||||
return events as unknown as LumeEvent[];
|
||||
const events = await ndk.fetchEvents({
|
||||
kinds: [1],
|
||||
authors: [pubkey],
|
||||
since: nHoursAgo(48),
|
||||
});
|
||||
return [...events] as unknown as LumeEvent[];
|
||||
});
|
||||
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
|
||||
@@ -16,15 +16,14 @@ export function UserScreen() {
|
||||
const parentRef = useRef();
|
||||
|
||||
const { pubkey } = useParams();
|
||||
const { fetcher, relayUrls } = useNDK();
|
||||
const { ndk } = useNDK();
|
||||
const { status, data } = useQuery(['user-feed', pubkey], async () => {
|
||||
const events = await fetcher.fetchAllEvents(
|
||||
relayUrls,
|
||||
{ kinds: [1], authors: [pubkey] },
|
||||
{ since: nHoursAgo(48) },
|
||||
{ sort: true }
|
||||
);
|
||||
return events as unknown as LumeEvent[];
|
||||
const events = await ndk.fetchEvents({
|
||||
kinds: [1],
|
||||
authors: [pubkey],
|
||||
since: nHoursAgo(48),
|
||||
});
|
||||
return [...events] as unknown as LumeEvent[];
|
||||
});
|
||||
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// inspire by: https://github.com/nostr-dev-kit/ndk-react/
|
||||
import NDK from '@nostr-dev-kit/ndk';
|
||||
import { ndkAdapter } from '@nostr-fetch/adapter-ndk';
|
||||
import { fetch } from '@tauri-apps/plugin-http';
|
||||
import { NostrFetcher } from 'nostr-fetch';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import TauriAdapter from '@libs/ndk/cache';
|
||||
@@ -15,10 +13,6 @@ export const NDKInstance = () => {
|
||||
const [relayUrls, setRelayUrls] = useState<string[]>([]);
|
||||
|
||||
const cacheAdapter = useMemo(() => new TauriAdapter(), []);
|
||||
const fetcher = useMemo<NostrFetcher>(
|
||||
() => (ndk ? NostrFetcher.withCustomPool(ndkAdapter(ndk)) : undefined),
|
||||
[ndk]
|
||||
);
|
||||
|
||||
// TODO: fully support NIP-11
|
||||
async function verifyRelays(relays: string[]) {
|
||||
@@ -37,7 +31,6 @@ export const NDKInstance = () => {
|
||||
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: { Accept: 'application/nostr+json' },
|
||||
});
|
||||
|
||||
@@ -67,7 +60,7 @@ export const NDKInstance = () => {
|
||||
const instance = new NDK({ explicitRelayUrls, cacheAdapter });
|
||||
|
||||
try {
|
||||
await instance.connect();
|
||||
await instance.connect(10000);
|
||||
} catch (error) {
|
||||
throw new Error('NDK instance init failed: ', error);
|
||||
}
|
||||
@@ -87,6 +80,5 @@ export const NDKInstance = () => {
|
||||
return {
|
||||
ndk,
|
||||
relayUrls,
|
||||
fetcher,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// source: https://github.com/nostr-dev-kit/ndk-react/
|
||||
import NDK from '@nostr-dev-kit/ndk';
|
||||
import { NostrFetcher } from 'nostr-fetch';
|
||||
import { PropsWithChildren, createContext, useContext } from 'react';
|
||||
|
||||
import { NDKInstance } from '@libs/ndk/instance';
|
||||
@@ -8,24 +7,21 @@ import { NDKInstance } from '@libs/ndk/instance';
|
||||
interface NDKContext {
|
||||
ndk: NDK;
|
||||
relayUrls: string[];
|
||||
fetcher: NostrFetcher;
|
||||
}
|
||||
|
||||
const NDKContext = createContext<NDKContext>({
|
||||
ndk: new NDK({}),
|
||||
relayUrls: [],
|
||||
fetcher: undefined,
|
||||
});
|
||||
|
||||
const NDKProvider = ({ children }: PropsWithChildren<object>) => {
|
||||
const { ndk, relayUrls, fetcher } = NDKInstance();
|
||||
const { ndk, relayUrls } = NDKInstance();
|
||||
|
||||
return (
|
||||
<NDKContext.Provider
|
||||
value={{
|
||||
ndk,
|
||||
relayUrls,
|
||||
fetcher,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
// source: https://github.com/nbd-wtf/nostr-tools/blob/b1fc8ab401b8074f53e6a05a1a6a13422fb01b2d/nip44.ts
|
||||
import { xchacha20 } from '@noble/ciphers/chacha';
|
||||
import { secp256k1 } from '@noble/curves/secp256k1';
|
||||
import { sha256 } from '@noble/hashes/sha256';
|
||||
import { randomBytes } from '@noble/hashes/utils';
|
||||
import { base64 } from '@scure/base';
|
||||
|
||||
export function getConversationKey(privkeyA: string, pubkeyB: string) {
|
||||
const key = secp256k1.getSharedSecret(privkeyA, '02' + pubkeyB);
|
||||
return sha256(key.slice(1, 33));
|
||||
}
|
||||
|
||||
export function nip44Encrypt(
|
||||
privkey: string,
|
||||
pubkey: string,
|
||||
text: string,
|
||||
ver = 1
|
||||
): string {
|
||||
if (ver !== 1) throw new Error('NIP44: unknown encryption version');
|
||||
|
||||
const key = getConversationKey(privkey, pubkey);
|
||||
const nonce = randomBytes(24);
|
||||
const plaintext = new TextEncoder().encode(text);
|
||||
const ciphertext = xchacha20(key, nonce, plaintext, plaintext);
|
||||
const ctb64 = base64.encode(ciphertext);
|
||||
const nonceb64 = base64.encode(nonce);
|
||||
|
||||
return JSON.stringify({ ciphertext: ctb64, nonce: nonceb64, v: 1 });
|
||||
}
|
||||
|
||||
export function nip44Decrypt(privkey: string, pubkey: string, data: string): string {
|
||||
const dt = JSON.parse(data);
|
||||
if (dt.v !== 1) throw new Error('NIP44: unknown encryption version');
|
||||
|
||||
let { ciphertext, nonce } = dt;
|
||||
ciphertext = base64.decode(ciphertext);
|
||||
nonce = base64.decode(nonce);
|
||||
|
||||
const key = getConversationKey(privkey, pubkey);
|
||||
const plaintext = xchacha20(key, nonce, ciphertext, ciphertext);
|
||||
const text = new TextDecoder('utf-8').decode(plaintext);
|
||||
|
||||
return text;
|
||||
}
|
||||
@@ -19,7 +19,13 @@ interface IPreFetchedResource {
|
||||
imagesPropertyType?: string;
|
||||
proxyUrl?: string;
|
||||
url: string;
|
||||
data: any;
|
||||
data: string;
|
||||
}
|
||||
|
||||
function throwOnLoopback(address: string) {
|
||||
if (OPENGRAPH.REGEX_LOOPBACK.test(address)) {
|
||||
throw new Error('SSRF request detected, trying to query host');
|
||||
}
|
||||
}
|
||||
|
||||
function metaTag(doc: cheerio.CheerioAPI, type: string, attr: string) {
|
||||
@@ -28,42 +34,42 @@ function metaTag(doc: cheerio.CheerioAPI, type: string, attr: string) {
|
||||
}
|
||||
|
||||
function metaTagContent(doc: cheerio.CheerioAPI, type: string, attr: string) {
|
||||
return doc(`meta[${attr}='${type}']`).attr('content');
|
||||
return doc(`meta[${attr}='${type}']`).attr(`content`);
|
||||
}
|
||||
|
||||
function getTitle(doc: cheerio.CheerioAPI) {
|
||||
let title =
|
||||
metaTagContent(doc, 'og:title', 'property') ||
|
||||
metaTagContent(doc, 'og:title', 'name');
|
||||
metaTagContent(doc, `og:title`, `property`) ||
|
||||
metaTagContent(doc, `og:title`, `name`);
|
||||
if (!title) {
|
||||
title = doc('title').text();
|
||||
title = doc(`title`).text();
|
||||
}
|
||||
return title;
|
||||
}
|
||||
|
||||
function getSiteName(doc: cheerio.CheerioAPI) {
|
||||
const siteName =
|
||||
metaTagContent(doc, 'og:site_name', 'property') ||
|
||||
metaTagContent(doc, 'og:site_name', 'name');
|
||||
metaTagContent(doc, `og:site_name`, `property`) ||
|
||||
metaTagContent(doc, `og:site_name`, `name`);
|
||||
return siteName;
|
||||
}
|
||||
|
||||
function getDescription(doc: cheerio.CheerioAPI) {
|
||||
const description =
|
||||
metaTagContent(doc, 'description', 'name') ||
|
||||
metaTagContent(doc, 'Description', 'name') ||
|
||||
metaTagContent(doc, 'og:description', 'property');
|
||||
metaTagContent(doc, `description`, `name`) ||
|
||||
metaTagContent(doc, `Description`, `name`) ||
|
||||
metaTagContent(doc, `og:description`, `property`);
|
||||
return description;
|
||||
}
|
||||
|
||||
function getMediaType(doc: cheerio.CheerioAPI) {
|
||||
const node = metaTag(doc, 'medium', 'name');
|
||||
const node = metaTag(doc, `medium`, `name`);
|
||||
if (node) {
|
||||
const content = node.attr('content');
|
||||
return content === 'image' ? 'photo' : content;
|
||||
const content = node.attr(`content`);
|
||||
return content === `image` ? `photo` : content;
|
||||
}
|
||||
return (
|
||||
metaTagContent(doc, 'og:type', 'property') || metaTagContent(doc, 'og:type', 'name')
|
||||
metaTagContent(doc, `og:type`, `property`) || metaTagContent(doc, `og:type`, `name`)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -77,14 +83,14 @@ function getImages(
|
||||
let src: string | undefined;
|
||||
let dic: Record<string, boolean> = {};
|
||||
|
||||
const imagePropertyType = imagesPropertyType ?? 'og';
|
||||
const imagePropertyType = imagesPropertyType ?? `og`;
|
||||
nodes =
|
||||
metaTag(doc, `${imagePropertyType}:image`, 'property') ||
|
||||
metaTag(doc, `${imagePropertyType}:image`, 'name');
|
||||
metaTag(doc, `${imagePropertyType}:image`, `property`) ||
|
||||
metaTag(doc, `${imagePropertyType}:image`, `name`);
|
||||
|
||||
if (nodes) {
|
||||
nodes.each((_: number, node: cheerio.Element) => {
|
||||
if (node.type === 'tag') {
|
||||
if (node.type === `tag`) {
|
||||
src = node.attribs.content;
|
||||
if (src) {
|
||||
src = new URL(src, rootUrl).href;
|
||||
@@ -95,18 +101,18 @@ function getImages(
|
||||
}
|
||||
|
||||
if (images.length <= 0 && !imagesPropertyType) {
|
||||
src = doc('link[rel=image_src]').attr('href');
|
||||
src = doc(`link[rel=image_src]`).attr(`href`);
|
||||
if (src) {
|
||||
src = new URL(src, rootUrl).href;
|
||||
images = [src];
|
||||
} else {
|
||||
nodes = doc('img');
|
||||
nodes = doc(`img`);
|
||||
|
||||
if (nodes?.length) {
|
||||
dic = {};
|
||||
images = [];
|
||||
nodes.each((_: number, node: cheerio.Element) => {
|
||||
if (node.type === 'tag') src = node.attribs.src;
|
||||
if (node.type === `tag`) src = node.attribs.src;
|
||||
if (src && !dic[src]) {
|
||||
dic[src] = true;
|
||||
// width = node.attribs.width;
|
||||
@@ -135,32 +141,32 @@ function getVideos(doc: cheerio.CheerioAPI) {
|
||||
let videoObj;
|
||||
let index;
|
||||
|
||||
const nodes = metaTag(doc, 'og:video', 'property') || metaTag(doc, 'og:video', 'name');
|
||||
const nodes = metaTag(doc, `og:video`, `property`) || metaTag(doc, `og:video`, `name`);
|
||||
|
||||
if (nodes?.length) {
|
||||
nodeTypes =
|
||||
metaTag(doc, 'og:video:type', 'property') || metaTag(doc, 'og:video:type', 'name');
|
||||
metaTag(doc, `og:video:type`, `property`) || metaTag(doc, `og:video:type`, `name`);
|
||||
nodeSecureUrls =
|
||||
metaTag(doc, 'og:video:secure_url', 'property') ||
|
||||
metaTag(doc, 'og:video:secure_url', 'name');
|
||||
metaTag(doc, `og:video:secure_url`, `property`) ||
|
||||
metaTag(doc, `og:video:secure_url`, `name`);
|
||||
width =
|
||||
metaTagContent(doc, 'og:video:width', 'property') ||
|
||||
metaTagContent(doc, 'og:video:width', 'name');
|
||||
metaTagContent(doc, `og:video:width`, `property`) ||
|
||||
metaTagContent(doc, `og:video:width`, `name`);
|
||||
height =
|
||||
metaTagContent(doc, 'og:video:height', 'property') ||
|
||||
metaTagContent(doc, 'og:video:height', 'name');
|
||||
metaTagContent(doc, `og:video:height`, `property`) ||
|
||||
metaTagContent(doc, `og:video:height`, `name`);
|
||||
|
||||
for (index = 0; index < nodes.length; index += 1) {
|
||||
const node = nodes[index];
|
||||
if (node.type === 'tag') video = node.attribs.content;
|
||||
if (node.type === `tag`) video = node.attribs.content;
|
||||
|
||||
nodeType = nodeTypes?.[index];
|
||||
if (nodeType?.type === 'tag') {
|
||||
if (nodeType?.type === `tag`) {
|
||||
videoType = nodeType ? nodeType.attribs.content : null;
|
||||
}
|
||||
|
||||
nodeSecureUrl = nodeSecureUrls?.[index];
|
||||
if (nodeSecureUrl?.type === 'tag') {
|
||||
if (nodeSecureUrl?.type === `tag`) {
|
||||
videoSecureUrl = nodeSecureUrl ? nodeSecureUrl.attribs.content : null;
|
||||
}
|
||||
|
||||
@@ -171,7 +177,7 @@ function getVideos(doc: cheerio.CheerioAPI) {
|
||||
width,
|
||||
height,
|
||||
};
|
||||
if (videoType && videoType.indexOf('video/') === 0) {
|
||||
if (videoType && videoType.indexOf(`video/`) === 0) {
|
||||
videos.splice(0, 0, videoObj);
|
||||
} else {
|
||||
videos.push(videoObj);
|
||||
@@ -193,7 +199,7 @@ function getFavicons(doc: cheerio.CheerioAPI, rootUrl: string) {
|
||||
let nodes: cheerio.Cheerio<cheerio.Element> | never[] = [];
|
||||
let src: string | undefined;
|
||||
|
||||
const relSelectors = ['rel=icon', `rel="shortcut icon"`, 'rel=apple-touch-icon'];
|
||||
const relSelectors = [`rel=icon`, `rel="shortcut icon"`, `rel=apple-touch-icon`];
|
||||
|
||||
relSelectors.forEach((relSelector) => {
|
||||
// look for all icon tags
|
||||
@@ -202,9 +208,9 @@ function getFavicons(doc: cheerio.CheerioAPI, rootUrl: string) {
|
||||
// collect all images from icon tags
|
||||
if (nodes.length) {
|
||||
nodes.each((_: number, node: cheerio.Element) => {
|
||||
if (node.type === 'tag') src = node.attribs.href;
|
||||
if (node.type === `tag`) src = node.attribs.href;
|
||||
if (src) {
|
||||
src = new URL(rootUrl).href;
|
||||
src = new URL(src, rootUrl).href;
|
||||
images.push(src);
|
||||
}
|
||||
});
|
||||
@@ -222,7 +228,7 @@ function getFavicons(doc: cheerio.CheerioAPI, rootUrl: string) {
|
||||
function parseImageResponse(url: string, contentType: string) {
|
||||
return {
|
||||
url,
|
||||
mediaType: 'image',
|
||||
mediaType: `image`,
|
||||
contentType,
|
||||
favicons: [getDefaultFavicon(url)],
|
||||
};
|
||||
@@ -231,7 +237,7 @@ function parseImageResponse(url: string, contentType: string) {
|
||||
function parseAudioResponse(url: string, contentType: string) {
|
||||
return {
|
||||
url,
|
||||
mediaType: 'audio',
|
||||
mediaType: `audio`,
|
||||
contentType,
|
||||
favicons: [getDefaultFavicon(url)],
|
||||
};
|
||||
@@ -240,7 +246,7 @@ function parseAudioResponse(url: string, contentType: string) {
|
||||
function parseVideoResponse(url: string, contentType: string) {
|
||||
return {
|
||||
url,
|
||||
mediaType: 'video',
|
||||
mediaType: `video`,
|
||||
contentType,
|
||||
favicons: [getDefaultFavicon(url)],
|
||||
};
|
||||
@@ -249,7 +255,7 @@ function parseVideoResponse(url: string, contentType: string) {
|
||||
function parseApplicationResponse(url: string, contentType: string) {
|
||||
return {
|
||||
url,
|
||||
mediaType: 'application',
|
||||
mediaType: `application`,
|
||||
contentType,
|
||||
favicons: [getDefaultFavicon(url)],
|
||||
};
|
||||
@@ -268,7 +274,7 @@ function parseTextResponse(
|
||||
title: getTitle(doc),
|
||||
siteName: getSiteName(doc),
|
||||
description: getDescription(doc),
|
||||
mediaType: getMediaType(doc) || 'website',
|
||||
mediaType: getMediaType(doc) || `website`,
|
||||
contentType,
|
||||
images: getImages(doc, url, options.imagesPropertyType),
|
||||
videos: getVideos(doc),
|
||||
@@ -287,11 +293,11 @@ function parseUnknownResponse(
|
||||
|
||||
function parseResponse(response: IPreFetchedResource, options?: ILinkPreviewOptions) {
|
||||
try {
|
||||
let contentType = response.headers['content-type'];
|
||||
let contentType = response.headers[`content-type`];
|
||||
// console.warn(`original content type`, contentType);
|
||||
if (contentType?.indexOf(';')) {
|
||||
if (contentType?.indexOf(`;`)) {
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
contentType = contentType.split(';')[0];
|
||||
contentType = contentType.split(`;`)[0];
|
||||
// console.warn(`splitting content type`, contentType);
|
||||
}
|
||||
|
||||
@@ -330,19 +336,117 @@ function parseResponse(response: IPreFetchedResource, options?: ILinkPreviewOpti
|
||||
}
|
||||
}
|
||||
|
||||
export async function getLinkPreview(text: string) {
|
||||
const fetchUrl = text;
|
||||
const options = {
|
||||
method: 'GET',
|
||||
timeout: 5,
|
||||
};
|
||||
|
||||
let response = await fetch(fetchUrl, options);
|
||||
|
||||
if (response.status > 300 && response.status < 309) {
|
||||
const forwardedUrl = response.headers.location || '';
|
||||
response = await fetch(forwardedUrl, options);
|
||||
/**
|
||||
* Parses the text, extracts the first link it finds and does a HTTP request
|
||||
* to fetch the website content, afterwards it tries to parse the internal HTML
|
||||
* and extract the information via meta tags
|
||||
* @param text string, text to be parsed
|
||||
* @param options ILinkPreviewOptions
|
||||
*/
|
||||
export async function getLinkPreview(text: string, options?: ILinkPreviewOptions) {
|
||||
if (!text || typeof text !== `string`) {
|
||||
throw new Error(`link-preview-js did not receive a valid url or text`);
|
||||
}
|
||||
|
||||
return parseResponse(response);
|
||||
const detectedUrl = text
|
||||
.replace(/\n/g, ` `)
|
||||
.split(` `)
|
||||
.find((token) => OPENGRAPH.REGEX_VALID_URL.test(token));
|
||||
|
||||
if (!detectedUrl) {
|
||||
throw new Error(`link-preview-js did not receive a valid a url or text`);
|
||||
}
|
||||
|
||||
if (options?.followRedirects === `manual` && !options?.handleRedirects) {
|
||||
throw new Error(
|
||||
`link-preview-js followRedirects is set to manual, but no handleRedirects function was provided`
|
||||
);
|
||||
}
|
||||
|
||||
if (options?.resolveDNSHost) {
|
||||
const resolvedUrl = await options.resolveDNSHost(detectedUrl);
|
||||
|
||||
throwOnLoopback(resolvedUrl);
|
||||
}
|
||||
|
||||
const timeout = options?.timeout ?? 3000; // 3 second timeout default
|
||||
const controller = new AbortController();
|
||||
const timeoutCounter = setTimeout(() => controller.abort(), timeout);
|
||||
|
||||
const fetchOptions = {
|
||||
headers: options?.headers ?? {},
|
||||
redirect: options?.followRedirects ?? `error`,
|
||||
signal: controller.signal,
|
||||
};
|
||||
|
||||
const fetchUrl = options?.proxyUrl ? options.proxyUrl.concat(detectedUrl) : detectedUrl;
|
||||
|
||||
// Seems like fetchOptions type definition is out of date
|
||||
// https://github.com/node-fetch/node-fetch/issues/741
|
||||
let response = await fetch(fetchUrl, fetchOptions as any).catch((e) => {
|
||||
if (e.name === `AbortError`) {
|
||||
throw new Error(`Request timeout`);
|
||||
}
|
||||
|
||||
clearTimeout(timeoutCounter);
|
||||
throw e;
|
||||
});
|
||||
|
||||
if (
|
||||
response.status > 300 &&
|
||||
response.status < 309 &&
|
||||
fetchOptions.redirect === `manual` &&
|
||||
options?.handleRedirects
|
||||
) {
|
||||
const forwardedUrl = response.headers.get(`location`) || ``;
|
||||
|
||||
if (!options.handleRedirects(fetchUrl, forwardedUrl)) {
|
||||
throw new Error(`link-preview-js could not handle redirect`);
|
||||
}
|
||||
|
||||
if (options?.resolveDNSHost) {
|
||||
const resolvedUrl = await options.resolveDNSHost(forwardedUrl);
|
||||
|
||||
throwOnLoopback(resolvedUrl);
|
||||
}
|
||||
|
||||
response = await fetch(forwardedUrl, fetchOptions as any);
|
||||
}
|
||||
|
||||
clearTimeout(timeoutCounter);
|
||||
|
||||
const headers: Record<string, string> = {};
|
||||
response.headers.forEach((header, key) => {
|
||||
headers[key] = header;
|
||||
});
|
||||
|
||||
const normalizedResponse: IPreFetchedResource = {
|
||||
url: options?.proxyUrl ? response.url.replace(options.proxyUrl, ``) : response.url,
|
||||
headers,
|
||||
data: await response.text(),
|
||||
};
|
||||
|
||||
return parseResponse(normalizedResponse, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip the library fetching the website for you, instead pass a response object
|
||||
* from whatever source you get and use the internal parsing of the HTML to return
|
||||
* the necessary information
|
||||
* @param response Preview Response
|
||||
* @param options IPreviewLinkOptions
|
||||
*/
|
||||
export async function getPreviewFromContent(
|
||||
response: IPreFetchedResource,
|
||||
options?: ILinkPreviewOptions
|
||||
) {
|
||||
if (!response || typeof response !== `object`) {
|
||||
throw new Error(`link-preview-js did not receive a valid response object`);
|
||||
}
|
||||
|
||||
if (!response.url) {
|
||||
throw new Error(`link-preview-js did not receive a valid response object`);
|
||||
}
|
||||
|
||||
return parseResponse(response, options);
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ export function Composer() {
|
||||
await publish({ content: serializedContent, kind: 1, tags });
|
||||
|
||||
// send native notifiation
|
||||
await sendNativeNotification('Publish post successfully');
|
||||
await sendNativeNotification('Publish postr successfully');
|
||||
|
||||
// update state
|
||||
setStatus('done');
|
||||
|
||||
@@ -42,7 +42,7 @@ export function ComposerModal() {
|
||||
<ChevronRightIcon className="h-4 w-4 text-white/50" />
|
||||
</span>
|
||||
<div className="inline-flex h-7 w-max items-center justify-center gap-0.5 rounded bg-white/10 pl-3 pr-1.5 text-sm font-medium text-white">
|
||||
New Post
|
||||
New Postr
|
||||
<ChevronDownIcon className="h-4 w-4" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@ export function MentionUser({ pubkey }: { pubkey: string }) {
|
||||
onClick={() =>
|
||||
setWidget({
|
||||
kind: BLOCK_KINDS.user,
|
||||
title: user?.nip05 || user?.name || user?.displayNam,
|
||||
title: user?.nip05 || user?.name || user?.display_name,
|
||||
content: pubkey,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,22 +7,25 @@ import { NoteSkeleton, Reply } from '@shared/notes';
|
||||
import { LumeEvent } from '@utils/types';
|
||||
|
||||
export function RepliesList({ id }: { id: string }) {
|
||||
const { relayUrls, fetcher } = useNDK();
|
||||
const { ndk } = useNDK();
|
||||
const { status, data } = useQuery(['thread', id], async () => {
|
||||
const events = (await fetcher.fetchAllEvents(
|
||||
relayUrls,
|
||||
{ kinds: [1], '#e': [id] },
|
||||
{ since: 0 }
|
||||
)) as unknown as LumeEvent[];
|
||||
if (events.length > 0) {
|
||||
const events = await ndk.fetchEvents({
|
||||
kinds: [1],
|
||||
'#e': [id],
|
||||
since: 0,
|
||||
});
|
||||
|
||||
const array = [...events] as unknown as LumeEvent[];
|
||||
|
||||
if (array.length > 0) {
|
||||
const replies = new Set();
|
||||
events.forEach((event) => {
|
||||
array.forEach((event) => {
|
||||
const tags = event.tags.filter((el) => el[0] === 'e' && el[1] !== id);
|
||||
if (tags.length > 0) {
|
||||
tags.forEach((tag) => {
|
||||
const rootIndex = events.findIndex((el) => el.id === tag[1]);
|
||||
const rootIndex = array.findIndex((el) => el.id === tag[1]);
|
||||
if (rootIndex) {
|
||||
const rootEvent = events[rootIndex];
|
||||
const rootEvent = array[rootIndex];
|
||||
if (rootEvent.replies) {
|
||||
rootEvent.replies.push(event);
|
||||
} else {
|
||||
@@ -33,10 +36,10 @@ export function RepliesList({ id }: { id: string }) {
|
||||
});
|
||||
}
|
||||
});
|
||||
const cleanEvents = events.filter((ev) => !replies.has(ev.id));
|
||||
const cleanEvents = array.filter((ev) => !replies.has(ev.id));
|
||||
return cleanEvents;
|
||||
}
|
||||
return events;
|
||||
return array;
|
||||
});
|
||||
|
||||
if (status === 'loading') {
|
||||
|
||||
@@ -12,16 +12,16 @@ import { nHoursAgo } from '@utils/date';
|
||||
import { LumeEvent } from '@utils/types';
|
||||
|
||||
export function NotificationModal({ pubkey }: { pubkey: string }) {
|
||||
const { fetcher, relayUrls } = useNDK();
|
||||
const { ndk } = useNDK();
|
||||
const { status, data } = useQuery(
|
||||
['notification', pubkey],
|
||||
async () => {
|
||||
const events = await fetcher.fetchAllEvents(
|
||||
relayUrls,
|
||||
{ '#p': [pubkey], kinds: [1, 6, 7, 9735] },
|
||||
{ since: nHoursAgo(24) }
|
||||
);
|
||||
const filterSelf = events.filter((el) => el.pubkey !== pubkey);
|
||||
const events = await ndk.fetchEvents({
|
||||
'#p': [pubkey],
|
||||
kinds: [1, 6, 7, 9735],
|
||||
since: nHoursAgo(24),
|
||||
});
|
||||
const filterSelf = [...events].filter((el) => el.pubkey !== pubkey);
|
||||
const sorted = filterSelf.sort((a, b) => a.created_at - b.created_at);
|
||||
return sorted as unknown as LumeEvent[];
|
||||
},
|
||||
|
||||
@@ -17,7 +17,7 @@ export function NotiMention({ event }: { event: NDKEvent }) {
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start gap-1">
|
||||
<NotiUser pubkey={event.pubkey} />
|
||||
<p className="leading-none text-white/50">reply your post</p>
|
||||
<p className="leading-none text-white/50">reply your postr</p>
|
||||
</div>
|
||||
<span className="leading-none text-white/50">{createdAt}</span>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@ export function NotiRepost({ event }: { event: NDKEvent }) {
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start gap-1">
|
||||
<NotiUser pubkey={event.pubkey} />
|
||||
<p className="leading-none text-white/50">repost your post</p>
|
||||
<p className="leading-none text-white/50">repost your postr</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="leading-none text-white/50">{createdAt}</span>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { NDKEvent, NDKKind, NDKPrivateKeySigner, NDKUser } from '@nostr-dev-kit/ndk';
|
||||
import { ndkAdapter } from '@nostr-fetch/adapter-ndk';
|
||||
import destr from 'destr';
|
||||
import { LRUCache } from 'lru-cache';
|
||||
import { NostrEvent } from 'nostr-fetch';
|
||||
import { NostrFetcher } from 'nostr-fetch';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useNDK } from '@libs/ndk/provider';
|
||||
import {
|
||||
@@ -19,11 +21,12 @@ import { nHoursAgo } from '@utils/date';
|
||||
import { useAccount } from '@utils/hooks/useAccount';
|
||||
|
||||
export function useNostr() {
|
||||
const privkey = useStronghold((state) => state.privkey);
|
||||
|
||||
const { ndk, relayUrls, fetcher } = useNDK();
|
||||
const { ndk, relayUrls } = useNDK();
|
||||
const { account } = useAccount();
|
||||
|
||||
const fetcher = useMemo(() => NostrFetcher.withCustomPool(ndkAdapter(ndk)), [ndk]);
|
||||
const privkey = useStronghold((state) => state.privkey);
|
||||
|
||||
async function fetchNetwork(prevFollow?: string[]) {
|
||||
const follows = new Set<string>(prevFollow || []);
|
||||
const lruNetwork = new LRUCache<string, string, void>({ max: 300 });
|
||||
@@ -43,14 +46,9 @@ export function useNostr() {
|
||||
// fetch network
|
||||
if (!account.network) {
|
||||
console.log("fetching user's network...");
|
||||
const events = await fetcher.fetchAllEvents(
|
||||
relayUrls,
|
||||
{ kinds: [3], authors: [...follows] },
|
||||
{ since: 0 },
|
||||
{ skipVerification: true }
|
||||
);
|
||||
const events = await ndk.fetchEvents({ kinds: [3], authors: [...follows] });
|
||||
|
||||
events.forEach((event: NostrEvent) => {
|
||||
events.forEach((event: NDKEvent) => {
|
||||
event.tags.forEach((tag) => {
|
||||
if (tag[0] === 'p') lruNetwork.set(tag[1], tag[1]);
|
||||
});
|
||||
@@ -88,9 +86,11 @@ export function useNostr() {
|
||||
|
||||
const events = await fetcher.fetchAllEvents(
|
||||
relayUrls,
|
||||
{ kinds: [1], authors: network },
|
||||
{ since: since },
|
||||
{ skipVerification: true }
|
||||
{
|
||||
kinds: [1],
|
||||
authors: network,
|
||||
},
|
||||
{ since: since }
|
||||
);
|
||||
|
||||
for (const event of events) {
|
||||
@@ -117,17 +117,25 @@ export function useNostr() {
|
||||
if (!ndk) return { status: 'failed', message: 'NDK instance not found' };
|
||||
|
||||
const lastLogin = await getLastLogin();
|
||||
const incomingMessages = await fetcher.fetchAllEvents(
|
||||
|
||||
const outgoingMessages = await fetcher.fetchAllEvents(
|
||||
relayUrls,
|
||||
{
|
||||
kinds: [4],
|
||||
'#p': [account.pubkey],
|
||||
authors: [account.pubkey],
|
||||
},
|
||||
{ since: lastLogin },
|
||||
{ skipVerification: true }
|
||||
{ since: lastLogin }
|
||||
);
|
||||
|
||||
for (const event of incomingMessages) {
|
||||
const incomingMessages = await fetcher.fetchAllEvents(
|
||||
relayUrls,
|
||||
{ kinds: [4], '#p': [account.pubkey] },
|
||||
{ since: lastLogin }
|
||||
);
|
||||
|
||||
const messages = [...outgoingMessages, ...incomingMessages];
|
||||
|
||||
for (const event of messages) {
|
||||
const receiverPubkey = event.tags.find((t) => t[0] === 'p')[1] || account.pubkey;
|
||||
await createChat(
|
||||
event.id,
|
||||
@@ -172,11 +180,11 @@ export function useNostr() {
|
||||
return event;
|
||||
};
|
||||
|
||||
const createZap = async (event: NostrEvent, amount: number, message?: string) => {
|
||||
// @ts-expect-error, LumeEvent to NostrEvent
|
||||
const createZap = async (event: NDKEvent, amount: number, message?: string) => {
|
||||
// @ts-expect-error, LumeEvent to NDKEvent
|
||||
event.id = event.event_id;
|
||||
|
||||
// @ts-expect-error, LumeEvent to NostrEvent
|
||||
// @ts-expect-error, LumeEvent to NDKEvent
|
||||
if (typeof event.content !== 'string') event.content = event.content.original;
|
||||
|
||||
if (typeof event.tags === 'string') event.tags = destr(event.tags);
|
||||
@@ -188,6 +196,7 @@ export function useNostr() {
|
||||
ndk.signer = signer;
|
||||
}
|
||||
|
||||
// @ts-expect-error, LumeEvent to NDKEvent
|
||||
const ndkEvent = new NDKEvent(ndk, event);
|
||||
const res = await ndkEvent.zap(amount, message ?? 'zap from lume');
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ export function useOpenGraph(url: string) {
|
||||
async () => {
|
||||
const res = await getLinkPreview(url);
|
||||
if (!res) {
|
||||
throw new Error("Can' fetch");
|
||||
throw new Error('fetch preview failed');
|
||||
}
|
||||
return res;
|
||||
},
|
||||
|
||||
@@ -5,25 +5,26 @@ import { createNote } from '@libs/storage';
|
||||
|
||||
import { nHoursAgo } from '@utils/date';
|
||||
import { useAccount } from '@utils/hooks/useAccount';
|
||||
import { nip02ToArray } from '@utils/transform';
|
||||
|
||||
import { useNostr } from './useNostr';
|
||||
import { useNostr } from '@utils/hooks/useNostr';
|
||||
|
||||
export function useSocial() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { publish } = useNostr();
|
||||
const { fetcher, relayUrls } = useNDK();
|
||||
const { ndk } = useNDK();
|
||||
const { account } = useAccount();
|
||||
const { status, data: userFollows } = useQuery(
|
||||
['userFollows', account.pubkey],
|
||||
async () => {
|
||||
const res = await fetcher.fetchLastEvent(relayUrls, {
|
||||
kinds: [3],
|
||||
authors: [account.pubkey],
|
||||
const keys = [];
|
||||
const user = ndk.getUser({ hexpubkey: account.pubkey });
|
||||
const follows = await user.follows();
|
||||
|
||||
follows.forEach((item) => {
|
||||
keys.push(item.hexpubkey);
|
||||
});
|
||||
const list = nip02ToArray(res.tags);
|
||||
return list;
|
||||
|
||||
return keys;
|
||||
},
|
||||
{
|
||||
enabled: account ? true : false,
|
||||
@@ -67,11 +68,11 @@ export function useSocial() {
|
||||
});
|
||||
|
||||
// fetch events
|
||||
const events = await fetcher.fetchAllEvents(
|
||||
relayUrls,
|
||||
{ authors: [pubkey], kinds: [1, 6] },
|
||||
{ since: nHoursAgo(48) }
|
||||
);
|
||||
const events = await ndk.fetchEvents({
|
||||
authors: [pubkey],
|
||||
kinds: [1, 6],
|
||||
since: nHoursAgo(24),
|
||||
});
|
||||
|
||||
for (const event of events) {
|
||||
await createNote(
|
||||
|
||||
Reference in New Issue
Block a user