refactor: use specta for commands (#192)
* feat: add tauri-specta * refactor: system library * chore: format
This commit is contained in:
@@ -1,902 +0,0 @@
|
||||
import {
|
||||
type Event,
|
||||
type EventWithReplies,
|
||||
type Interests,
|
||||
type Keys,
|
||||
type LumeColumn,
|
||||
type Metadata,
|
||||
type Settings,
|
||||
Relays,
|
||||
} from "@lume/types";
|
||||
import { generateContentTags } from "@lume/utils";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import type { WebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||
import { open } from "@tauri-apps/plugin-dialog";
|
||||
import { readFile } from "@tauri-apps/plugin-fs";
|
||||
|
||||
enum NSTORE_KEYS {
|
||||
settings = "lume_user_settings",
|
||||
columns = "lume_user_columns",
|
||||
}
|
||||
|
||||
export class Ark {
|
||||
public windows: WebviewWindow[];
|
||||
public settings: Settings;
|
||||
public accounts: string[];
|
||||
|
||||
constructor() {
|
||||
this.windows = [];
|
||||
this.settings = undefined;
|
||||
}
|
||||
|
||||
public async get_accounts() {
|
||||
try {
|
||||
const cmd: string = await invoke("get_accounts");
|
||||
const parse = cmd.split(/\s+/).filter((v) => v.startsWith("npub1"));
|
||||
const accounts = [...new Set(parse)];
|
||||
|
||||
if (!this.accounts) {
|
||||
this.accounts = accounts;
|
||||
}
|
||||
|
||||
return accounts;
|
||||
} catch (e) {
|
||||
console.info(String(e));
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async load_account(npub: string) {
|
||||
try {
|
||||
const cmd: boolean = await invoke("load_account", {
|
||||
npub,
|
||||
});
|
||||
return cmd;
|
||||
} catch (e) {
|
||||
throw new Error(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
public async nostr_connect(uri: string) {
|
||||
try {
|
||||
const remoteKey = uri.replace("bunker://", "").split("?")[0];
|
||||
const npub: string = await invoke("to_npub", { hex: remoteKey });
|
||||
|
||||
if (npub) {
|
||||
const connect: string = await invoke("nostr_connect", {
|
||||
npub,
|
||||
uri,
|
||||
});
|
||||
|
||||
return connect;
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
public async create_keys() {
|
||||
try {
|
||||
const cmd: Keys = await invoke("create_account");
|
||||
return cmd;
|
||||
} catch (e) {
|
||||
console.error(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
public async save_account(nsec: string, password = "") {
|
||||
try {
|
||||
const cmd: string = await invoke("save_account", {
|
||||
nsec,
|
||||
password,
|
||||
});
|
||||
|
||||
return cmd;
|
||||
} catch (e) {
|
||||
throw new Error(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
public async event_to_bech32(id: string, relays: string[]) {
|
||||
try {
|
||||
const cmd: string = await invoke("event_to_bech32", {
|
||||
id,
|
||||
relays,
|
||||
});
|
||||
return cmd;
|
||||
} catch (e) {
|
||||
throw new Error(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
public async get_relays() {
|
||||
try {
|
||||
const cmd: Relays = await invoke("get_relays");
|
||||
return cmd;
|
||||
} catch (e) {
|
||||
console.error(String(e));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async add_relay(url: string) {
|
||||
try {
|
||||
const relayUrl = new URL(url);
|
||||
|
||||
if (relayUrl.protocol === "wss:" || relayUrl.protocol === "ws:") {
|
||||
const cmd: boolean = await invoke("connect_relay", { relay: relayUrl });
|
||||
return cmd;
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
public async remove_relay(url: string) {
|
||||
try {
|
||||
const relayUrl = new URL(url);
|
||||
|
||||
if (relayUrl.protocol === "wss:" || relayUrl.protocol === "ws:") {
|
||||
const cmd: boolean = await invoke("remove_relay", { relay: relayUrl });
|
||||
return cmd;
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
public async get_activities(account: string, kind: "1" | "6" | "9735" = "1") {
|
||||
try {
|
||||
const events: Event[] = await invoke("get_activities", { account, kind });
|
||||
return events;
|
||||
} catch (e) {
|
||||
console.error(String(e));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async get_event(id: string) {
|
||||
try {
|
||||
const eventId: string = id.replace("nostr:", "").replace(/[^\w\s]/gi, "");
|
||||
const cmd: string = await invoke("get_event", { id: eventId });
|
||||
const event: Event = JSON.parse(cmd);
|
||||
return event;
|
||||
} catch (e) {
|
||||
console.error(id, String(e));
|
||||
throw new Error(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
public async search(content: string, limit: number) {
|
||||
try {
|
||||
if (content.length < 1) return [];
|
||||
|
||||
const events: Event[] = await invoke("search", {
|
||||
content: content.trim(),
|
||||
limit,
|
||||
});
|
||||
|
||||
return events;
|
||||
} catch (e) {
|
||||
console.info(String(e));
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private dedup_events(nostrEvents: Event[]) {
|
||||
const seens = new Set<string>();
|
||||
const events = nostrEvents.filter((event) => {
|
||||
const eTags = event.tags.filter((el) => el[0] === "e");
|
||||
const ids = eTags.map((item) => item[1]);
|
||||
const isDup = ids.some((id) => seens.has(id));
|
||||
|
||||
// Add found ids to seen list
|
||||
for (const id of ids) {
|
||||
seens.add(id);
|
||||
}
|
||||
|
||||
// Filter NSFW event
|
||||
if (this.settings?.nsfw) {
|
||||
const wTags = event.tags.filter((t) => t[0] === "content-warning");
|
||||
const isLewd = wTags.length > 0;
|
||||
|
||||
return !isDup && !isLewd;
|
||||
}
|
||||
|
||||
// Filter duplicate event
|
||||
return !isDup;
|
||||
});
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
public async get_local_events(
|
||||
pubkeys: string[],
|
||||
limit: number,
|
||||
asOf?: number,
|
||||
) {
|
||||
try {
|
||||
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
|
||||
const nostrEvents: Event[] = await invoke("get_local_events", {
|
||||
pubkeys,
|
||||
limit,
|
||||
until,
|
||||
});
|
||||
const events = this.dedup_events(nostrEvents);
|
||||
|
||||
return events;
|
||||
} catch (e) {
|
||||
console.error("[get_local_events] failed", String(e));
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async get_global_events(limit: number, asOf?: number) {
|
||||
try {
|
||||
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
|
||||
const nostrEvents: Event[] = await invoke("get_global_events", {
|
||||
limit,
|
||||
until,
|
||||
});
|
||||
const events = this.dedup_events(nostrEvents);
|
||||
|
||||
return events;
|
||||
} catch (e) {
|
||||
console.error("[get_global_events] failed", String(e));
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async get_hashtag_events(
|
||||
hashtags: string[],
|
||||
limit: number,
|
||||
asOf?: number,
|
||||
) {
|
||||
try {
|
||||
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
|
||||
const nostrTags = hashtags.map((tag) => tag.replace("#", ""));
|
||||
const nostrEvents: Event[] = await invoke("get_hashtag_events", {
|
||||
hashtags: nostrTags,
|
||||
limit,
|
||||
until,
|
||||
});
|
||||
const events = this.dedup_events(nostrEvents);
|
||||
|
||||
return events;
|
||||
} catch (e) {
|
||||
console.error("[get_hashtag_events] failed", String(e));
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async get_group_events(
|
||||
contacts: string[],
|
||||
limit: number,
|
||||
asOf?: number,
|
||||
) {
|
||||
try {
|
||||
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
|
||||
const nostrEvents: Event[] = await invoke("get_group_events", {
|
||||
list: contacts,
|
||||
limit,
|
||||
until,
|
||||
});
|
||||
const events = this.dedup_events(nostrEvents);
|
||||
|
||||
return events;
|
||||
} catch (e) {
|
||||
console.error("[get_group_events] failed", String(e));
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async get_events_by(pubkey: string, limit: number, asOf?: number) {
|
||||
try {
|
||||
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
|
||||
const nostrEvents: Event[] = await invoke("get_events_by", {
|
||||
publicKey: pubkey,
|
||||
limit,
|
||||
as_of: until,
|
||||
});
|
||||
|
||||
return nostrEvents.sort((a, b) => b.created_at - a.created_at);
|
||||
} catch (e) {
|
||||
console.error("[get_events_by] failed", String(e));
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async publish(
|
||||
content: string,
|
||||
reply_to?: string,
|
||||
quote?: boolean,
|
||||
nsfw?: boolean,
|
||||
) {
|
||||
try {
|
||||
const g = await generateContentTags(content);
|
||||
|
||||
const eventContent = g.content;
|
||||
const eventTags = g.tags;
|
||||
|
||||
if (reply_to) {
|
||||
const replyEvent = await this.get_event(reply_to);
|
||||
const relayHint =
|
||||
replyEvent.tags.find((ev) => ev[0] === "e")?.[0][2] ?? "";
|
||||
|
||||
if (quote) {
|
||||
eventTags.push(["e", replyEvent.id, relayHint, "mention"]);
|
||||
eventTags.push(["q", replyEvent.id]);
|
||||
} else {
|
||||
const rootEvent = replyEvent.tags.find((ev) => ev[3] === "root");
|
||||
|
||||
if (rootEvent) {
|
||||
eventTags.push([
|
||||
"e",
|
||||
rootEvent[1],
|
||||
rootEvent[2] || relayHint,
|
||||
"root",
|
||||
]);
|
||||
}
|
||||
|
||||
eventTags.push(["e", replyEvent.id, relayHint, "reply"]);
|
||||
eventTags.push(["p", replyEvent.pubkey]);
|
||||
}
|
||||
}
|
||||
|
||||
if (nsfw) {
|
||||
eventTags.push(["L", "content-warning"]);
|
||||
eventTags.push(["l", "reason", "content-warning"]);
|
||||
eventTags.push(["content-warning", "nsfw"]);
|
||||
}
|
||||
|
||||
const cmd: string = await invoke("publish", {
|
||||
content: eventContent,
|
||||
tags: eventTags,
|
||||
});
|
||||
|
||||
return cmd;
|
||||
} catch (e) {
|
||||
throw new Error(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
public async reply_to(content: string, tags: string[]) {
|
||||
try {
|
||||
const cmd: string = await invoke("reply_to", { content, tags });
|
||||
return cmd;
|
||||
} catch (e) {
|
||||
throw new Error(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
public async repost(id: string, author: string) {
|
||||
try {
|
||||
const cmd: string = await invoke("repost", { id, pubkey: author });
|
||||
return cmd;
|
||||
} catch (e) {
|
||||
throw new Error(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
public async get_event_thread(id: string) {
|
||||
try {
|
||||
const events: EventWithReplies[] = await invoke("get_thread", {
|
||||
id,
|
||||
});
|
||||
|
||||
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;
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public get_thread(tags: string[][], gossip: boolean = false) {
|
||||
let root: string = null;
|
||||
let reply: string = null;
|
||||
|
||||
// Get all event references from tags, ignore mention
|
||||
const events = tags.filter((el) => el[0] === "e" && el[3] !== "mention");
|
||||
|
||||
if (gossip) {
|
||||
const relays = tags.filter((el) => el[0] === "e" && el[2]?.length);
|
||||
|
||||
if (relays.length >= 1) {
|
||||
for (const relay of relays) {
|
||||
if (relay[2]?.length) this.add_relay(relay[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (events.length === 1) {
|
||||
root = events[0][1];
|
||||
}
|
||||
|
||||
if (events.length > 1) {
|
||||
root = events.find((el) => el[3] === "root")?.[1] ?? events[0][1];
|
||||
reply = events.find((el) => el[3] === "reply")?.[1] ?? events[1][1];
|
||||
}
|
||||
|
||||
// Fix some rare case when root === reply
|
||||
if (root && reply && root === reply) {
|
||||
reply = null;
|
||||
}
|
||||
|
||||
return {
|
||||
root,
|
||||
reply,
|
||||
};
|
||||
}
|
||||
|
||||
public async get_profile(pubkey: string) {
|
||||
try {
|
||||
const id = pubkey.replace("nostr:", "").replace(/[^\w\s]/gi, "");
|
||||
const cmd: Metadata = await invoke("get_profile", { id });
|
||||
|
||||
return cmd;
|
||||
} catch (e) {
|
||||
console.error(pubkey, String(e));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async get_current_user_profile() {
|
||||
try {
|
||||
const cmd: Metadata = await invoke("get_current_user_profile");
|
||||
return cmd;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async create_profile(profile: Metadata) {
|
||||
try {
|
||||
const event: string = await invoke("create_profile", {
|
||||
name: profile.name || "",
|
||||
display_name: profile.display_name || "",
|
||||
displayName: profile.display_name || "",
|
||||
about: profile.about || "",
|
||||
picture: profile.picture || "",
|
||||
banner: profile.banner || "",
|
||||
nip05: profile.nip05 || "",
|
||||
lud16: profile.lud16 || "",
|
||||
website: profile.website || "",
|
||||
});
|
||||
return event;
|
||||
} catch (e) {
|
||||
throw new Error(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
public async set_contact_list(pubkeys: string[]) {
|
||||
try {
|
||||
const cmd: boolean = await invoke("set_contact_list", { pubkeys });
|
||||
return cmd;
|
||||
} catch (e) {
|
||||
throw new Error(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
public async get_contact_list() {
|
||||
try {
|
||||
const cmd: string[] = await invoke("get_contact_list");
|
||||
return cmd;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async follow(id: string, alias?: string) {
|
||||
try {
|
||||
const cmd: string = await invoke("follow", { id, alias });
|
||||
return cmd;
|
||||
} catch (e) {
|
||||
throw new Error(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
public async unfollow(id: string) {
|
||||
try {
|
||||
const cmd: string = await invoke("unfollow", { id });
|
||||
return cmd;
|
||||
} catch (e) {
|
||||
throw new Error(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
public async user_to_bech32(key: string, relays: string[]) {
|
||||
try {
|
||||
const cmd: string = await invoke("user_to_bech32", {
|
||||
key,
|
||||
relays,
|
||||
});
|
||||
return cmd;
|
||||
} catch (e) {
|
||||
throw new Error(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
public async verify_nip05(pubkey: string, nip05: string) {
|
||||
try {
|
||||
const cmd: boolean = await invoke("verify_nip05", {
|
||||
key: pubkey,
|
||||
nip05,
|
||||
});
|
||||
return cmd;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async set_nwc(uri: string) {
|
||||
try {
|
||||
const cmd: boolean = await invoke("set_nwc", { uri });
|
||||
return cmd;
|
||||
} catch (e) {
|
||||
throw new Error(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
public async load_nwc() {
|
||||
try {
|
||||
const cmd: boolean = await invoke("load_nwc");
|
||||
return cmd;
|
||||
} catch (e) {
|
||||
throw new Error(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
public async get_balance() {
|
||||
try {
|
||||
const cmd: number = await invoke("get_balance");
|
||||
return cmd;
|
||||
} catch (e) {
|
||||
throw new Error(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
public async zap_profile(id: string, amount: number, message: string) {
|
||||
try {
|
||||
const cmd: boolean = await invoke("zap_profile", { id, amount, message });
|
||||
return cmd;
|
||||
} catch (e) {
|
||||
throw new Error(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
public async zap_event(id: string, amount: number, message: string) {
|
||||
try {
|
||||
const cmd: boolean = await invoke("zap_event", { id, amount, message });
|
||||
return cmd;
|
||||
} catch (e) {
|
||||
throw new Error(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
public async upload(filePath?: string) {
|
||||
const allowExts = [
|
||||
"png",
|
||||
"jpeg",
|
||||
"jpg",
|
||||
"gif",
|
||||
"mp4",
|
||||
"mp3",
|
||||
"webm",
|
||||
"mkv",
|
||||
"avi",
|
||||
"mov",
|
||||
];
|
||||
|
||||
const selected =
|
||||
filePath ||
|
||||
(
|
||||
await open({
|
||||
multiple: false,
|
||||
filters: [
|
||||
{
|
||||
name: "Media",
|
||||
extensions: allowExts,
|
||||
},
|
||||
],
|
||||
})
|
||||
).path;
|
||||
|
||||
// User cancelled action
|
||||
if (!selected) return null;
|
||||
|
||||
try {
|
||||
const file = await readFile(selected);
|
||||
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;
|
||||
} catch (e) {
|
||||
throw new Error(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
public async get_columns() {
|
||||
try {
|
||||
const cmd: string = await invoke("get_nstore", {
|
||||
key: NSTORE_KEYS.columns,
|
||||
});
|
||||
const columns: LumeColumn[] = cmd ? JSON.parse(cmd) : [];
|
||||
return columns;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async set_columns(columns: LumeColumn[]) {
|
||||
try {
|
||||
const cmd: string = await invoke("set_nstore", {
|
||||
key: NSTORE_KEYS.columns,
|
||||
content: JSON.stringify(columns),
|
||||
});
|
||||
return cmd;
|
||||
} catch (e) {
|
||||
throw new Error(e);
|
||||
}
|
||||
}
|
||||
|
||||
public async get_settings() {
|
||||
try {
|
||||
if (this.settings) return this.settings;
|
||||
|
||||
const cmd: string = await invoke("get_nstore", {
|
||||
key: NSTORE_KEYS.settings,
|
||||
});
|
||||
const settings: Settings = cmd ? JSON.parse(cmd) : null;
|
||||
|
||||
this.settings = settings;
|
||||
|
||||
return settings;
|
||||
} catch {
|
||||
const defaultSettings: Settings = {
|
||||
autoUpdate: false,
|
||||
enhancedPrivacy: false,
|
||||
notification: false,
|
||||
zap: false,
|
||||
nsfw: false,
|
||||
};
|
||||
this.settings = defaultSettings;
|
||||
return defaultSettings;
|
||||
}
|
||||
}
|
||||
|
||||
public async set_settings(settings: Settings) {
|
||||
try {
|
||||
const cmd: string = await invoke("set_nstore", {
|
||||
key: NSTORE_KEYS.settings,
|
||||
content: JSON.stringify(settings),
|
||||
});
|
||||
return cmd;
|
||||
} catch (e) {
|
||||
throw new Error(e);
|
||||
}
|
||||
}
|
||||
|
||||
public async get_nstore(key: string) {
|
||||
try {
|
||||
const cmd: string = await invoke("get_nstore", {
|
||||
key,
|
||||
});
|
||||
const parse: string | string[] = cmd ? JSON.parse(cmd) : null;
|
||||
if (!parse.length) return null;
|
||||
return parse;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async set_nstore(key: string, content: string) {
|
||||
try {
|
||||
const cmd: string = await invoke("set_nstore", {
|
||||
key,
|
||||
content,
|
||||
});
|
||||
return cmd;
|
||||
} catch (e) {
|
||||
throw new Error(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
public async open_event_id(id: string) {
|
||||
try {
|
||||
const label = `event-${id}`;
|
||||
const url = `/events/${id}`;
|
||||
|
||||
await invoke("open_window", {
|
||||
label,
|
||||
title: "Thread",
|
||||
url,
|
||||
width: 500,
|
||||
height: 800,
|
||||
});
|
||||
} catch (e) {
|
||||
throw new Error(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
public async open_event(event: Event) {
|
||||
try {
|
||||
let root: string = undefined;
|
||||
let reply: string = undefined;
|
||||
|
||||
const eTags = event.tags.filter(
|
||||
(tag) => tag[0] === "e" || tag[0] === "q",
|
||||
);
|
||||
|
||||
root = eTags.find((el) => el[3] === "root")?.[1];
|
||||
reply = eTags.find((el) => el[3] === "reply")?.[1];
|
||||
|
||||
if (!root) root = eTags[0]?.[1];
|
||||
if (!reply) reply = eTags[1]?.[1];
|
||||
|
||||
const label = `event-${event.id}`;
|
||||
const url = `/events/${root ?? reply ?? event.id}`;
|
||||
|
||||
await invoke("open_window", {
|
||||
label,
|
||||
title: "Thread",
|
||||
url,
|
||||
width: 500,
|
||||
height: 800,
|
||||
});
|
||||
} catch (e) {
|
||||
throw new Error(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
public async open_profile(pubkey: string) {
|
||||
try {
|
||||
const label = `user-${pubkey}`;
|
||||
await invoke("open_window", {
|
||||
label,
|
||||
title: "Profile",
|
||||
url: `/users/${pubkey}`,
|
||||
width: 500,
|
||||
height: 800,
|
||||
});
|
||||
} catch (e) {
|
||||
throw new Error(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
public async open_editor(reply_to?: string, quote = false) {
|
||||
try {
|
||||
let url: string;
|
||||
|
||||
if (reply_to) {
|
||||
url = `/editor?reply_to=${reply_to}"e=${quote}`;
|
||||
} else {
|
||||
url = "/editor";
|
||||
}
|
||||
|
||||
const label = `editor-${reply_to ? reply_to : 0}`;
|
||||
|
||||
await invoke("open_window", {
|
||||
label,
|
||||
title: "Editor",
|
||||
url,
|
||||
width: 560,
|
||||
height: 340,
|
||||
});
|
||||
} catch (e) {
|
||||
throw new Error(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
public async open_nwc() {
|
||||
try {
|
||||
const label = "nwc";
|
||||
await invoke("open_window", {
|
||||
label,
|
||||
title: "Nostr Wallet Connect",
|
||||
url: "/nwc",
|
||||
width: 400,
|
||||
height: 600,
|
||||
});
|
||||
} catch (e) {
|
||||
throw new Error(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
public async open_zap(id: string, pubkey: string, account: string) {
|
||||
try {
|
||||
const label = `zap-${id}`;
|
||||
await invoke("open_window", {
|
||||
label,
|
||||
title: "Zap",
|
||||
url: `/zap/${id}?pubkey=${pubkey}&account=${account}`,
|
||||
width: 400,
|
||||
height: 500,
|
||||
});
|
||||
} catch (e) {
|
||||
throw new Error(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
public async open_settings() {
|
||||
try {
|
||||
const label = "settings";
|
||||
await invoke("open_window", {
|
||||
label,
|
||||
title: "Settings",
|
||||
url: "/settings",
|
||||
width: 800,
|
||||
height: 500,
|
||||
});
|
||||
} catch (e) {
|
||||
throw new Error(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
public async open_search() {
|
||||
try {
|
||||
const label = "search";
|
||||
await invoke("open_window", {
|
||||
label,
|
||||
title: "Search",
|
||||
url: "/search",
|
||||
width: 400,
|
||||
height: 600,
|
||||
});
|
||||
} catch (e) {
|
||||
throw new Error(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
public async open_activity(account: string) {
|
||||
try {
|
||||
const label = "activity";
|
||||
await invoke("open_window", {
|
||||
label,
|
||||
title: "Activity",
|
||||
url: `/activity/${account}/texts`,
|
||||
width: 400,
|
||||
height: 600,
|
||||
});
|
||||
} catch (e) {
|
||||
throw new Error(String(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
export function usePreview(url: string) {
|
||||
const { isLoading, isError, data } = useQuery({
|
||||
queryKey: ["url", url],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const cmd = await invoke("fetch_opg", { url });
|
||||
console.log(cmd);
|
||||
return cmd;
|
||||
} catch (e) {
|
||||
throw new Error(e);
|
||||
}
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
staleTime: Number.POSITIVE_INFINITY,
|
||||
retry: 2,
|
||||
});
|
||||
|
||||
return { isLoading, isError, data };
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from "./ark";
|
||||
export * from "./hooks/useEvent";
|
||||
export * from "./hooks/useProfile";
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@lume/ark",
|
||||
"name": "@lume/system",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"main": "./src/index.ts",
|
||||
164
packages/system/src/account.ts
Normal file
164
packages/system/src/account.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import { Metadata } from "@lume/types";
|
||||
import { commands } from "./commands";
|
||||
|
||||
export class NostrAccount {
|
||||
static async getAccounts() {
|
||||
const query = await commands.getAccounts();
|
||||
|
||||
if (query.status === "ok") {
|
||||
const accounts = query.data
|
||||
.split(/\s+/)
|
||||
.filter((v) => v.startsWith("npub1"));
|
||||
|
||||
return [...new Set(accounts)];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
static async loadAccount(npub: string) {
|
||||
const query = await commands.loadAccount(npub);
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
}
|
||||
|
||||
static async createAccount() {
|
||||
const query = await commands.createAccount();
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
}
|
||||
|
||||
static async createProfile(profile: Metadata) {
|
||||
const query = await commands.createProfile(
|
||||
profile.name || "",
|
||||
profile.display_name || "",
|
||||
profile.about || "",
|
||||
profile.picture || "",
|
||||
profile.banner || "",
|
||||
profile.nip05 || "",
|
||||
profile.lud16 || "",
|
||||
profile.website || "",
|
||||
);
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
}
|
||||
|
||||
static async saveAccount(nsec: string, password = "") {
|
||||
const query = await commands.saveAccount(nsec, password);
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
}
|
||||
|
||||
static async connectRemoteAccount(uri: string) {
|
||||
const remoteKey = uri.replace("bunker://", "").split("?")[0];
|
||||
const npub = await commands.toNpub(remoteKey);
|
||||
|
||||
if (npub.status === "ok") {
|
||||
const connect = await commands.nostrConnect(npub.data, uri);
|
||||
|
||||
if (connect.status === "ok") {
|
||||
return connect.data;
|
||||
} else {
|
||||
throw new Error(connect.error);
|
||||
}
|
||||
} else {
|
||||
throw new Error(npub.error);
|
||||
}
|
||||
}
|
||||
|
||||
static async setContactList(pubkeys: string[]) {
|
||||
const query = await commands.setContactList(pubkeys);
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
}
|
||||
|
||||
static async setWallet(uri: string) {
|
||||
const query = await commands.setNwc(uri);
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
}
|
||||
|
||||
static async getProfile() {
|
||||
const query = await commands.getCurrentUserProfile();
|
||||
|
||||
if (query.status === "ok") {
|
||||
return JSON.parse(query.data) as Metadata;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static async getBalance() {
|
||||
const query = await commands.getBalance();
|
||||
|
||||
if (query.status === "ok") {
|
||||
return parseInt(query.data);
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static async getContactList() {
|
||||
const query = await commands.getContactList();
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
static async follow(pubkey: string, alias?: string) {
|
||||
const query = await commands.follow(pubkey, alias);
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
}
|
||||
|
||||
static async unfollow(pubkey: string) {
|
||||
const query = await commands.unfollow(pubkey);
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
}
|
||||
|
||||
static async f2f(npub: string) {
|
||||
const query = await commands.friendToFriend(npub);
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
609
packages/system/src/commands.ts
Normal file
609
packages/system/src/commands.ts
Normal file
@@ -0,0 +1,609 @@
|
||||
// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually.
|
||||
|
||||
export const commands = {
|
||||
async getRelays(): Promise<Result<Relays, null>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_relays") };
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async connectRelay(relay: string): Promise<Result<boolean, null>> {
|
||||
try {
|
||||
return {
|
||||
status: "ok",
|
||||
data: await TAURI_INVOKE("connect_relay", { relay }),
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async removeRelay(relay: string): Promise<Result<boolean, null>> {
|
||||
try {
|
||||
return {
|
||||
status: "ok",
|
||||
data: await TAURI_INVOKE("remove_relay", { relay }),
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getAccounts(): Promise<Result<string, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_accounts") };
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async createAccount(): Promise<Result<Account, null>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("create_account") };
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async saveAccount(
|
||||
nsec: string,
|
||||
password: string,
|
||||
): Promise<Result<string, string>> {
|
||||
try {
|
||||
return {
|
||||
status: "ok",
|
||||
data: await TAURI_INVOKE("save_account", { nsec, password }),
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getEncryptedKey(
|
||||
npub: string,
|
||||
password: string,
|
||||
): Promise<Result<string, string>> {
|
||||
try {
|
||||
return {
|
||||
status: "ok",
|
||||
data: await TAURI_INVOKE("get_encrypted_key", { npub, password }),
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async nostrConnect(
|
||||
npub: string,
|
||||
uri: string,
|
||||
): Promise<Result<string, string>> {
|
||||
try {
|
||||
return {
|
||||
status: "ok",
|
||||
data: await TAURI_INVOKE("nostr_connect", { npub, uri }),
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async loadAccount(npub: string): Promise<Result<boolean, string>> {
|
||||
try {
|
||||
return {
|
||||
status: "ok",
|
||||
data: await TAURI_INVOKE("load_account", { npub }),
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async eventToBech32(
|
||||
id: string,
|
||||
relays: string[],
|
||||
): Promise<Result<string, null>> {
|
||||
try {
|
||||
return {
|
||||
status: "ok",
|
||||
data: await TAURI_INVOKE("event_to_bech32", { id, relays }),
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async userToBech32(
|
||||
key: string,
|
||||
relays: string[],
|
||||
): Promise<Result<string, null>> {
|
||||
try {
|
||||
return {
|
||||
status: "ok",
|
||||
data: await TAURI_INVOKE("user_to_bech32", { key, relays }),
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async toNpub(hex: string): Promise<Result<string, null>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("to_npub", { hex }) };
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async verifyNip05(
|
||||
key: string,
|
||||
nip05: string,
|
||||
): Promise<Result<boolean, string>> {
|
||||
try {
|
||||
return {
|
||||
status: "ok",
|
||||
data: await TAURI_INVOKE("verify_nip05", { key, nip05 }),
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async runNotification(accounts: string[]): Promise<Result<null, null>> {
|
||||
try {
|
||||
return {
|
||||
status: "ok",
|
||||
data: await TAURI_INVOKE("run_notification", { accounts }),
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getActivities(
|
||||
account: string,
|
||||
kind: string,
|
||||
): Promise<Result<string[], string>> {
|
||||
try {
|
||||
return {
|
||||
status: "ok",
|
||||
data: await TAURI_INVOKE("get_activities", { account, kind }),
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getCurrentUserProfile(): Promise<Result<string, string>> {
|
||||
try {
|
||||
return {
|
||||
status: "ok",
|
||||
data: await TAURI_INVOKE("get_current_user_profile"),
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getProfile(id: string): Promise<Result<string, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_profile", { id }) };
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getContactList(): Promise<Result<string[], string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_contact_list") };
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async setContactList(pubkeys: string[]): Promise<Result<boolean, string>> {
|
||||
try {
|
||||
return {
|
||||
status: "ok",
|
||||
data: await TAURI_INVOKE("set_contact_list", { pubkeys }),
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async createProfile(
|
||||
name: string,
|
||||
displayName: string,
|
||||
about: string,
|
||||
picture: string,
|
||||
banner: string,
|
||||
nip05: string,
|
||||
lud16: string,
|
||||
website: string,
|
||||
): Promise<Result<string, string>> {
|
||||
try {
|
||||
return {
|
||||
status: "ok",
|
||||
data: await TAURI_INVOKE("create_profile", {
|
||||
name,
|
||||
displayName,
|
||||
about,
|
||||
picture,
|
||||
banner,
|
||||
nip05,
|
||||
lud16,
|
||||
website,
|
||||
}),
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async follow(
|
||||
id: string,
|
||||
alias: string | null,
|
||||
): Promise<Result<string, string>> {
|
||||
try {
|
||||
return {
|
||||
status: "ok",
|
||||
data: await TAURI_INVOKE("follow", { id, alias }),
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async unfollow(id: string): Promise<Result<string, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("unfollow", { id }) };
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getNstore(key: string): Promise<Result<string, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_nstore", { key }) };
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async setNstore(
|
||||
key: string,
|
||||
content: string,
|
||||
): Promise<Result<string, string>> {
|
||||
try {
|
||||
return {
|
||||
status: "ok",
|
||||
data: await TAURI_INVOKE("set_nstore", { key, content }),
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async setNwc(uri: string): Promise<Result<boolean, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("set_nwc", { uri }) };
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async loadNwc(): Promise<Result<boolean, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("load_nwc") };
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getBalance(): Promise<Result<string, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_balance") };
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async zapProfile(
|
||||
id: string,
|
||||
amount: string,
|
||||
message: string,
|
||||
): Promise<Result<boolean, string>> {
|
||||
try {
|
||||
return {
|
||||
status: "ok",
|
||||
data: await TAURI_INVOKE("zap_profile", { id, amount, message }),
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async zapEvent(
|
||||
id: string,
|
||||
amount: string,
|
||||
message: string,
|
||||
): Promise<Result<boolean, string>> {
|
||||
try {
|
||||
return {
|
||||
status: "ok",
|
||||
data: await TAURI_INVOKE("zap_event", { id, amount, message }),
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async friendToFriend(npub: string): Promise<Result<boolean, string>> {
|
||||
try {
|
||||
return {
|
||||
status: "ok",
|
||||
data: await TAURI_INVOKE("friend_to_friend", { npub }),
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getEvent(id: string): Promise<Result<string, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_event", { id }) };
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getReplies(id: string): Promise<Result<string[], string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_replies", { id }) };
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getEventsBy(
|
||||
publicKey: string,
|
||||
asOf: string | null,
|
||||
): Promise<Result<string[], string>> {
|
||||
try {
|
||||
return {
|
||||
status: "ok",
|
||||
data: await TAURI_INVOKE("get_events_by", { publicKey, asOf }),
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getLocalEvents(
|
||||
pubkeys: string[],
|
||||
until: string | null,
|
||||
): Promise<Result<string[], string>> {
|
||||
try {
|
||||
return {
|
||||
status: "ok",
|
||||
data: await TAURI_INVOKE("get_local_events", { pubkeys, until }),
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getGlobalEvents(
|
||||
until: string | null,
|
||||
): Promise<Result<string[], string>> {
|
||||
try {
|
||||
return {
|
||||
status: "ok",
|
||||
data: await TAURI_INVOKE("get_global_events", { until }),
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getHashtagEvents(
|
||||
hashtags: string[],
|
||||
until: string | null,
|
||||
): Promise<Result<string[], string>> {
|
||||
try {
|
||||
return {
|
||||
status: "ok",
|
||||
data: await TAURI_INVOKE("get_hashtag_events", { hashtags, until }),
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async publish(
|
||||
content: string,
|
||||
tags: string[][],
|
||||
): Promise<Result<string, string>> {
|
||||
try {
|
||||
return {
|
||||
status: "ok",
|
||||
data: await TAURI_INVOKE("publish", { content, tags }),
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async repost(raw: string): Promise<Result<string, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("repost", { raw }) };
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async showInFolder(path: string): Promise<void> {
|
||||
await TAURI_INVOKE("show_in_folder", { path });
|
||||
},
|
||||
async createColumn(
|
||||
label: string,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
url: string,
|
||||
): Promise<Result<string, string>> {
|
||||
try {
|
||||
return {
|
||||
status: "ok",
|
||||
data: await TAURI_INVOKE("create_column", {
|
||||
label,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
url,
|
||||
}),
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async closeColumn(label: string): Promise<Result<boolean, null>> {
|
||||
try {
|
||||
return {
|
||||
status: "ok",
|
||||
data: await TAURI_INVOKE("close_column", { label }),
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async repositionColumn(
|
||||
label: string,
|
||||
x: number,
|
||||
y: number,
|
||||
): Promise<Result<null, string>> {
|
||||
try {
|
||||
return {
|
||||
status: "ok",
|
||||
data: await TAURI_INVOKE("reposition_column", { label, x, y }),
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async resizeColumn(
|
||||
label: string,
|
||||
width: number,
|
||||
height: number,
|
||||
): Promise<Result<null, string>> {
|
||||
try {
|
||||
return {
|
||||
status: "ok",
|
||||
data: await TAURI_INVOKE("resize_column", { label, width, height }),
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async openWindow(
|
||||
label: string,
|
||||
title: string,
|
||||
url: string,
|
||||
width: number,
|
||||
height: number,
|
||||
): Promise<Result<null, string>> {
|
||||
try {
|
||||
return {
|
||||
status: "ok",
|
||||
data: await TAURI_INVOKE("open_window", {
|
||||
label,
|
||||
title,
|
||||
url,
|
||||
width,
|
||||
height,
|
||||
}),
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async setBadge(count: number): Promise<void> {
|
||||
await TAURI_INVOKE("set_badge", { count });
|
||||
},
|
||||
};
|
||||
|
||||
/** user-defined types **/
|
||||
|
||||
export type Account = { npub: string; nsec: string };
|
||||
export type Relays = {
|
||||
connected: string[];
|
||||
read: string[] | null;
|
||||
write: string[] | null;
|
||||
both: string[] | null;
|
||||
};
|
||||
|
||||
/** tauri-specta globals **/
|
||||
|
||||
import { invoke as TAURI_INVOKE } from "@tauri-apps/api/core";
|
||||
import * as TAURI_API_EVENT from "@tauri-apps/api/event";
|
||||
import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow";
|
||||
|
||||
type __EventObj__<T> = {
|
||||
listen: (
|
||||
cb: TAURI_API_EVENT.EventCallback<T>,
|
||||
) => ReturnType<typeof TAURI_API_EVENT.listen<T>>;
|
||||
once: (
|
||||
cb: TAURI_API_EVENT.EventCallback<T>,
|
||||
) => ReturnType<typeof TAURI_API_EVENT.once<T>>;
|
||||
emit: T extends null
|
||||
? (payload?: T) => ReturnType<typeof TAURI_API_EVENT.emit>
|
||||
: (payload: T) => ReturnType<typeof TAURI_API_EVENT.emit>;
|
||||
};
|
||||
|
||||
export type Result<T, E> =
|
||||
| { status: "ok"; data: T }
|
||||
| { status: "error"; error: E };
|
||||
|
||||
function __makeEvents__<T extends Record<string, any>>(
|
||||
mappings: Record<keyof T, string>,
|
||||
) {
|
||||
return new Proxy(
|
||||
{} as unknown as {
|
||||
[K in keyof T]: __EventObj__<T[K]> & {
|
||||
(handle: __WebviewWindow__): __EventObj__<T[K]>;
|
||||
};
|
||||
},
|
||||
{
|
||||
get: (_, event) => {
|
||||
const name = mappings[event as keyof T];
|
||||
|
||||
return new Proxy((() => {}) as any, {
|
||||
apply: (_, __, [window]: [__WebviewWindow__]) => ({
|
||||
listen: (arg: any) => window.listen(name, arg),
|
||||
once: (arg: any) => window.once(name, arg),
|
||||
emit: (arg: any) => window.emit(name, arg),
|
||||
}),
|
||||
get: (_, command: keyof __EventObj__<any>) => {
|
||||
switch (command) {
|
||||
case "listen":
|
||||
return (arg: any) => TAURI_API_EVENT.listen(name, arg);
|
||||
case "once":
|
||||
return (arg: any) => TAURI_API_EVENT.once(name, arg);
|
||||
case "emit":
|
||||
return (arg: any) => TAURI_API_EVENT.emit(name, arg);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
28
packages/system/src/dedup.ts
Normal file
28
packages/system/src/dedup.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { NostrEvent } from "@lume/types";
|
||||
|
||||
export function dedupEvents(nostrEvents: NostrEvent[], nsfw: boolean = false) {
|
||||
const seens = new Set<string>();
|
||||
const events = nostrEvents.filter((event) => {
|
||||
const eTags = event.tags.filter((el) => el[0] === "e");
|
||||
const ids = eTags.map((item) => item[1]);
|
||||
const isDup = ids.some((id) => seens.has(id));
|
||||
|
||||
// Add found ids to seen list
|
||||
for (const id of ids) {
|
||||
seens.add(id);
|
||||
}
|
||||
|
||||
// Filter NSFW event
|
||||
if (nsfw) {
|
||||
const wTags = event.tags.filter((t) => t[0] === "content-warning");
|
||||
const isLewd = wTags.length > 0;
|
||||
|
||||
return !isDup && !isLewd;
|
||||
}
|
||||
|
||||
// Filter duplicate event
|
||||
return !isDup;
|
||||
});
|
||||
|
||||
return events;
|
||||
}
|
||||
200
packages/system/src/event.ts
Normal file
200
packages/system/src/event.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import { EventWithReplies, Kind, NostrEvent } from "@lume/types";
|
||||
import { commands } from "./commands";
|
||||
import { generateContentTags } from "@lume/utils";
|
||||
|
||||
export class LumeEvent {
|
||||
public id: string;
|
||||
public pubkey: string;
|
||||
public created_at: number;
|
||||
public kind: Kind;
|
||||
public tags: string[][];
|
||||
public content: string;
|
||||
public sig: string;
|
||||
public relay?: string;
|
||||
#raw: NostrEvent;
|
||||
|
||||
constructor(event: NostrEvent) {
|
||||
this.#raw = event;
|
||||
Object.assign(this, event);
|
||||
}
|
||||
|
||||
get mentions() {
|
||||
return this.tags.filter((tag) => tag[0] === "p").map((tag) => tag[1]);
|
||||
}
|
||||
|
||||
static getEventThread(tags: string[][]) {
|
||||
let root: string = null;
|
||||
let reply: string = null;
|
||||
|
||||
// Get all event references from tags, ignore mention
|
||||
const events = tags.filter((el) => el[0] === "e" && el[3] !== "mention");
|
||||
|
||||
/*
|
||||
if (gossip) {
|
||||
const relays = tags.filter((el) => el[0] === "e" && el[2]?.length);
|
||||
|
||||
if (relays.length >= 1) {
|
||||
for (const relay of relays) {
|
||||
if (relay[2]?.length) this.add_relay(relay[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
if (events.length === 1) {
|
||||
root = events[0][1];
|
||||
}
|
||||
|
||||
if (events.length > 1) {
|
||||
root = events.find((el) => el[3] === "root")?.[1] ?? events[0][1];
|
||||
reply = events.find((el) => el[3] === "reply")?.[1] ?? events[1][1];
|
||||
}
|
||||
|
||||
// Fix some rare case when root === reply
|
||||
if (root && reply && root === reply) {
|
||||
reply = null;
|
||||
}
|
||||
|
||||
return {
|
||||
root,
|
||||
reply,
|
||||
};
|
||||
}
|
||||
|
||||
static async getReplies(id: string) {
|
||||
const query = await commands.getReplies(id);
|
||||
|
||||
if (query.status === "ok") {
|
||||
const events = query.data.map(
|
||||
(item) => JSON.parse(item) as EventWithReplies,
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return events.filter((ev) => !replies.has(ev.id));
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
}
|
||||
|
||||
static async publish(
|
||||
content: string,
|
||||
reply_to?: string,
|
||||
quote?: boolean,
|
||||
nsfw?: boolean,
|
||||
) {
|
||||
const g = await generateContentTags(content);
|
||||
|
||||
const eventContent = g.content;
|
||||
const eventTags = g.tags;
|
||||
|
||||
if (reply_to) {
|
||||
const queryReply = await commands.getEvent(reply_to);
|
||||
|
||||
if (queryReply.status === "ok") {
|
||||
const replyEvent = JSON.parse(queryReply.data) as NostrEvent;
|
||||
const relayHint =
|
||||
replyEvent.tags.find((ev) => ev[0] === "e")?.[0][2] ?? "";
|
||||
|
||||
if (quote) {
|
||||
eventTags.push(["e", replyEvent.id, relayHint, "mention"]);
|
||||
eventTags.push(["q", replyEvent.id]);
|
||||
} else {
|
||||
const rootEvent = replyEvent.tags.find((ev) => ev[3] === "root");
|
||||
|
||||
if (rootEvent) {
|
||||
eventTags.push([
|
||||
"e",
|
||||
rootEvent[1],
|
||||
rootEvent[2] || relayHint,
|
||||
"root",
|
||||
]);
|
||||
}
|
||||
|
||||
eventTags.push(["e", replyEvent.id, relayHint, "reply"]);
|
||||
eventTags.push(["p", replyEvent.pubkey]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (nsfw) {
|
||||
eventTags.push(["L", "content-warning"]);
|
||||
eventTags.push(["l", "reason", "content-warning"]);
|
||||
eventTags.push(["content-warning", "nsfw"]);
|
||||
}
|
||||
|
||||
const query = await commands.publish(eventContent, eventTags);
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
}
|
||||
|
||||
static async zap(id: string, amount: number, message: string) {
|
||||
const query = await commands.zapEvent(id, amount.toString(), message);
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
}
|
||||
|
||||
public async idAsBech32() {
|
||||
const query = await commands.eventToBech32(this.id, []);
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
}
|
||||
|
||||
public async pubkeyAsBech32() {
|
||||
const query = await commands.userToBech32(this.pubkey, []);
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
}
|
||||
|
||||
public async repost() {
|
||||
const query = await commands.repost(JSON.stringify(this.#raw));
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Event } from "@lume/types";
|
||||
import type { Event, NostrEvent } from "@lume/types";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
@@ -12,7 +12,7 @@ export function useEvent(id: string) {
|
||||
.split("'")[0]
|
||||
.split(".")[0];
|
||||
const cmd: string = await invoke("get_event", { id: eventId });
|
||||
const event: Event = JSON.parse(cmd);
|
||||
const event: NostrEvent = JSON.parse(cmd);
|
||||
return event;
|
||||
} catch (e) {
|
||||
throw new Error(e);
|
||||
53
packages/system/src/hooks/useInfiniteEvents.tsx
Normal file
53
packages/system/src/hooks/useInfiniteEvents.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import { commands } from "../commands";
|
||||
import { dedupEvents } from "../dedup";
|
||||
import { NostrEvent } from "@lume/types";
|
||||
|
||||
export function useInfiniteEvents(
|
||||
contacts: string[],
|
||||
label: string,
|
||||
account: string,
|
||||
nsfw?: boolean,
|
||||
) {
|
||||
const pubkeys = contacts;
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
isFetching,
|
||||
isFetchingNextPage,
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
} = useInfiniteQuery({
|
||||
queryKey: [label, account],
|
||||
initialPageParam: 0,
|
||||
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
||||
try {
|
||||
const until: string = pageParam > 0 ? pageParam.toString() : undefined;
|
||||
const query = await commands.getLocalEvents(pubkeys, until);
|
||||
|
||||
if (query.status === "ok") {
|
||||
const nostrEvents = query.data as unknown as NostrEvent[];
|
||||
const events = dedupEvents(nostrEvents, nsfw);
|
||||
|
||||
return events;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error(e);
|
||||
}
|
||||
},
|
||||
getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1,
|
||||
select: (data) => data?.pages.flatMap((page) => page),
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
return {
|
||||
data,
|
||||
isLoading,
|
||||
isFetching,
|
||||
isFetchingNextPage,
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Metadata } from "@lume/types";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { commands } from "../commands";
|
||||
|
||||
export function useProfile(pubkey: string, embed?: string) {
|
||||
const {
|
||||
@@ -11,15 +11,16 @@ export function useProfile(pubkey: string, embed?: string) {
|
||||
queryKey: ["user", pubkey],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
if (embed) {
|
||||
const profile: Metadata = JSON.parse(embed);
|
||||
return profile;
|
||||
if (embed) return JSON.parse(embed) as Metadata;
|
||||
|
||||
const normalize = pubkey.replace("nostr:", "").replace(/[^\w\s]/gi, "");
|
||||
const query = await commands.getProfile(normalize);
|
||||
|
||||
if (query.status === "ok") {
|
||||
return JSON.parse(query.data) as Metadata;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
|
||||
const id = pubkey.replace("nostr:", "").replace(/[^\w\s]/gi, "");
|
||||
const cmd: Metadata = await invoke("get_profile", { id });
|
||||
|
||||
return cmd;
|
||||
} catch (e) {
|
||||
throw new Error(e);
|
||||
}
|
||||
8
packages/system/src/index.ts
Normal file
8
packages/system/src/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export * from "./event";
|
||||
export * from "./account";
|
||||
export * from "./query";
|
||||
export * from "./window";
|
||||
export * from "./commands";
|
||||
export * from "./hooks/useEvent";
|
||||
export * from "./hooks/useInfiniteEvents";
|
||||
export * from "./hooks/useProfile";
|
||||
303
packages/system/src/query.ts
Normal file
303
packages/system/src/query.ts
Normal file
@@ -0,0 +1,303 @@
|
||||
import { LumeColumn, Metadata, NostrEvent, Settings } from "@lume/types";
|
||||
import { commands } from "./commands";
|
||||
import { resolveResource } from "@tauri-apps/api/path";
|
||||
import { readFile, readTextFile } from "@tauri-apps/plugin-fs";
|
||||
import { isPermissionGranted } from "@tauri-apps/plugin-notification";
|
||||
import { open } from "@tauri-apps/plugin-dialog";
|
||||
import { dedupEvents } from "./dedup";
|
||||
|
||||
enum NSTORE_KEYS {
|
||||
settings = "lume_user_settings",
|
||||
columns = "lume_user_columns",
|
||||
}
|
||||
|
||||
export class NostrQuery {
|
||||
static async upload(filePath?: string) {
|
||||
const allowExts = [
|
||||
"png",
|
||||
"jpeg",
|
||||
"jpg",
|
||||
"gif",
|
||||
"mp4",
|
||||
"mp3",
|
||||
"webm",
|
||||
"mkv",
|
||||
"avi",
|
||||
"mov",
|
||||
];
|
||||
|
||||
const selected =
|
||||
filePath ||
|
||||
(
|
||||
await open({
|
||||
multiple: false,
|
||||
filters: [
|
||||
{
|
||||
name: "Media",
|
||||
extensions: allowExts,
|
||||
},
|
||||
],
|
||||
})
|
||||
).path;
|
||||
|
||||
// User cancelled action
|
||||
if (!selected) return null;
|
||||
|
||||
try {
|
||||
const file = await readFile(selected);
|
||||
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;
|
||||
} catch (e) {
|
||||
throw new Error(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
static async getProfile(pubkey: string) {
|
||||
const normalize = pubkey.replace("nostr:", "").replace(/[^\w\s]/gi, "");
|
||||
const query = await commands.getProfile(normalize);
|
||||
|
||||
if (query.status === "ok") {
|
||||
const profile: Metadata = JSON.parse(query.data);
|
||||
return profile;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static async getEvent(id: string) {
|
||||
const normalize: string = id.replace("nostr:", "").replace(/[^\w\s]/gi, "");
|
||||
const query = await commands.getEvent(normalize);
|
||||
|
||||
if (query.status === "ok") {
|
||||
const event: NostrEvent = JSON.parse(query.data);
|
||||
return event;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static async getUserEvents(pubkey: string, asOf?: number) {
|
||||
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
|
||||
const query = await commands.getEventsBy(pubkey, until);
|
||||
|
||||
if (query.status === "ok") {
|
||||
const events = query.data.map((item) => JSON.parse(item) as NostrEvent);
|
||||
return events;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
static async getUserActivities(
|
||||
account: string,
|
||||
kind: "1" | "6" | "9735" = "1",
|
||||
) {
|
||||
const query = await commands.getActivities(account, kind);
|
||||
|
||||
if (query.status === "ok") {
|
||||
const events = query.data.map((item) => JSON.parse(item) as NostrEvent);
|
||||
return events;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
static async getLocalEvents(pubkeys: string[], asOf?: number) {
|
||||
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
|
||||
const query = await commands.getLocalEvents(pubkeys, until);
|
||||
|
||||
if (query.status === "ok") {
|
||||
const events = query.data.map((item) => JSON.parse(item) as NostrEvent);
|
||||
const dedup = dedupEvents(events);
|
||||
|
||||
return dedup;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
static async getGlobalEvents(asOf?: number) {
|
||||
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
|
||||
const query = await commands.getGlobalEvents(until);
|
||||
|
||||
if (query.status === "ok") {
|
||||
const events = query.data.map((item) => JSON.parse(item) as NostrEvent);
|
||||
const dedup = dedupEvents(events);
|
||||
|
||||
return dedup;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
static async getHashtagEvents(hashtags: string[], asOf?: number) {
|
||||
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
|
||||
const nostrTags = hashtags.map((tag) => tag.replace("#", ""));
|
||||
const query = await commands.getHashtagEvents(nostrTags, until);
|
||||
|
||||
if (query.status === "ok") {
|
||||
const events = query.data.map((item) => JSON.parse(item) as NostrEvent);
|
||||
const dedup = dedupEvents(events);
|
||||
|
||||
return dedup;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
static async verifyNip05(pubkey: string, nip05?: string) {
|
||||
if (!nip05) return false;
|
||||
|
||||
const query = await commands.verifyNip05(pubkey, nip05);
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static async getNstore(key: string) {
|
||||
const query = await commands.getNstore(key);
|
||||
|
||||
if (query.status === "ok") {
|
||||
const data: string | string[] = query.data
|
||||
? JSON.parse(query.data)
|
||||
: null;
|
||||
return data;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static async setNstore(key: string, value: string) {
|
||||
const query = await commands.setNstore(key, value);
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
}
|
||||
|
||||
static async getSettings() {
|
||||
const query = await commands.getNstore(NSTORE_KEYS.settings);
|
||||
|
||||
if (query.status === "ok") {
|
||||
const settings: Settings = query.data ? JSON.parse(query.data) : null;
|
||||
const isGranted = await isPermissionGranted();
|
||||
|
||||
return { ...settings, notification: isGranted };
|
||||
} else {
|
||||
const initial: Settings = {
|
||||
autoUpdate: false,
|
||||
enhancedPrivacy: false,
|
||||
notification: false,
|
||||
zap: false,
|
||||
nsfw: false,
|
||||
};
|
||||
|
||||
return initial;
|
||||
}
|
||||
}
|
||||
|
||||
static async setSettings(settings: Settings) {
|
||||
const query = await commands.setNstore(
|
||||
NSTORE_KEYS.settings,
|
||||
JSON.stringify(settings),
|
||||
);
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
}
|
||||
|
||||
static async getColumns() {
|
||||
const query = await commands.getNstore(NSTORE_KEYS.columns);
|
||||
|
||||
if (query.status === "ok") {
|
||||
const columns: LumeColumn[] = query.data ? JSON.parse(query.data) : [];
|
||||
|
||||
if (columns.length < 1) {
|
||||
const systemPath = "resources/system_columns.json";
|
||||
const resourcePath = await resolveResource(systemPath);
|
||||
const resourceFile = await readTextFile(resourcePath);
|
||||
const systemColumns: LumeColumn[] = JSON.parse(resourceFile);
|
||||
|
||||
return systemColumns;
|
||||
}
|
||||
|
||||
return columns;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
static async setColumns(columns: LumeColumn[]) {
|
||||
const query = await commands.setNstore(
|
||||
NSTORE_KEYS.columns,
|
||||
JSON.stringify(columns),
|
||||
);
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
}
|
||||
|
||||
static async getRelays() {
|
||||
const query = await commands.getRelays();
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
}
|
||||
|
||||
static async connectRelay(url: string) {
|
||||
const relayUrl = new URL(url);
|
||||
|
||||
if (relayUrl.protocol === "wss:" || relayUrl.protocol === "ws:") {
|
||||
const query = await commands.connectRelay(relayUrl.toString());
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static async removeRelay(url: string) {
|
||||
const relayUrl = new URL(url);
|
||||
|
||||
if (relayUrl.protocol === "wss:" || relayUrl.protocol === "ws:") {
|
||||
const query = await commands.removeRelay(relayUrl.toString());
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
140
packages/system/src/window.ts
Normal file
140
packages/system/src/window.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { NostrEvent } from "@lume/types";
|
||||
import { commands } from "./commands";
|
||||
|
||||
export class LumeWindow {
|
||||
static async openEvent(event: NostrEvent) {
|
||||
const eTags = event.tags.filter((tag) => tag[0] === "e" || tag[0] === "q");
|
||||
const root: string =
|
||||
eTags.find((el) => el[3] === "root")?.[1] ?? eTags[0]?.[1];
|
||||
const reply: string =
|
||||
eTags.find((el) => el[3] === "reply")?.[1] ?? eTags[1]?.[1];
|
||||
|
||||
const label = `event-${event.id}`;
|
||||
const url = `/events/${root ?? reply ?? event.id}`;
|
||||
|
||||
const query = await commands.openWindow(label, "Thread", url, 500, 800);
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
}
|
||||
|
||||
static async openProfile(pubkey: string) {
|
||||
const label = `user-${pubkey}`;
|
||||
const query = await commands.openWindow(
|
||||
label,
|
||||
"Profile",
|
||||
`/users/${pubkey}`,
|
||||
500,
|
||||
800,
|
||||
);
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
}
|
||||
|
||||
static async openEditor(reply_to?: string, quote = false) {
|
||||
let url: string;
|
||||
|
||||
if (reply_to) {
|
||||
url = `/editor?reply_to=${reply_to}"e=${quote}`;
|
||||
} else {
|
||||
url = "/editor";
|
||||
}
|
||||
|
||||
const label = `editor-${reply_to ? reply_to : 0}`;
|
||||
const query = await commands.openWindow(label, "Editor", url, 560, 340);
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
}
|
||||
|
||||
static async openZap(id: string, pubkey: string) {
|
||||
const nwc = await commands.loadNwc();
|
||||
|
||||
if (nwc.status === "ok") {
|
||||
const status = nwc.data;
|
||||
|
||||
if (!status) {
|
||||
const label = "nwc";
|
||||
await commands.openWindow(
|
||||
label,
|
||||
"Nostr Wallet Connect",
|
||||
"/nwc",
|
||||
400,
|
||||
600,
|
||||
);
|
||||
} else {
|
||||
const label = `zap-${id}`;
|
||||
await commands.openWindow(
|
||||
label,
|
||||
"Zap",
|
||||
`/zap/${id}?pubkey=${pubkey}`,
|
||||
400,
|
||||
500,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw new Error(nwc.error);
|
||||
}
|
||||
}
|
||||
|
||||
static async openSettings() {
|
||||
const label = "settings";
|
||||
const query = await commands.openWindow(
|
||||
label,
|
||||
"Settings",
|
||||
"/settings",
|
||||
800,
|
||||
500,
|
||||
);
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
}
|
||||
|
||||
static async openSearch() {
|
||||
const label = "search";
|
||||
const query = await commands.openWindow(
|
||||
label,
|
||||
"Search",
|
||||
"/search",
|
||||
400,
|
||||
600,
|
||||
);
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
}
|
||||
|
||||
static async openActivity(account: string) {
|
||||
const label = "activity";
|
||||
const query = await commands.openWindow(
|
||||
label,
|
||||
"Activity",
|
||||
`/activity/${account}/texts`,
|
||||
400,
|
||||
600,
|
||||
);
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
7
packages/types/index.d.ts
vendored
7
packages/types/index.d.ts
vendored
@@ -26,7 +26,7 @@ export enum Kind {
|
||||
// #TODO: Add all nostr kinds
|
||||
}
|
||||
|
||||
export interface Event {
|
||||
export interface NostrEvent {
|
||||
id: string;
|
||||
pubkey: string;
|
||||
created_at: number;
|
||||
@@ -34,11 +34,10 @@ export interface Event {
|
||||
tags: string[][];
|
||||
content: string;
|
||||
sig: string;
|
||||
relay?: string;
|
||||
}
|
||||
|
||||
export interface EventWithReplies extends Event {
|
||||
replies: Array<Event>;
|
||||
export interface EventWithReplies extends NostrEvent {
|
||||
replies: Array<NostrEvent>;
|
||||
}
|
||||
|
||||
export interface Metadata {
|
||||
|
||||
@@ -4,43 +4,11 @@
|
||||
"private": true,
|
||||
"main": "./src/index.ts",
|
||||
"dependencies": {
|
||||
"@getalby/sdk": "^3.5.1",
|
||||
"@lume/ark": "workspace:^",
|
||||
"@lume/icons": "workspace:^",
|
||||
"@lume/utils": "workspace:^",
|
||||
"@radix-ui/react-accordion": "^1.1.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.0.5",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@radix-ui/react-collapsible": "^1.0.3",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@radix-ui/react-hover-card": "^1.0.7",
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@tanstack/react-query": "^5.36.0",
|
||||
"@tanstack/react-router": "^1.32.5",
|
||||
"framer-motion": "^11.2.0",
|
||||
"get-urls": "^12.1.0",
|
||||
"media-chrome": "^3.2.2",
|
||||
"minidenticons": "^4.2.1",
|
||||
"nanoid": "^5.0.7",
|
||||
"qrcode.react": "^3.1.0",
|
||||
"re-resizable": "^6.9.16",
|
||||
"react": "^18.3.1",
|
||||
"react-currency-input-field": "^3.8.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.51.4",
|
||||
"react-hotkeys-hook": "^4.5.0",
|
||||
"react-i18next": "^14.1.1",
|
||||
"react-snap-carousel": "^0.4.0",
|
||||
"react-string-replace": "^1.1.1",
|
||||
"slate": "^0.103.0",
|
||||
"slate-react": "^0.102.0",
|
||||
"sonner": "^1.4.41",
|
||||
"string-strip-html": "^13.4.8",
|
||||
"uqr": "^0.1.2",
|
||||
"use-debounce": "^10.0.0",
|
||||
"virtua": "^0.31.0"
|
||||
"react-snap-carousel": "^0.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lume/tailwindcss": "workspace:^",
|
||||
|
||||
Reference in New Issue
Block a user