feat: update rust nostr
This commit is contained in:
@@ -6,11 +6,7 @@
|
||||
"dependencies": {
|
||||
"@getalby/sdk": "^3.2.3",
|
||||
"@lume/icons": "workspace:^",
|
||||
"@lume/ndk-cache-tauri": "workspace:^",
|
||||
"@lume/storage": "workspace:^",
|
||||
"@lume/utils": "workspace:^",
|
||||
"@nostr-dev-kit/ndk": "^2.4.0",
|
||||
"@nostr-fetch/adapter-ndk": "^0.15.0",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@radix-ui/react-collapsible": "^1.0.3",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
@@ -21,13 +17,9 @@
|
||||
"@tanstack/react-query": "^5.18.1",
|
||||
"get-urls": "^12.1.0",
|
||||
"jotai": "^2.6.4",
|
||||
"linkify-react": "^4.1.3",
|
||||
"linkifyjs": "^4.1.3",
|
||||
"media-chrome": "^2.1.0",
|
||||
"minidenticons": "^4.2.0",
|
||||
"nanoid": "^5.0.5",
|
||||
"nostr-fetch": "^0.15.0",
|
||||
"nostr-tools": "1.17.0",
|
||||
"qrcode.react": "^3.1.0",
|
||||
"re-resizable": "^6.9.11",
|
||||
"react": "^18.2.0",
|
||||
@@ -37,8 +29,6 @@
|
||||
"react-string-replace": "^1.1.1",
|
||||
"sonner": "^1.4.0",
|
||||
"string-strip-html": "^13.4.6",
|
||||
"tippy.js": "^6.3.7",
|
||||
"use-context-selector": "^1.4.1",
|
||||
"virtua": "^0.23.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -46,7 +36,6 @@
|
||||
"@lume/tsconfig": "workspace:^",
|
||||
"@lume/types": "workspace:^",
|
||||
"@types/react": "^18.2.52",
|
||||
"tailwind-merge": "^2.2.1",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
|
||||
@@ -1,301 +1,56 @@
|
||||
import { Account, type NDKEventWithReplies, type NIP05 } from "@lume/types";
|
||||
import NDK, {
|
||||
NDKEvent,
|
||||
NDKFilter,
|
||||
NDKKind,
|
||||
NDKNip46Signer,
|
||||
NDKPrivateKeySigner,
|
||||
NDKRelay,
|
||||
NDKSubscriptionCacheUsage,
|
||||
NDKTag,
|
||||
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";
|
||||
import { NostrFetcher, normalizeRelayUrl } from "nostr-fetch";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { type CurrentAccount, Event, Metadata } from "@lume/types";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
export class Ark {
|
||||
public ndk: NDK;
|
||||
public account: Account;
|
||||
public account: CurrentAccount;
|
||||
|
||||
constructor({
|
||||
ndk,
|
||||
account,
|
||||
}: {
|
||||
ndk: NDK;
|
||||
account: Account;
|
||||
}) {
|
||||
this.ndk = ndk;
|
||||
constructor(account: CurrentAccount) {
|
||||
this.account = account;
|
||||
}
|
||||
|
||||
public async connectDepot() {
|
||||
return this.ndk.addExplicitRelay(
|
||||
new NDKRelay(normalizeRelayUrl("ws://localhost:6090")),
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
public updateNostrSigner({
|
||||
signer,
|
||||
}: { signer: NDKNip46Signer | NDKPrivateKeySigner }) {
|
||||
this.ndk.signer = signer;
|
||||
return this.ndk.signer;
|
||||
}
|
||||
|
||||
public subscribe({
|
||||
filter,
|
||||
closeOnEose = false,
|
||||
cb,
|
||||
}: {
|
||||
filter: NDKFilter;
|
||||
closeOnEose: boolean;
|
||||
cb: (event: NDKEvent) => void;
|
||||
}) {
|
||||
const sub = this.ndk.subscribe(filter, { closeOnEose });
|
||||
sub.addListener("event", (event: NDKEvent) => cb(event));
|
||||
return sub;
|
||||
}
|
||||
|
||||
public getNDKEvent(event: NostrEvent) {
|
||||
return new NDKEvent(this.ndk, event);
|
||||
}
|
||||
|
||||
public async createEvent({
|
||||
kind,
|
||||
tags,
|
||||
content,
|
||||
rootReplyTo = undefined,
|
||||
replyTo = undefined,
|
||||
}: {
|
||||
kind: NDKKind | number;
|
||||
tags: NDKTag[];
|
||||
content?: string;
|
||||
rootReplyTo?: string;
|
||||
replyTo?: string;
|
||||
}) {
|
||||
public async event_to_bech32(id: string, relays: string[]) {
|
||||
try {
|
||||
const event = new NDKEvent(this.ndk);
|
||||
if (content) event.content = content;
|
||||
event.kind = kind;
|
||||
event.tags = tags;
|
||||
|
||||
if (rootReplyTo) {
|
||||
const rootEvent = await this.ndk.fetchEvent(rootReplyTo);
|
||||
if (rootEvent) event.tag(rootEvent, "root");
|
||||
}
|
||||
|
||||
if (replyTo) {
|
||||
const replyEvent = await this.ndk.fetchEvent(replyTo);
|
||||
if (replyEvent) event.tag(replyEvent, "reply");
|
||||
}
|
||||
|
||||
const publish = await event.publish();
|
||||
|
||||
if (!publish) throw new Error("Failed to publish event");
|
||||
return {
|
||||
id: event.id,
|
||||
seens: [...publish.values()].map((item) => item.url),
|
||||
};
|
||||
} catch (e) {
|
||||
throw new Error(e);
|
||||
}
|
||||
}
|
||||
|
||||
public getCleanPubkey(pubkey: string) {
|
||||
try {
|
||||
let hexstring = pubkey
|
||||
.replace("nostr:", "")
|
||||
.split("'")[0]
|
||||
.split(".")[0]
|
||||
.split(",")[0]
|
||||
.split("?")[0];
|
||||
|
||||
if (
|
||||
hexstring.startsWith("npub1") ||
|
||||
hexstring.startsWith("nprofile1") ||
|
||||
hexstring.startsWith("naddr1")
|
||||
) {
|
||||
const decoded = nip19.decode(hexstring);
|
||||
|
||||
if (decoded.type === "nprofile") hexstring = decoded.data.pubkey;
|
||||
if (decoded.type === "npub") hexstring = decoded.data;
|
||||
if (decoded.type === "naddr") hexstring = decoded.data.pubkey;
|
||||
}
|
||||
|
||||
return hexstring;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
public async getUserProfile(pubkey?: string) {
|
||||
try {
|
||||
const currentUserPubkey = this.account.pubkey;
|
||||
const hexstring = pubkey
|
||||
? this.getCleanPubkey(pubkey)
|
||||
: currentUserPubkey;
|
||||
|
||||
const user = this.ndk.getUser({
|
||||
pubkey: hexstring,
|
||||
const cmd: string = await invoke("event_to_bech32", {
|
||||
id,
|
||||
relays,
|
||||
});
|
||||
|
||||
const profile = await user.fetchProfile({
|
||||
cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST,
|
||||
});
|
||||
|
||||
return profile;
|
||||
return cmd;
|
||||
} catch {
|
||||
throw new Error("user not found");
|
||||
console.error("get nevent id failed");
|
||||
}
|
||||
}
|
||||
|
||||
public async getUserContacts(pubkey?: string) {
|
||||
public async get_event(id: string) {
|
||||
try {
|
||||
const currentUserPubkey = this.account.pubkey;
|
||||
const hexstring = pubkey
|
||||
? this.getCleanPubkey(pubkey)
|
||||
: currentUserPubkey;
|
||||
|
||||
const user = this.ndk.getUser({
|
||||
pubkey: hexstring,
|
||||
});
|
||||
|
||||
const contacts = [...(await user.follows(undefined, false))].map(
|
||||
(user) => user.pubkey,
|
||||
);
|
||||
|
||||
if (!pubkey || pubkey === this.account.pubkey) {
|
||||
this.account.contacts = contacts;
|
||||
}
|
||||
|
||||
return contacts;
|
||||
const cmd: string = await invoke("get_event", { id });
|
||||
const event = JSON.parse(cmd) as Event;
|
||||
return event;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
console.error("failed to get event", id);
|
||||
}
|
||||
}
|
||||
|
||||
public async getUserRelays({ pubkey }: { pubkey?: string }) {
|
||||
try {
|
||||
const user = this.ndk.getUser({
|
||||
pubkey: pubkey ? pubkey : this.account.pubkey,
|
||||
});
|
||||
return await user.relayList();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
public async newContactList({ tags }: { tags: NDKTag[] }) {
|
||||
const publish = await this.createEvent({
|
||||
kind: NDKKind.Contacts,
|
||||
tags: tags,
|
||||
});
|
||||
|
||||
if (publish) {
|
||||
this.account.contacts = tags.map((item) => item[1]);
|
||||
return publish;
|
||||
}
|
||||
}
|
||||
|
||||
public async createContact(pubkey: string) {
|
||||
const user = this.ndk.getUser({ pubkey: this.account.pubkey });
|
||||
const contacts = await user.follows();
|
||||
return await user.follow(new NDKUser({ pubkey: pubkey }), contacts);
|
||||
}
|
||||
|
||||
public async deleteContact(pubkey: string) {
|
||||
const user = this.ndk.getUser({ pubkey: this.account.pubkey });
|
||||
const contacts = await user.follows();
|
||||
contacts.delete(new NDKUser({ pubkey: pubkey }));
|
||||
|
||||
const event = new NDKEvent(this.ndk);
|
||||
event.content = "";
|
||||
event.kind = NDKKind.Contacts;
|
||||
event.tags = [...contacts].map((item) => [
|
||||
"p",
|
||||
item.pubkey,
|
||||
item.relayUrls?.[0] || "",
|
||||
"",
|
||||
]);
|
||||
|
||||
return await event.publish();
|
||||
}
|
||||
|
||||
public async getAllEvents({ filter }: { filter: NDKFilter }) {
|
||||
const events = await this.ndk.fetchEvents(filter);
|
||||
if (!events) return [];
|
||||
return [...events];
|
||||
}
|
||||
|
||||
public getCleanEventId(id: string) {
|
||||
let eventId: string = id.replace("nostr:", "").split("'")[0].split(".")[0];
|
||||
|
||||
if (
|
||||
eventId.startsWith("nevent1") ||
|
||||
eventId.startsWith("note1") ||
|
||||
eventId.startsWith("naddr1")
|
||||
) {
|
||||
const decode = nip19.decode(eventId);
|
||||
if (decode.type === "nevent") eventId = decode.data.id;
|
||||
if (decode.type === "note") eventId = decode.data;
|
||||
}
|
||||
|
||||
return eventId;
|
||||
}
|
||||
|
||||
public async getEventById(id: string) {
|
||||
try {
|
||||
const eventId = this.getCleanEventId(id);
|
||||
return await this.ndk.fetchEvent(eventId);
|
||||
} catch {
|
||||
throw new Error("event not found");
|
||||
}
|
||||
}
|
||||
|
||||
public async getEventByFilter({
|
||||
filter,
|
||||
cache,
|
||||
}: { filter: NDKFilter; cache?: NDKSubscriptionCacheUsage }) {
|
||||
const event = await this.ndk.fetchEvent(filter, {
|
||||
cacheUsage: cache || NDKSubscriptionCacheUsage.CACHE_FIRST,
|
||||
});
|
||||
|
||||
if (!event) return null;
|
||||
return event;
|
||||
}
|
||||
|
||||
public async getEvents(filter: NDKFilter) {
|
||||
const events = await this.ndk.fetchEvents(filter);
|
||||
if (!events) return [];
|
||||
return [...events];
|
||||
}
|
||||
|
||||
public getEventThread({
|
||||
public parse_event_thread({
|
||||
content,
|
||||
tags,
|
||||
}: { content: string; tags: NDKTag[] }) {
|
||||
}: { content: string; tags: string[][] }) {
|
||||
let rootEventId: string = null;
|
||||
let replyEventId: string = null;
|
||||
|
||||
// Ignore quote repost
|
||||
if (content.includes("nostr:note1") || content.includes("nostr:nevent1"))
|
||||
return null;
|
||||
|
||||
// Get all event references from tags, ignore mention
|
||||
const events = tags.filter((el) => el[0] === "e" && el[3] !== "mention");
|
||||
|
||||
if (!events.length) return null;
|
||||
|
||||
if (events.length === 1)
|
||||
if (events.length === 1) {
|
||||
return {
|
||||
rootEventId: events[0][1],
|
||||
replyEventId: null,
|
||||
};
|
||||
|
||||
}
|
||||
if (events.length > 1) {
|
||||
rootEventId = events.find((el) => el[3] === "root")?.[1];
|
||||
replyEventId = events.find((el) => el[3] === "reply")?.[1];
|
||||
@@ -312,310 +67,24 @@ export class Ark {
|
||||
};
|
||||
}
|
||||
|
||||
public async getThreads(id: string) {
|
||||
const eventId = this.getCleanEventId(id);
|
||||
const fetcher = NostrFetcher.withCustomPool(ndkAdapter(this.ndk));
|
||||
const relayUrls = Array.from(this.ndk.pool.relays.keys());
|
||||
|
||||
public async get_metadata(id: string) {
|
||||
try {
|
||||
const rawEvents = (await fetcher.fetchAllEvents(
|
||||
relayUrls,
|
||||
{
|
||||
kinds: [NDKKind.Text],
|
||||
"#e": [eventId],
|
||||
},
|
||||
{ since: 0 },
|
||||
{ sort: true },
|
||||
)) as unknown as NostrEvent[];
|
||||
|
||||
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 && el[3] !== "mention",
|
||||
);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const cleanEvents = events.filter((ev) => !replies.has(ev.id));
|
||||
return cleanEvents;
|
||||
}
|
||||
|
||||
return events;
|
||||
const cmd: Metadata = await invoke("get_metadata", { id });
|
||||
return cmd;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
fetcher.shutdown();
|
||||
console.error("failed to get metadata", id);
|
||||
}
|
||||
}
|
||||
|
||||
public async getAllRelaysFromContacts({ signal }: { signal: AbortSignal }) {
|
||||
const fetcher = NostrFetcher.withCustomPool(ndkAdapter(this.ndk));
|
||||
const connectedRelays = Array.from(this.ndk.pool.relays.keys());
|
||||
|
||||
public async user_to_bech32(key: string, relays: string[]) {
|
||||
try {
|
||||
const relayMap = new Map<string, string[]>();
|
||||
const relayEvents = fetcher.fetchLatestEventsPerAuthor(
|
||||
{
|
||||
authors: this.account.contacts,
|
||||
relayUrls: connectedRelays,
|
||||
},
|
||||
{ kinds: [NDKKind.RelayList] },
|
||||
1,
|
||||
{ abortSignal: signal },
|
||||
);
|
||||
|
||||
for await (const { author, events } of relayEvents) {
|
||||
if (events.length) {
|
||||
const relayTags = events[0].tags.filter((item) => item[0] === "r");
|
||||
for (const tag of relayTags) {
|
||||
const item = relayMap.get(tag[1]);
|
||||
if (item?.length) {
|
||||
item.push(author);
|
||||
} else {
|
||||
relayMap.set(tag[1], [author]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return relayMap;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
fetcher.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
public async getInfiniteEvents({
|
||||
filter,
|
||||
limit,
|
||||
pageParam = 0,
|
||||
signal = undefined,
|
||||
dedup = true,
|
||||
}: {
|
||||
filter: NDKFilter;
|
||||
limit: number;
|
||||
pageParam?: number;
|
||||
signal?: AbortSignal;
|
||||
dedup?: boolean;
|
||||
}) {
|
||||
const fetcher = NostrFetcher.withCustomPool(ndkAdapter(this.ndk));
|
||||
const relayUrls = Array.from(this.ndk.pool.relays.keys());
|
||||
const seenIds = new Set<string>();
|
||||
const dedupQueue = new Set<string>();
|
||||
|
||||
try {
|
||||
const events = await fetcher.fetchLatestEvents(relayUrls, filter, limit, {
|
||||
asOf: pageParam === 0 ? undefined : pageParam,
|
||||
abortSignal: signal,
|
||||
const cmd: string = await invoke("user_to_bech32", {
|
||||
key,
|
||||
relays,
|
||||
});
|
||||
|
||||
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 (tags.length) {
|
||||
for (const tag of tags) {
|
||||
if (seenIds.has(tag)) {
|
||||
dedupQueue.add(event.id);
|
||||
break;
|
||||
}
|
||||
|
||||
seenIds.add(tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 cmd;
|
||||
} catch {
|
||||
console.error("get nprofile id failed");
|
||||
}
|
||||
}
|
||||
|
||||
public async getRelayEvents({
|
||||
relayUrl,
|
||||
filter,
|
||||
limit,
|
||||
pageParam = 0,
|
||||
signal = undefined,
|
||||
}: {
|
||||
relayUrl: string;
|
||||
filter: NDKFilter;
|
||||
limit: number;
|
||||
pageParam?: number;
|
||||
signal?: AbortSignal;
|
||||
dedup?: boolean;
|
||||
}) {
|
||||
const fetcher = NostrFetcher.withCustomPool(ndkAdapter(this.ndk));
|
||||
|
||||
try {
|
||||
const events = await fetcher.fetchLatestEvents(
|
||||
[normalizeRelayUrl(relayUrl)],
|
||||
filter,
|
||||
limit,
|
||||
{
|
||||
asOf: pageParam === 0 ? undefined : pageParam,
|
||||
abortSignal: signal,
|
||||
},
|
||||
);
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload media file to nostr.build
|
||||
* @todo support multiple backends
|
||||
*/
|
||||
public async upload({ fileExts }: { fileExts?: string[] }) {
|
||||
const defaultExts = ["png", "jpeg", "jpg", "gif"].concat(fileExts);
|
||||
|
||||
const selected = await open({
|
||||
multiple: false,
|
||||
filters: [
|
||||
{
|
||||
name: "Image",
|
||||
extensions: defaultExts,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (!selected) return null;
|
||||
|
||||
const file = await readFile(selected.path);
|
||||
const blob = new Blob([file]);
|
||||
|
||||
const data = new FormData();
|
||||
data.append("fileToUpload", blob);
|
||||
data.append("submit", "Upload Image");
|
||||
|
||||
const res = await fetch("https://nostr.build/api/v2/upload/files", {
|
||||
method: "POST",
|
||||
body: data,
|
||||
});
|
||||
|
||||
if (!res.ok) return null;
|
||||
|
||||
const json = await res.json();
|
||||
const content = json.data[0];
|
||||
|
||||
return content.url as string;
|
||||
}
|
||||
|
||||
public async validateNIP05({
|
||||
pubkey,
|
||||
nip05,
|
||||
signal,
|
||||
}: {
|
||||
pubkey: string;
|
||||
nip05: string;
|
||||
signal?: AbortSignal;
|
||||
}) {
|
||||
const localPath = nip05.split("@")[0];
|
||||
const service = nip05.split("@")[1];
|
||||
const verifyURL = `https://${service}/.well-known/nostr.json?name=${localPath}`;
|
||||
|
||||
const res = await fetch(verifyURL, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
signal,
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error(`Failed to fetch NIP-05 service: ${nip05}`);
|
||||
|
||||
const data: NIP05 = await res.json();
|
||||
|
||||
if (!data.names) return false;
|
||||
if (data.names[localPath.toLowerCase()] === pubkey) return true;
|
||||
if (data.names[localPath] === pubkey) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async getAppRecommend({
|
||||
unknownKind,
|
||||
author,
|
||||
}: { unknownKind: string; author?: string }) {
|
||||
const event = await this.ndk.fetchEvent({
|
||||
kinds: [NDKKind.AppRecommendation],
|
||||
"#d": [unknownKind],
|
||||
authors: this.account.contacts || [author],
|
||||
});
|
||||
|
||||
if (event) return event.tags.filter((item) => item[0] !== "d");
|
||||
|
||||
const altEvent = await this.ndk.fetchEvent({
|
||||
kinds: [NDKKind.AppHandler],
|
||||
"#k": [unknownKind],
|
||||
authors: this.account.contacts || [author],
|
||||
});
|
||||
|
||||
if (altEvent) return altEvent.tags.filter((item) => item[0] !== "d");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public async getOAuthServices() {
|
||||
const trusted: NDKEvent[] = [];
|
||||
|
||||
const services = await this.ndk.fetchEvents({
|
||||
kinds: [NDKKind.AppHandler],
|
||||
"#k": ["24133"],
|
||||
});
|
||||
|
||||
for (const service of services) {
|
||||
const nip05 = JSON.parse(service.content).nip05 as string;
|
||||
try {
|
||||
const validate = await this.validateNIP05({
|
||||
pubkey: service.pubkey,
|
||||
nip05,
|
||||
});
|
||||
if (validate) trusted.push(service);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
return trusted;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useStorage } from "@lume/storage";
|
||||
import { Kind } from "@lume/types";
|
||||
import {
|
||||
AUDIOS,
|
||||
IMAGES,
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
cn,
|
||||
regionNames,
|
||||
} from "@lume/utils";
|
||||
import { NDKKind } from "@nostr-dev-kit/ndk";
|
||||
import { fetch } from "@tauri-apps/plugin-http";
|
||||
import getUrls from "get-urls";
|
||||
import { nanoid } from "nanoid";
|
||||
@@ -32,7 +31,6 @@ export function NoteContent({
|
||||
}: {
|
||||
className?: string;
|
||||
}) {
|
||||
const storage = useStorage();
|
||||
const event = useNoteContext();
|
||||
|
||||
const [content, setContent] = useState(event.content);
|
||||
@@ -42,7 +40,7 @@ export function NoteContent({
|
||||
});
|
||||
|
||||
const richContent = useMemo(() => {
|
||||
if (event.kind !== NDKKind.Text) return content;
|
||||
if (event.kind !== Kind.Text) return content;
|
||||
|
||||
let parsedContent: string | ReactNode[] = stripHtml(
|
||||
content.replace(/\n{2,}\s*/g, "\n"),
|
||||
|
||||
@@ -2,32 +2,22 @@ import { HorizontalDotsIcon } from "@lume/icons";
|
||||
import { COL_TYPES } from "@lume/utils";
|
||||
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
||||
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { type EventPointer } from "nostr-tools/lib/types/nip19";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import { useArk } from "../../hooks/useArk";
|
||||
import { useColumnContext } from "../column/provider";
|
||||
import { useNoteContext } from "./provider";
|
||||
|
||||
export function NoteMenu() {
|
||||
const ark = useArk();
|
||||
const event = useNoteContext();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { addColumn } = useColumnContext();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const copyID = async () => {
|
||||
await writeText(
|
||||
nip19.neventEncode({
|
||||
id: event.id,
|
||||
author: event.pubkey,
|
||||
} as EventPointer),
|
||||
);
|
||||
setOpen(false);
|
||||
await writeText(await ark.event_to_bech32(event.id, [""]));
|
||||
};
|
||||
|
||||
const copyRaw = async () => {
|
||||
@@ -35,26 +25,17 @@ export function NoteMenu() {
|
||||
};
|
||||
|
||||
const copyNpub = async () => {
|
||||
await writeText(nip19.npubEncode(event.pubkey));
|
||||
await writeText(await ark.user_to_bech32(event.pubkey, [""]));
|
||||
};
|
||||
|
||||
const copyLink = async () => {
|
||||
await writeText(
|
||||
`https://njump.me/${nip19.neventEncode({
|
||||
id: event.id,
|
||||
author: event.pubkey,
|
||||
} as EventPointer)}`,
|
||||
`https://njump.me/${await ark.event_to_bech32(event.id, [""])}`,
|
||||
);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const muteUser = async () => {
|
||||
event.muted();
|
||||
toast.info("You've muted this user");
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root open={open} onOpenChange={setOpen}>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
@@ -134,15 +115,6 @@ export function NoteMenu() {
|
||||
{t("note.menu.copyRaw")}
|
||||
</button>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={muteUser}
|
||||
className="inline-flex items-center gap-3 px-3 text-sm font-medium text-red-500 rounded-lg h-9 hover:bg-red-500 hover:text-red-50 focus:outline-none"
|
||||
>
|
||||
{t("note.menu.mute")}
|
||||
</button>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu.Root>
|
||||
|
||||
@@ -14,7 +14,7 @@ export function NoteThread({
|
||||
}) {
|
||||
const ark = useArk();
|
||||
const event = useNoteContext();
|
||||
const thread = ark.getEventThread({
|
||||
const thread = ark.parse_event_thread({
|
||||
content: event.content,
|
||||
tags: event.tags,
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { NDKUserProfile } from "@nostr-dev-kit/ndk";
|
||||
import { Metadata } from "@lume/types";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ReactNode, createContext, useContext } from "react";
|
||||
import { useArk } from "../../hooks/useArk";
|
||||
|
||||
const UserContext = createContext<NDKUserProfile>(null);
|
||||
const UserContext = createContext<Metadata>(null);
|
||||
|
||||
export function UserProvider({
|
||||
pubkey,
|
||||
@@ -14,9 +14,10 @@ export function UserProvider({
|
||||
const { data: user } = useQuery({
|
||||
queryKey: ["user", pubkey],
|
||||
queryFn: async () => {
|
||||
if (embed) return JSON.parse(embed) as NDKUserProfile;
|
||||
if (embed) return JSON.parse(embed) as Metadata;
|
||||
|
||||
const profile = await ark.get_metadata(pubkey);
|
||||
|
||||
const profile = await ark.getUserProfile(pubkey);
|
||||
if (!profile)
|
||||
throw new Error(
|
||||
`Cannot get metadata for ${pubkey}, will be retry after 10 seconds`,
|
||||
|
||||
@@ -6,7 +6,7 @@ export function useEvent(id: string) {
|
||||
const { isLoading, isError, data } = useQuery({
|
||||
queryKey: ["event", id],
|
||||
queryFn: async () => {
|
||||
const event = await ark.getEventById(id);
|
||||
const event = await ark.get_event(id);
|
||||
if (!event)
|
||||
throw new Error(
|
||||
`Cannot get event with ${id}, will be retry after 10 seconds`,
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { NDKUserProfile } from "@nostr-dev-kit/ndk";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useArk } from "./useArk";
|
||||
|
||||
export function useProfile(pubkey: string) {
|
||||
const ark = useArk();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const {
|
||||
isLoading,
|
||||
isError,
|
||||
@@ -13,16 +10,13 @@ export function useProfile(pubkey: string) {
|
||||
} = useQuery({
|
||||
queryKey: ["user", pubkey],
|
||||
queryFn: async () => {
|
||||
const profile = await ark.getUserProfile(pubkey);
|
||||
const profile = await ark.get_metadata(pubkey);
|
||||
if (!profile)
|
||||
throw new Error(
|
||||
`Cannot get metadata for ${pubkey}, will be retry after 10 seconds`,
|
||||
);
|
||||
return profile;
|
||||
},
|
||||
initialData: () => {
|
||||
return queryClient.getQueryData(["user", pubkey]) as NDKUserProfile;
|
||||
},
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
|
||||
@@ -1,255 +1,18 @@
|
||||
import { LoaderIcon } from "@lume/icons";
|
||||
import { NDKCacheAdapterTauri } from "@lume/ndk-cache-tauri";
|
||||
import { useStorage } from "@lume/storage";
|
||||
import {
|
||||
FETCH_LIMIT,
|
||||
QUOTES,
|
||||
activityUnreadAtom,
|
||||
sendNativeNotification,
|
||||
} from "@lume/utils";
|
||||
import NDK, {
|
||||
NDKEvent,
|
||||
NDKKind,
|
||||
NDKNip46Signer,
|
||||
NDKPrivateKeySigner,
|
||||
NDKRelay,
|
||||
NDKRelayAuthPolicies,
|
||||
NDKUser,
|
||||
} from "@nostr-dev-kit/ndk";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { message } from "@tauri-apps/plugin-dialog";
|
||||
import { fetch } from "@tauri-apps/plugin-http";
|
||||
import { useSetAtom } from "jotai";
|
||||
import Linkify from "linkify-react";
|
||||
import { normalizeRelayUrlSet } from "nostr-fetch";
|
||||
import { PropsWithChildren, useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Ark } from "./ark";
|
||||
import { LumeContext } from "./context";
|
||||
|
||||
export const LumeProvider = ({ children }: PropsWithChildren<object>) => {
|
||||
const storage = useStorage();
|
||||
const queryClient = useQueryClient();
|
||||
const setUnreadActivity = useSetAtom(activityUnreadAtom);
|
||||
|
||||
const [ark, setArk] = useState<Ark>(undefined);
|
||||
const [ndk, setNDK] = useState<NDK>(undefined);
|
||||
|
||||
async function initNostrSigner({
|
||||
nsecbunker,
|
||||
}: {
|
||||
nsecbunker?: boolean;
|
||||
}) {
|
||||
try {
|
||||
if (!storage.currentUser) return null;
|
||||
|
||||
// NIP-46 Signer
|
||||
if (nsecbunker) {
|
||||
const localSignerPrivkey = await storage.loadPrivkey(
|
||||
storage.currentUser.pubkey,
|
||||
);
|
||||
|
||||
if (!localSignerPrivkey) return null;
|
||||
|
||||
const localSigner = new NDKPrivateKeySigner(localSignerPrivkey);
|
||||
const bunker = new NDK({
|
||||
explicitRelayUrls: normalizeRelayUrlSet([
|
||||
"wss://relay.nsecbunker.com/",
|
||||
"wss://nostr.vulpem.com/",
|
||||
]),
|
||||
});
|
||||
await bunker.connect(2000);
|
||||
|
||||
const remoteSigner = new NDKNip46Signer(
|
||||
bunker,
|
||||
storage.currentUser.pubkey,
|
||||
localSigner,
|
||||
);
|
||||
await remoteSigner.blockUntilReady();
|
||||
|
||||
return remoteSigner;
|
||||
}
|
||||
|
||||
// Privkey Signer
|
||||
const userPrivkey = await storage.loadPrivkey(storage.currentUser.pubkey);
|
||||
if (!userPrivkey) return null;
|
||||
|
||||
// load nwc
|
||||
storage.nwc = await storage.loadPrivkey(
|
||||
`${storage.currentUser.pubkey}.nwc`,
|
||||
);
|
||||
|
||||
return new NDKPrivateKeySigner(userPrivkey);
|
||||
} catch (e) {
|
||||
toast.error(String(e));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function initNDK() {
|
||||
try {
|
||||
const explicitRelayUrls = normalizeRelayUrlSet([
|
||||
"wss://nostr.mutinywallet.com/",
|
||||
"wss://bostr.nokotaro.com/",
|
||||
"wss://purplepag.es/",
|
||||
]);
|
||||
|
||||
const outboxRelayUrls = normalizeRelayUrlSet(["wss://purplepag.es/"]);
|
||||
|
||||
const tauriCache = new NDKCacheAdapterTauri(storage);
|
||||
const ndk = new NDK({
|
||||
cacheAdapter: tauriCache,
|
||||
explicitRelayUrls,
|
||||
outboxRelayUrls,
|
||||
enableOutboxModel: !storage.settings.lowPower,
|
||||
autoConnectUserRelays: !storage.settings.lowPower,
|
||||
autoFetchUserMutelist: false, // #TODO: add support mute list
|
||||
clientName: "Lume",
|
||||
});
|
||||
|
||||
// use tauri fetch
|
||||
ndk.httpFetch = fetch;
|
||||
|
||||
// add signer
|
||||
const signer = await initNostrSigner({
|
||||
nsecbunker: storage.settings.nsecbunker,
|
||||
});
|
||||
|
||||
if (signer) ndk.signer = signer;
|
||||
|
||||
// connect
|
||||
await ndk.connect(3000);
|
||||
|
||||
// auth
|
||||
ndk.relayAuthDefaultPolicy = async (
|
||||
relay: NDKRelay,
|
||||
challenge: string,
|
||||
) => {
|
||||
const signIn = NDKRelayAuthPolicies.signIn({ ndk });
|
||||
const event = await signIn(relay, challenge).catch((e) =>
|
||||
console.log("auth failed", e),
|
||||
);
|
||||
if (event) {
|
||||
await sendNativeNotification(
|
||||
`You've sign in sucessfully to relay: ${relay.url}`,
|
||||
);
|
||||
return event;
|
||||
}
|
||||
};
|
||||
|
||||
setNDK(ndk);
|
||||
} catch (e) {
|
||||
toast.error(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
async function initArk() {
|
||||
if (!ndk) await message("Something wrong!", { type: "error" });
|
||||
|
||||
// ark utils
|
||||
const ark = new Ark({ ndk, account: storage.currentUser });
|
||||
|
||||
try {
|
||||
if (ndk && storage.currentUser) {
|
||||
const user = new NDKUser({ pubkey: storage.currentUser.pubkey });
|
||||
ndk.activeUser = user;
|
||||
|
||||
// update contacts
|
||||
const contacts = await ark.getUserContacts();
|
||||
|
||||
if (contacts?.length) {
|
||||
console.log("total contacts: ", contacts.length);
|
||||
for (const pubkey of ark.account.contacts) {
|
||||
await queryClient.prefetchQuery({
|
||||
queryKey: ["user", pubkey],
|
||||
queryFn: async () => {
|
||||
return await ark.getUserProfile(pubkey);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// subscribe for new activity
|
||||
const activitySub = ndk.subscribe(
|
||||
{
|
||||
kinds: [NDKKind.Text, NDKKind.Repost, NDKKind.Zap],
|
||||
since: Math.floor(Date.now() / 1000),
|
||||
"#p": [ark.account.pubkey],
|
||||
},
|
||||
{ closeOnEose: false, groupable: false },
|
||||
);
|
||||
|
||||
activitySub.addListener("event", async (event: NDKEvent) => {
|
||||
if (event.pubkey === storage.currentUser.pubkey) return;
|
||||
|
||||
setUnreadActivity((state) => state + 1);
|
||||
const profile = await ark.getUserProfile(event.pubkey);
|
||||
|
||||
switch (event.kind) {
|
||||
case NDKKind.Text:
|
||||
return await sendNativeNotification(
|
||||
`${
|
||||
profile.displayName || profile.name || "Anon"
|
||||
} has replied to your note`,
|
||||
);
|
||||
case NDKKind.Repost:
|
||||
return await sendNativeNotification(
|
||||
`${
|
||||
profile.displayName || profile.name || "Anon"
|
||||
} has reposted to your note`,
|
||||
);
|
||||
case NDKKind.Zap:
|
||||
return await sendNativeNotification(
|
||||
`${
|
||||
profile.displayName || profile.name || "Anon"
|
||||
} has zapped to your note`,
|
||||
);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error(String(e));
|
||||
}
|
||||
|
||||
setArk(ark);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (ndk) initArk();
|
||||
}, [ndk]);
|
||||
async function setupArk() {
|
||||
const _ark = new Ark();
|
||||
setArk(_ark);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!ark && !ndk) initNDK();
|
||||
if (!ark) setupArk();
|
||||
}, []);
|
||||
|
||||
if (!ark) {
|
||||
return (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="relative flex items-center justify-center w-screen h-screen bg-white dark:bg-black"
|
||||
>
|
||||
<div className="flex flex-col items-start max-w-2xl gap-1">
|
||||
<h5 className="font-semibold uppercase">TIP:</h5>
|
||||
<Linkify
|
||||
options={{
|
||||
target: "_blank",
|
||||
className: "text-blue-500 hover:text-blue-600",
|
||||
}}
|
||||
>
|
||||
<div className="text-4xl font-semibold leading-snug text-neutral-300 dark:text-neutral-700">
|
||||
{QUOTES[Math.floor(Math.random() * QUOTES.length)]}
|
||||
</div>
|
||||
</Linkify>
|
||||
</div>
|
||||
<div className="absolute bottom-5 right-5 inline-flex items-center gap-2.5">
|
||||
<LoaderIcon className="w-6 h-6 text-blue-500 animate-spin" />
|
||||
<p className="font-semibold">Starting</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <LumeContext.Provider value={ark}>{children}</LumeContext.Provider>;
|
||||
};
|
||||
|
||||
@@ -16,6 +16,6 @@
|
||||
"tailwindcss": "^3.4.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"tailwindcss-radix-colors": "^1.2.0"
|
||||
"@evilmartians/harmony": "^1.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import harmonyPalette from "@evilmartians/harmony/tailwind";
|
||||
|
||||
const config = {
|
||||
theme: {
|
||||
colors: harmonyPalette,
|
||||
extend: {
|
||||
keyframes: {
|
||||
slideDownAndFade: {
|
||||
@@ -41,7 +44,6 @@ const config = {
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
require("tailwindcss-radix-colors"),
|
||||
require("@tailwindcss/forms"),
|
||||
require("@tailwindcss/typography"),
|
||||
require("tailwind-scrollbar")({ nocompatible: true }),
|
||||
|
||||
72
packages/types/index.d.ts
vendored
72
packages/types/index.d.ts
vendored
@@ -1,8 +1,56 @@
|
||||
import {
|
||||
type NDKEvent,
|
||||
NDKRelayList,
|
||||
type NDKUserProfile,
|
||||
} from "@nostr-dev-kit/ndk";
|
||||
import { type NDKEvent, type NDKUserProfile } from "@nostr-dev-kit/ndk";
|
||||
|
||||
export interface Keys {
|
||||
npub: string;
|
||||
nsec: string;
|
||||
}
|
||||
|
||||
export enum Kind {
|
||||
Metadata = 0,
|
||||
Text = 1,
|
||||
RecommendRelay = 2,
|
||||
Contacts = 3,
|
||||
Repost = 6,
|
||||
Reaction = 7,
|
||||
// NIP-89: App Metadata
|
||||
AppRecommendation = 31989,
|
||||
AppHandler = 31990,
|
||||
// #TODO: Add all nostr kinds
|
||||
}
|
||||
|
||||
export interface Event {
|
||||
id: string;
|
||||
pubkey: string;
|
||||
created_at: number;
|
||||
kind: Kind;
|
||||
tags: string[][];
|
||||
content: string;
|
||||
sig: string;
|
||||
}
|
||||
|
||||
export interface Metadata {
|
||||
name: Option<string>;
|
||||
display_name: Option<string>;
|
||||
about: Option<string>;
|
||||
website: Option<string>;
|
||||
picture: Option<string>;
|
||||
banner: Option<string>;
|
||||
nip05: Option<string>;
|
||||
lud06: Option<string>;
|
||||
lud16: Option<string>;
|
||||
}
|
||||
|
||||
export interface CurrentAccount {
|
||||
npub: string;
|
||||
contacts: string[];
|
||||
interests: Interests;
|
||||
}
|
||||
|
||||
export interface Interests {
|
||||
hashtags: string[];
|
||||
users: string[];
|
||||
words: string[];
|
||||
}
|
||||
|
||||
export interface RichContent {
|
||||
parsed: string;
|
||||
@@ -12,14 +60,6 @@ export interface RichContent {
|
||||
notes: string[];
|
||||
}
|
||||
|
||||
export interface Account {
|
||||
id: string;
|
||||
pubkey: string;
|
||||
is_active: number;
|
||||
contacts: string[];
|
||||
relayList: string[];
|
||||
}
|
||||
|
||||
export interface IColumn {
|
||||
id?: number;
|
||||
kind: number;
|
||||
@@ -115,9 +155,3 @@ export interface NIP05 {
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface Interests {
|
||||
hashtags: string[];
|
||||
users: string[];
|
||||
words: string[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user