This commit is contained in:
Ren Amamiya
2023-08-12 11:18:10 +07:00
parent 36b2acba6a
commit bb089bb259
27 changed files with 502 additions and 481 deletions

View File

@@ -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) => {

View File

@@ -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>

View File

@@ -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>
);
}

View File

@@ -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();

View File

@@ -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>

View File

@@ -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';

View File

@@ -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" />

View File

@@ -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>

View File

@@ -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({

View File

@@ -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({

View File

@@ -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,
};
};

View File

@@ -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}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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');

View File

@@ -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>

View File

@@ -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,
})
}

View File

@@ -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') {

View File

@@ -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[];
},

View File

@@ -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>

View File

@@ -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>

View File

@@ -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');

View File

@@ -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;
},

View File

@@ -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(