chore: fixed circular import

This commit is contained in:
reya 2024-01-12 14:46:50 +07:00
parent a9d10ff93b
commit ad6ae6745d
16 changed files with 509 additions and 906 deletions

View File

@ -1,9 +1,19 @@
import react from "@vitejs/plugin-react-swc";
import million from "million/compiler";
import { defineConfig } from "vite";
import viteTsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
plugins: [react(), viteTsconfigPaths()],
plugins: [
million.vite({
auto: {
threshold: 0.05,
},
mute: true,
}),
react(),
viteTsconfigPaths(),
],
envPrefix: ["VITE_", "TAURI_"],
build: {
target: process.env.TAURI_PLATFORM === "windows" ? "chrome105" : "safari13",

View File

@ -15,5 +15,8 @@
"packageManager": "pnpm@8.9.0",
"engines": {
"node": ">=18"
},
"dependencies": {
"million": "^2.6.4"
}
}

View File

@ -12,6 +12,7 @@ import NDK, {
NDKUser,
NostrEvent,
} from "@nostr-dev-kit/ndk";
import { ndkAdapter } from "@nostr-fetch/adapter-ndk";
import { open } from "@tauri-apps/plugin-dialog";
import { readFile } from "@tauri-apps/plugin-fs";
import { fetch } from "@tauri-apps/plugin-http";
@ -20,22 +21,17 @@ import { nip19 } from "nostr-tools";
export class Ark {
#storage: LumeStorage;
#fetcher: NostrFetcher;
public ndk: NDK;
constructor({
ndk,
storage,
fetcher,
}: {
ndk: NDK;
storage: LumeStorage;
fetcher: NostrFetcher;
}) {
this.ndk = ndk;
this.#storage = storage;
this.#fetcher = fetcher;
}
public async connectDepot() {
@ -303,17 +299,15 @@ export class Ark {
};
}
public async getThreads({
id,
data,
}: { id: string; data?: NDKEventWithReplies[] }) {
let events = data || null;
public async getThreads({ id }: { id: string }) {
const fetcher = NostrFetcher.withCustomPool(ndkAdapter(this.ndk));
if (!data) {
try {
const relayUrls = [...this.ndk.pool.relays.values()].map(
(item) => item.url,
);
const rawEvents = (await this.#fetcher.fetchAllEvents(
const rawEvents = (await fetcher.fetchAllEvents(
relayUrls,
{
kinds: [NDKKind.Text],
@ -322,64 +316,76 @@ export class Ark {
{ since: 0 },
{ sort: true },
)) as unknown as NostrEvent[];
events = rawEvents.map(
const events = rawEvents.map(
(event) => new NDKEvent(this.ndk, event),
) as NDKEvent[] as NDKEventWithReplies[];
}
if (events.length > 0) {
const replies = new Set();
for (const event of events) {
const tags = event.tags.filter((el) => el[0] === "e" && el[1] !== id);
if (tags.length > 0) {
for (const tag of tags) {
const rootIndex = events.findIndex((el) => el.id === tag[1]);
if (rootIndex !== -1) {
const rootEvent = events[rootIndex];
if (rootEvent?.replies) {
rootEvent.replies.push(event);
} else {
rootEvent.replies = [event];
if (events.length > 0) {
const replies = new Set();
for (const event of events) {
const tags = event.tags.filter((el) => el[0] === "e" && el[1] !== id);
if (tags.length > 0) {
for (const tag of tags) {
const rootIndex = events.findIndex((el) => el.id === tag[1]);
if (rootIndex !== -1) {
const rootEvent = events[rootIndex];
if (rootEvent?.replies) {
rootEvent.replies.push(event);
} else {
rootEvent.replies = [event];
}
replies.add(event.id);
}
replies.add(event.id);
}
}
}
const cleanEvents = events.filter((ev) => !replies.has(ev.id));
return cleanEvents;
}
const cleanEvents = events.filter((ev) => !replies.has(ev.id));
return cleanEvents;
}
return events;
return events;
} catch (e) {
console.log(e);
} finally {
fetcher.shutdown();
}
}
public async getAllRelaysFromContacts() {
const LIMIT = 1;
const connectedRelays = this.ndk.pool
.connectedRelays()
.map((item) => item.url);
const relayMap = new Map<string, string[]>();
const relayEvents = this.#fetcher.fetchLatestEventsPerAuthor(
{
authors: this.#storage.account.contacts,
relayUrls: connectedRelays,
},
{ kinds: [NDKKind.RelayList] },
LIMIT,
);
const fetcher = NostrFetcher.withCustomPool(ndkAdapter(this.ndk));
try {
const LIMIT = 1;
const connectedRelays = this.ndk.pool
.connectedRelays()
.map((item) => item.url);
const relayMap = new Map<string, string[]>();
const relayEvents = fetcher.fetchLatestEventsPerAuthor(
{
authors: this.#storage.account.contacts,
relayUrls: connectedRelays,
},
{ kinds: [NDKKind.RelayList] },
LIMIT,
);
for await (const { author, events } of relayEvents) {
if (events[0]) {
for (const tag of events[0].tags) {
const users = relayMap.get(tag[1]);
for await (const { author, events } of relayEvents) {
if (events[0]) {
for (const tag of events[0].tags) {
const users = relayMap.get(tag[1]);
if (!users) relayMap.set(tag[1], [author]);
users.push(author);
if (!users) relayMap.set(tag[1], [author]);
users.push(author);
}
}
}
}
return relayMap;
return relayMap;
} catch (e) {
console.log(e);
} finally {
fetcher.shutdown();
}
}
public async getInfiniteEvents({
@ -395,51 +401,52 @@ export class Ark {
signal?: AbortSignal;
dedup?: boolean;
}) {
const seenIds = new Set<string>();
const dedupQueue = new Set<string>();
const fetcher = NostrFetcher.withCustomPool(ndkAdapter(this.ndk));
const relayUrls = [...this.ndk.pool.relays.values()].map(
(item) => item.url,
);
const seenIds = new Set<string>();
const dedupQueue = new Set<string>();
const events = await this.#fetcher.fetchLatestEvents(
relayUrls,
filter,
limit,
{
try {
const events = await fetcher.fetchLatestEvents(relayUrls, filter, limit, {
asOf: pageParam === 0 ? undefined : pageParam,
abortSignal: signal,
},
);
});
const ndkEvents = events.map((event) => {
return new NDKEvent(this.ndk, event);
});
const ndkEvents = events.map((event) => {
return new NDKEvent(this.ndk, event);
});
if (dedup) {
for (const event of ndkEvents) {
const tags = event.tags
.filter((el) => el[0] === "e")
?.map((item) => item[1]);
if (dedup) {
for (const event of ndkEvents) {
const tags = event.tags
.filter((el) => el[0] === "e")
?.map((item) => item[1]);
if (tags.length) {
for (const tag of tags) {
if (seenIds.has(tag)) {
dedupQueue.add(event.id);
break;
if (tags.length) {
for (const tag of tags) {
if (seenIds.has(tag)) {
dedupQueue.add(event.id);
break;
}
seenIds.add(tag);
}
seenIds.add(tag);
}
}
return ndkEvents
.filter((event) => !dedupQueue.has(event.id))
.sort((a, b) => b.created_at - a.created_at);
}
return ndkEvents
.filter((event) => !dedupQueue.has(event.id))
.sort((a, b) => b.created_at - a.created_at);
return ndkEvents.sort((a, b) => b.created_at - a.created_at);
} catch (e) {
console.log(e);
} finally {
fetcher.shutdown();
}
return ndkEvents.sort((a, b) => b.created_at - a.created_at);
}
public async getRelayEvents({
@ -456,21 +463,29 @@ export class Ark {
signal?: AbortSignal;
dedup?: boolean;
}) {
const events = await this.#fetcher.fetchLatestEvents(
[normalizeRelayUrl(relayUrl)],
filter,
limit,
{
asOf: pageParam === 0 ? undefined : pageParam,
abortSignal: signal,
},
);
const fetcher = NostrFetcher.withCustomPool(ndkAdapter(this.ndk));
const ndkEvents = events.map((event) => {
return new NDKEvent(this.ndk, event);
});
try {
const events = await fetcher.fetchLatestEvents(
[normalizeRelayUrl(relayUrl)],
filter,
limit,
{
asOf: pageParam === 0 ? undefined : pageParam,
abortSignal: signal,
},
);
return ndkEvents.sort((a, b) => b.created_at - a.created_at);
const ndkEvents = events.map((event) => {
return new NDKEvent(this.ndk, event);
});
return ndkEvents.sort((a, b) => b.created_at - a.created_at);
} catch (e) {
console.log(e);
} finally {
fetcher.shutdown();
}
}
/**

View File

@ -11,5 +11,3 @@ export const Column = {
Content: ColumnContent,
Route: Route,
};
export * from "./provider";

View File

@ -1,8 +1,8 @@
import { PinIcon } from "@lume/icons";
import { COL_TYPES } from "@lume/utils";
import * as Tooltip from "@radix-ui/react-tooltip";
import { useNoteContext } from "..";
import { useColumnContext } from "../../column";
import { useColumnContext } from "../../column/provider";
import { useNoteContext } from "../provider";
export function NotePin() {
const event = useNoteContext();

View File

@ -15,17 +15,15 @@ import { nip19 } from "nostr-tools";
import { ReactNode, useMemo, useState } from "react";
import { Link } from "react-router-dom";
import reactStringReplace from "react-string-replace";
import {
Hashtag,
ImagePreview,
LinkPreview,
MentionNote,
MentionUser,
VideoPreview,
useNoteContext,
useStorage,
} from "../..";
import { useStorage } from "../../provider";
import { Hashtag } from "./mentions/hashtag";
import { MentionNote } from "./mentions/note";
import { MentionUser } from "./mentions/user";
import { NIP89 } from "./nip89";
import { ImagePreview } from "./preview/image";
import { LinkPreview } from "./preview/link";
import { VideoPreview } from "./preview/video";
import { useNoteContext } from "./provider";
export function NoteContent({
className,
@ -229,7 +227,7 @@ export function NoteContent({
return parsedContent;
} catch (e) {
console.warn("[parser] parse failed: ", e);
console.warn(event.id, `[parser] parse failed: ${e}`);
return parsedContent;
}
}, [content]);
@ -251,7 +249,7 @@ export function NoteContent({
setContent(data.translatedText);
setTranslated(true);
} catch (e) {
console.error(String(e));
console.error(event.id, String(e));
}
};
@ -260,7 +258,7 @@ export function NoteContent({
}
return (
<div className={cn("", className)}>
<div className={cn(className)}>
<div className="break-p select-text whitespace-pre-line text-balance leading-normal">
{richContent}
</div>

View File

@ -25,17 +25,3 @@ export const Note = {
Child: NoteChild,
Thread: NoteThread,
};
export * from "./provider";
export * from "./primitives/text";
export * from "./primitives/repost";
export * from "./primitives/skeleton";
export * from "./primitives/thread";
export * from "./primitives/reply";
export * from "./preview/image";
export * from "./preview/link";
export * from "./preview/video";
export * from "./mentions/note";
export * from "./mentions/user";
export * from "./mentions/hashtag";
export * from "./mentions/invoice";

View File

@ -1,5 +1,5 @@
import { COL_TYPES } from "@lume/utils";
import { useColumnContext } from "../../column";
import { useColumnContext } from "../../column/provider";
export function Hashtag({ tag }: { tag: string }) {
const { addColumn } = useColumnContext();

View File

@ -3,7 +3,7 @@ import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import { memo } from "react";
import { Link } from "react-router-dom";
import { useProfile } from "../../../hooks/useProfile";
import { useColumnContext } from "../../column";
import { useColumnContext } from "../../column/provider";
export const MentionUser = memo(function MentionUser({
pubkey,

View File

@ -1,8 +1,8 @@
import { HorizontalDotsIcon, ShareIcon } from "@lume/icons";
import { HorizontalDotsIcon } from "@lume/icons";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
import { nip19 } from "nostr-tools";
import { EventPointer } from "nostr-tools/lib/types/nip19";
import { type EventPointer } from "nostr-tools/lib/types/nip19";
import { useState } from "react";
import { Link } from "react-router-dom";
import { useNoteContext } from "./provider";

View File

@ -2,9 +2,8 @@ import { PinIcon } from "@lume/icons";
import { COL_TYPES } from "@lume/utils";
import { Link } from "react-router-dom";
import { twMerge } from "tailwind-merge";
import { Note, useNoteContext } from ".";
import { useArk } from "../..";
import { useColumnContext } from "../column";
import { Note } from ".";
import { useArk, useColumnContext, useNoteContext } from "../..";
export function NoteThread({
className,

View File

@ -67,7 +67,7 @@ export function NoteUser({
decoding="async"
className="h-6 w-6 shrink-0 object-cover rounded-md"
/>
<Avatar.Fallback delayMs={300}>
<Avatar.Fallback delayMs={150}>
<img
src={fallbackAvatar}
alt={event.pubkey}
@ -120,7 +120,7 @@ export function NoteUser({
decoding="async"
className="h-6 w-6 rounded object-cover"
/>
<Avatar.Fallback delayMs={300}>
<Avatar.Fallback delayMs={150}>
<img
src={fallbackAvatar}
alt={event.pubkey}
@ -165,7 +165,7 @@ export function NoteUser({
decoding="async"
className="h-10 w-10 rounded-lg object-cover ring-1 ring-neutral-200/50 dark:ring-neutral-800/50"
/>
<Avatar.Fallback delayMs={300}>
<Avatar.Fallback delayMs={150}>
<img
src={fallbackAvatar}
alt={event.pubkey}
@ -216,7 +216,7 @@ export function NoteUser({
decoding="async"
className="h-9 w-9 rounded-lg bg-white object-cover ring-1 ring-neutral-200/50 dark:ring-neutral-800/50"
/>
<Avatar.Fallback delayMs={300}>
<Avatar.Fallback delayMs={150}>
<img
src={fallbackAvatar}
alt={event.pubkey}

View File

@ -1,246 +0,0 @@
import getUrls from "get-urls";
import { nanoid } from "nanoid";
import { nip19 } from "nostr-tools";
import { ReactNode } from "react";
import { Link } from "react-router-dom";
import reactStringReplace from "react-string-replace";
import {
Hashtag,
ImagePreview,
LinkPreview,
MentionNote,
MentionUser,
VideoPreview,
} from "../components/note";
import { useStorage } from "../provider";
const NOSTR_MENTIONS = [
"@npub1",
"nostr:npub1",
"nostr:nprofile1",
"nostr:naddr1",
"npub1",
"nprofile1",
"naddr1",
"Nostr:npub1",
"Nostr:nprofile1",
"Nostr:naddre1",
];
const NOSTR_EVENTS = [
"@nevent1",
"@note1",
"@nostr:note1",
"@nostr:nevent1",
"nostr:note1",
"note1",
"nostr:nevent1",
"nevent1",
"Nostr:note1",
"Nostr:nevent1",
];
// const BITCOINS = ['lnbc', 'bc1p', 'bc1q'];
const IMAGES = ["jpg", "jpeg", "gif", "png", "webp", "avif", "tiff"];
const VIDEOS = [
"mp4",
"mov",
"webm",
"wmv",
"flv",
"mts",
"avi",
"ogv",
"mkv",
"m3u8",
];
const AUDIOS = ["mp3", "ogg", "wav"];
export function useRichContent(content: string) {
const storage = useStorage();
let parsedContent: string | ReactNode[] = content.replace(/\n+/g, "\n");
let linkPreview: string;
let images: string[] = [];
let videos: string[] = [];
let audios: string[] = [];
let events: string[] = [];
const text = parsedContent;
const words = text.split(/( |\n)/);
const urls = [...getUrls(text)];
if (storage.settings.media && !storage.settings.lowPower) {
images = urls.filter((word) =>
IMAGES.some((el) => {
const url = new URL(word);
const extension = url.pathname.split(".")[1];
if (extension === el) return true;
return false;
}),
);
videos = urls.filter((word) =>
VIDEOS.some((el) => {
const url = new URL(word);
const extension = url.pathname.split(".")[1];
if (extension === el) return true;
return false;
}),
);
audios = urls.filter((word) =>
AUDIOS.some((el) => {
const url = new URL(word);
const extension = url.pathname.split(".")[1];
if (extension === el) return true;
return false;
}),
);
}
events = words.filter((word) =>
NOSTR_EVENTS.some((el) => word.startsWith(el)),
);
const hashtags = words.filter((word) => word.startsWith("#"));
const mentions = words.filter((word) =>
NOSTR_MENTIONS.some((el) => word.startsWith(el)),
);
try {
if (images.length) {
for (const image of images) {
parsedContent = reactStringReplace(parsedContent, image, (match, i) => (
<ImagePreview key={match + i} url={match} />
));
}
}
if (videos.length) {
for (const video of videos) {
parsedContent = reactStringReplace(parsedContent, video, (match, i) => (
<VideoPreview key={match + i} url={match} />
));
}
}
if (audios.length) {
for (const audio of audios) {
parsedContent = reactStringReplace(parsedContent, audio, (match, i) => (
<VideoPreview key={match + i} url={match} />
));
}
}
if (hashtags.length) {
for (const hashtag of hashtags) {
parsedContent = reactStringReplace(
parsedContent,
hashtag,
(match, i) => {
if (storage.settings.hashtag)
return <Hashtag key={match + i} tag={hashtag} />;
return null;
},
);
}
}
if (events.length) {
for (const event of events) {
const address = event
.replace("nostr:", "")
.replace(/[^a-zA-Z0-9]/g, "");
const decoded = nip19.decode(address);
if (decoded.type === "note") {
parsedContent = reactStringReplace(
parsedContent,
event,
(match, i) => (
<MentionNote key={match + i} eventId={decoded.data} />
),
);
}
if (decoded.type === "nevent") {
parsedContent = reactStringReplace(
parsedContent,
event,
(match, i) => (
<MentionNote key={match + i} eventId={decoded.data.id} />
),
);
}
}
}
if (mentions.length) {
for (const mention of mentions) {
const address = mention
.replace("nostr:", "")
.replace("@", "")
.replace(/[^a-zA-Z0-9]/g, "");
const decoded = nip19.decode(address);
if (decoded.type === "npub") {
parsedContent = reactStringReplace(
parsedContent,
mention,
(match, i) => <MentionUser key={match + i} pubkey={decoded.data} />,
);
}
if (decoded.type === "nprofile" || decoded.type === "naddr") {
parsedContent = reactStringReplace(
parsedContent,
mention,
(match, i) => (
<MentionUser key={match + i} pubkey={decoded.data.pubkey} />
),
);
}
}
}
parsedContent = reactStringReplace(
parsedContent,
/(https?:\/\/\S+)/g,
(match, i) => {
const url = new URL(match);
if (!linkPreview) {
linkPreview = match;
return <LinkPreview key={match + i} url={url.toString()} />;
}
return (
<Link
key={match + i}
to={url.toString()}
target="_blank"
rel="noreferrer"
className="break-all font-normal text-blue-500 hover:text-blue-600"
>
{url.toString()}
</Link>
);
},
);
parsedContent = reactStringReplace(parsedContent, "\n", () => {
return <div key={nanoid()} className="h-3" />;
});
if (typeof parsedContent[0] === "string") {
parsedContent[0] = parsedContent[0].trimStart();
}
return parsedContent;
} catch (e) {
console.warn("[parser] parse failed: ", e);
return parsedContent;
}
}

View File

@ -1,8 +1,21 @@
export * from "./ark";
export * from "./provider";
export * from "./components/column";
export * from "./components/note";
export * from "./hooks/useRichContent";
export * from "./hooks/useEvent";
export * from "./hooks/useProfile";
export * from "./hooks/useRelay";
export * from "./components/column/provider";
export * from "./components/column";
export * from "./components/note";
export * from "./components/note/provider";
export * from "./components/note/primitives/text";
export * from "./components/note/primitives/repost";
export * from "./components/note/primitives/skeleton";
export * from "./components/note/primitives/thread";
export * from "./components/note/primitives/reply";
export * from "./components/note/preview/image";
export * from "./components/note/preview/link";
export * from "./components/note/preview/video";
export * from "./components/note/mentions/note";
export * from "./components/note/mentions/user";
export * from "./components/note/mentions/hashtag";
export * from "./components/note/mentions/invoice";

View File

@ -10,7 +10,6 @@ import NDK, {
NDKRelay,
NDKRelayAuthPolicies,
} from "@nostr-dev-kit/ndk";
import { ndkAdapter } from "@nostr-fetch/adapter-ndk";
import { useQueryClient } from "@tanstack/react-query";
import { fetch } from "@tauri-apps/plugin-http";
import { locale, platform } from "@tauri-apps/plugin-os";
@ -18,11 +17,7 @@ import { relaunch } from "@tauri-apps/plugin-process";
import Database from "@tauri-apps/plugin-sql";
import { check } from "@tauri-apps/plugin-updater";
import Markdown from "markdown-to-jsx";
import {
NostrFetcher,
normalizeRelayUrl,
normalizeRelayUrlSet,
} from "nostr-fetch";
import { normalizeRelayUrl, normalizeRelayUrlSet } from "nostr-fetch";
import { PropsWithChildren, useEffect, useState } from "react";
import { createContext, useContextSelector } from "use-context-selector";
import { Ark } from "./ark";
@ -38,8 +33,6 @@ const LumeContext = createContext<Context>({
});
const LumeProvider = ({ children }: PropsWithChildren<object>) => {
const queryClient = useQueryClient();
const [context, setContext] = useState<Context>(undefined);
const [isNewVersion, setIsNewVersion] = useState(false);
@ -219,11 +212,8 @@ const LumeProvider = ({ children }: PropsWithChildren<object>) => {
});
}
// init nostr fetcher
const fetcher = NostrFetcher.withCustomPool(ndkAdapter(ndk));
// ark utils
const ark = new Ark({ storage, ndk, fetcher });
const ark = new Ark({ storage, ndk });
// update context
setContext({ ark, storage });

File diff suppressed because it is too large Load Diff