From afb7c87fa30cb8d651999304a8577d3766351427 Mon Sep 17 00:00:00 2001 From: reya <123083837+reyamir@users.noreply.github.com> Date: Tue, 7 May 2024 08:29:58 +0700 Subject: [PATCH] feat: add bell --- apps/desktop2/src/routes/$account.home.tsx | 292 ++-- apps/desktop2/src/routes/$account.tsx | 267 +-- apps/desktop2/src/routes/__root.tsx | 50 +- apps/desktop2/src/routes/index.tsx | 212 ++- packages/ark/src/ark.ts | 1759 ++++++++++---------- packages/types/index.d.ts | 227 +-- src-tauri/src/nostr/metadata.rs | 10 +- 7 files changed, 1436 insertions(+), 1381 deletions(-) diff --git a/apps/desktop2/src/routes/$account.home.tsx b/apps/desktop2/src/routes/$account.home.tsx index 576b74e7..57b93537 100644 --- a/apps/desktop2/src/routes/$account.home.tsx +++ b/apps/desktop2/src/routes/$account.home.tsx @@ -5,7 +5,7 @@ import type { EventColumns, LumeColumn } from "@lume/types"; import { createFileRoute } from "@tanstack/react-router"; import { listen } from "@tauri-apps/api/event"; import { resolveResource } from "@tauri-apps/api/path"; -import { getCurrent } from "@tauri-apps/api/webviewWindow"; +import { getCurrent } from "@tauri-apps/api/window"; import { readTextFile } from "@tauri-apps/plugin-fs"; import { nanoid } from "nanoid"; import { useEffect, useRef, useState } from "react"; @@ -13,173 +13,179 @@ import { useDebouncedCallback } from "use-debounce"; import { VList, type VListHandle } from "virtua"; export const Route = createFileRoute("/$account/home")({ - beforeLoad: async ({ context }) => { - const ark = context.ark; - const resourcePath = await resolveResource("resources/system_columns.json"); - const systemColumns: LumeColumn[] = JSON.parse( - await readTextFile(resourcePath), - ); - const userColumns = await ark.get_columns(); + beforeLoad: async ({ context }) => { + try { + const ark = context.ark; + const resourcePath = await resolveResource( + "resources/system_columns.json", + ); + const systemColumns: LumeColumn[] = JSON.parse( + await readTextFile(resourcePath), + ); + const userColumns = await ark.get_columns(); - return { - storedColumns: !userColumns.length ? systemColumns : userColumns, - }; - }, - component: Screen, + return { + storedColumns: !userColumns.length ? systemColumns : userColumns, + }; + } catch (e) { + console.error(String(e)); + } + }, + component: Screen, }); function Screen() { - const vlistRef = useRef(null); + const vlistRef = useRef(null); - const { account } = Route.useParams(); - const { ark, storedColumns } = Route.useRouteContext(); + const { account } = Route.useParams(); + const { ark, storedColumns } = Route.useRouteContext(); - const [selectedIndex, setSelectedIndex] = useState(-1); - const [columns, setColumns] = useState(storedColumns); - const [isScroll, setIsScroll] = useState(false); - const [isResize, setIsResize] = useState(false); + const [selectedIndex, setSelectedIndex] = useState(-1); + const [columns, setColumns] = useState(storedColumns); + const [isScroll, setIsScroll] = useState(false); + const [isResize, setIsResize] = useState(false); - const goLeft = () => { - const prevIndex = Math.max(selectedIndex - 1, 0); - setSelectedIndex(prevIndex); - vlistRef.current.scrollToIndex(prevIndex, { - align: "center", - }); - }; + const goLeft = () => { + const prevIndex = Math.max(selectedIndex - 1, 0); + setSelectedIndex(prevIndex); + vlistRef.current.scrollToIndex(prevIndex, { + align: "center", + }); + }; - const goRight = () => { - const nextIndex = Math.min(selectedIndex + 1, columns.length - 1); - setSelectedIndex(nextIndex); - vlistRef.current.scrollToIndex(nextIndex, { - align: "center", - }); - }; + const goRight = () => { + const nextIndex = Math.min(selectedIndex + 1, columns.length - 1); + setSelectedIndex(nextIndex); + vlistRef.current.scrollToIndex(nextIndex, { + align: "center", + }); + }; - const add = useDebouncedCallback((column: LumeColumn) => { - // update col label - column.label = `${column.label}-${nanoid()}`; + const add = useDebouncedCallback((column: LumeColumn) => { + // update col label + column.label = `${column.label}-${nanoid()}`; - // create new cols - const cols = [...columns]; - const openColIndex = cols.findIndex((col) => col.label === "open"); - const newCols = [ - ...cols.slice(0, openColIndex), - column, - ...cols.slice(openColIndex), - ]; + // create new cols + const cols = [...columns]; + const openColIndex = cols.findIndex((col) => col.label === "open"); + const newCols = [ + ...cols.slice(0, openColIndex), + column, + ...cols.slice(openColIndex), + ]; - setColumns(newCols); - setSelectedIndex(newCols.length); - setIsScroll(true); + setColumns(newCols); + setSelectedIndex(newCols.length); + setIsScroll(true); - // scroll to the newest column - vlistRef.current.scrollToIndex(newCols.length - 1, { - align: "end", - }); - }, 150); + // scroll to the newest column + vlistRef.current.scrollToIndex(newCols.length - 1, { + align: "end", + }); + }, 150); - const remove = useDebouncedCallback((label: string) => { - const newCols = columns.filter((t) => t.label !== label); + const remove = useDebouncedCallback((label: string) => { + const newCols = columns.filter((t) => t.label !== label); - setColumns(newCols); - setSelectedIndex(newCols.length); - setIsScroll(true); + setColumns(newCols); + setSelectedIndex(newCols.length); + setIsScroll(true); - // scroll to the first column - vlistRef.current.scrollToIndex(newCols.length - 1, { - align: "start", - }); - }, 150); + // scroll to the first column + vlistRef.current.scrollToIndex(newCols.length - 1, { + align: "start", + }); + }, 150); - const updateName = useDebouncedCallback((label: string, title: string) => { - const currentColIndex = columns.findIndex((col) => col.label === label); + const updateName = useDebouncedCallback((label: string, title: string) => { + const currentColIndex = columns.findIndex((col) => col.label === label); - const updatedCol = Object.assign({}, columns[currentColIndex]); - updatedCol.name = title; + const updatedCol = Object.assign({}, columns[currentColIndex]); + updatedCol.name = title; - const newCols = columns.slice(); - newCols[currentColIndex] = updatedCol; + const newCols = columns.slice(); + newCols[currentColIndex] = updatedCol; - setColumns(newCols); - }, 150); + setColumns(newCols); + }, 150); - const startResize = useDebouncedCallback( - () => setIsResize((prev) => !prev), - 150, - ); + const startResize = useDebouncedCallback( + () => setIsResize((prev) => !prev), + 150, + ); - useEffect(() => { - // save state - ark.set_columns(columns); - }, [columns]); + useEffect(() => { + // save state + ark.set_columns(columns); + }, [columns]); - useEffect(() => { - let unlistenColEvent: Awaited> | undefined = - undefined; - let unlistenWindowResize: Awaited> | undefined = - undefined; + useEffect(() => { + let unlistenColEvent: Awaited> | undefined = + undefined; + let unlistenWindowResize: Awaited> | undefined = + undefined; - (async () => { - if (unlistenColEvent && unlistenWindowResize) return; + (async () => { + if (unlistenColEvent && unlistenWindowResize) return; - unlistenColEvent = await listen("columns", (data) => { - if (data.payload.type === "add") add(data.payload.column); - if (data.payload.type === "remove") remove(data.payload.label); - if (data.payload.type === "set_title") - updateName(data.payload.label, data.payload.title); - }); + unlistenColEvent = await listen("columns", (data) => { + if (data.payload.type === "add") add(data.payload.column); + if (data.payload.type === "remove") remove(data.payload.label); + if (data.payload.type === "set_title") + updateName(data.payload.label, data.payload.title); + }); - unlistenWindowResize = await getCurrent().listen("tauri://resize", () => { - startResize(); - }); - })(); + unlistenWindowResize = await getCurrent().listen("tauri://resize", () => { + startResize(); + }); + })(); - return () => { - if (unlistenColEvent) unlistenColEvent(); - if (unlistenWindowResize) unlistenWindowResize(); - }; - }, []); + return () => { + if (unlistenColEvent) unlistenColEvent(); + if (unlistenWindowResize) unlistenWindowResize(); + }; + }, []); - return ( -
- setIsScroll(true)} - onScrollEnd={() => setIsScroll(false)} - className="scrollbar-none h-full w-full overflow-x-auto focus:outline-none" - > - {columns.map((column) => ( - - ))} - - -
- - -
-
-
- ); + return ( +
+ setIsScroll(true)} + onScrollEnd={() => setIsScroll(false)} + className="scrollbar-none h-full w-full overflow-x-auto focus:outline-none" + > + {columns.map((column) => ( + + ))} + + +
+ + +
+
+
+ ); } diff --git a/apps/desktop2/src/routes/$account.tsx b/apps/desktop2/src/routes/$account.tsx index fcd713a4..c0cd34b6 100644 --- a/apps/desktop2/src/routes/$account.tsx +++ b/apps/desktop2/src/routes/$account.tsx @@ -1,118 +1,183 @@ import { BellIcon, ComposeFilledIcon, PlusIcon, SearchIcon } from "@lume/icons"; +import { Event, Kind } from "@lume/types"; import { User } from "@lume/ui"; -import { cn } from "@lume/utils"; +import { + cn, + decodeZapInvoice, + displayNpub, + sendNativeNotification, +} from "@lume/utils"; import { Outlet, createFileRoute } from "@tanstack/react-router"; +import { UnlistenFn } from "@tauri-apps/api/event"; +import { getCurrent } from "@tauri-apps/api/window"; import { useEffect, useState } from "react"; export const Route = createFileRoute("/$account")({ - component: Screen, + beforeLoad: async ({ context }) => { + const ark = context.ark; + const accounts = await ark.get_all_accounts(); + + return { accounts }; + }, + component: Screen, }); function Screen() { - const { account } = Route.useParams(); - const { ark, platform } = Route.useRouteContext(); + const { ark, platform } = Route.useRouteContext(); + const navigate = Route.useNavigate(); - const navigate = Route.useNavigate(); - - return ( -
-
-
- - -
-
- - - -
-
-
-
- -
-
- ); + return ( +
+
+
+ + +
+
+ + + +
+
+
+
+ +
+
+ ); } -export function Accounts() { - const navigate = Route.useNavigate(); - const { ark } = Route.useRouteContext(); - const { account } = Route.useParams(); +function Accounts() { + const navigate = Route.useNavigate(); + const { ark, accounts } = Route.useRouteContext(); + const { account } = Route.useParams(); - const [accounts, setAccounts] = useState([]); + const changeAccount = async (npub: string) => { + if (npub === account) return; - const changeAccount = async (npub: string) => { - if (npub === account) return; - const select = await ark.load_selected_account(npub); - if (select) - return navigate({ to: "/$account/home", params: { account: npub } }); - }; + const select = await ark.load_selected_account(npub); - useEffect(() => { - async function getAllAccounts() { - const data = await ark.get_all_accounts(); - if (data) setAccounts(data); - } + if (select) { + return navigate({ to: "/$account/home", params: { account: npub } }); + } + }; - getAllAccounts(); - }, []); - - return ( -
- {accounts.map((user) => ( - - ))} -
- ); + return ( +
+ {accounts.map((user) => ( + + ))} +
+ ); +} + +function Bell() { + const { ark } = Route.useRouteContext(); + const { account } = Route.useParams(); + + const [isRing, setIsRing] = useState(false); + + useEffect(() => { + let unlisten: UnlistenFn = undefined; + + async function listenNotify() { + unlisten = await getCurrent().listen( + "activity", + async (payload) => { + setIsRing(true); + + const event: Event = JSON.parse(payload.payload); + const user = await ark.get_profile(event.pubkey); + const userName = + user.display_name || user.name || displayNpub(event.pubkey, 16); + + switch (event.kind) { + case Kind.Text: { + sendNativeNotification("Mentioned you in a note", userName); + break; + } + case Kind.Repost: { + sendNativeNotification("Reposted your note", userName); + break; + } + case Kind.ZapReceipt: { + const amount = decodeZapInvoice(event.tags); + sendNativeNotification( + `Zapped ₿ ${amount.bitcoinFormatted}`, + userName, + ); + break; + } + default: + break; + } + }, + ); + } + + if (!unlisten) listenNotify(); + + return () => { + if (unlisten) unlisten(); + }; + }, []); + + return ( + + ); } diff --git a/apps/desktop2/src/routes/__root.tsx b/apps/desktop2/src/routes/__root.tsx index 5f173e0d..3bcc7c57 100644 --- a/apps/desktop2/src/routes/__root.tsx +++ b/apps/desktop2/src/routes/__root.tsx @@ -7,38 +7,38 @@ import type { Platform } from "@tauri-apps/plugin-os"; import type { Descendant } from "slate"; type EditorElement = { - type: string; - children: Descendant[]; - eventId?: string; + type: string; + children: Descendant[]; + eventId?: string; }; interface RouterContext { - // System - ark: Ark; - queryClient: QueryClient; - // App info - platform?: Platform; - locale?: string; - // Settings - settings?: Settings; - interests?: Interests; - // Profile - accounts?: Account[]; - profile?: Metadata; - // Editor - initialValue?: EditorElement[]; + // System + ark: Ark; + queryClient: QueryClient; + // App info + platform?: Platform; + locale?: string; + // Settings + settings?: Settings; + interests?: Interests; + // Profile + accounts?: string[]; + profile?: Metadata; + // Editor + initialValue?: EditorElement[]; } export const Route = createRootRouteWithContext()({ - component: () => , - pendingComponent: Pending, - wrapInSuspense: true, + component: () => , + pendingComponent: Pending, + wrapInSuspense: true, }); function Pending() { - return ( -
- -
- ); + return ( +
+ +
+ ); } diff --git a/apps/desktop2/src/routes/index.tsx b/apps/desktop2/src/routes/index.tsx index 204a4279..70e91a28 100644 --- a/apps/desktop2/src/routes/index.tsx +++ b/apps/desktop2/src/routes/index.tsx @@ -4,131 +4,115 @@ import { Link } from "@tanstack/react-router"; import { createFileRoute, redirect } from "@tanstack/react-router"; import { invoke } from "@tauri-apps/api/core"; import { useState } from "react"; +import { toast } from "sonner"; export const Route = createFileRoute("/")({ - beforeLoad: async ({ context }) => { - const ark = context.ark; - const accounts = await ark.get_all_accounts(); + beforeLoad: async ({ context }) => { + const ark = context.ark; + const accounts = await ark.get_all_accounts(); - // Run notification service - if (accounts.length > 0) { - await invoke("run_notification", { accounts }); - } + if (!accounts.length) { + throw redirect({ + to: "/landing", + replace: true, + }); + } - switch (accounts.length) { - // Guest account - case 0: - throw redirect({ - to: "/landing", - replace: true, - }); - // Only 1 account, skip account selection screen - case 1: { - const account = accounts[0]; - const loadedAccount = await ark.load_selected_account(account); + // Run notification service + await invoke("run_notification", { accounts }); - if (loadedAccount) { - throw redirect({ - to: "/$account/home", - params: { account }, - replace: true, - }); - } - - break; - } - // Account selection - default: - return { accounts }; - } - }, - component: Screen, + return { accounts }; + }, + component: Screen, }); function Screen() { - const navigate = Route.useNavigate(); - const context = Route.useRouteContext(); + const navigate = Route.useNavigate(); + const { ark, accounts } = Route.useRouteContext(); - const [loading, setLoading] = useState(false); + const [loading, setLoading] = useState(false); - const select = async (npub: string) => { - setLoading(true); + const select = async (npub: string) => { + try { + setLoading(true); - const ark = context.ark; - const loadAccount = await ark.load_selected_account(npub); + const loadAccount = await ark.load_selected_account(npub); + if (loadAccount) { + return navigate({ + to: "/$account/home", + params: { account: npub }, + replace: true, + }); + } + } catch (e) { + setLoading(false); + toast.error(String(e)); + } + }; - if (loadAccount) { - return navigate({ - to: "/$account/home", - params: { account: npub }, - replace: true, - }); - } - }; + const currentDate = new Date().toLocaleString("default", { + weekday: "long", + month: "long", + day: "numeric", + }); - const currentDate = new Date().toLocaleString("default", { - weekday: "long", - month: "long", - day: "numeric", - }); - - return ( -
-
-
-

{currentDate}

-

Welcome back!

-
-
- {loading ? ( -
- -
- ) : ( - <> - {context.accounts.map((account) => ( - - ))} - -
-
- -
-

Add

-
- - - )} -
-
- - ); + return ( +
+
+
+

{currentDate}

+

Welcome back!

+
+
+ {loading ? ( +
+ +
+ ) : ( + <> + {accounts.map((account) => ( + + ))} + +
+
+ +
+

Add

+
+ + + )} +
+
+ + ); } diff --git a/packages/ark/src/ark.ts b/packages/ark/src/ark.ts index 2fcf3308..76041fb3 100644 --- a/packages/ark/src/ark.ts +++ b/packages/ark/src/ark.ts @@ -1,13 +1,13 @@ import { - Kind, - type Contact, - type Event, - type EventWithReplies, - type Interests, - type Keys, - type LumeColumn, - type Metadata, - type Settings, + Kind, + type Contact, + type Event, + type EventWithReplies, + type Interests, + type Keys, + type LumeColumn, + type Metadata, + type Settings, } from "@lume/types"; import { generateContentTags } from "@lume/utils"; import { invoke } from "@tauri-apps/api/core"; @@ -16,876 +16,879 @@ import { open } from "@tauri-apps/plugin-dialog"; import { readFile } from "@tauri-apps/plugin-fs"; enum NSTORE_KEYS { - settings = "lume_user_settings", - interests = "lume_user_interests", - columns = "lume_user_columns", + settings = "lume_user_settings", + interests = "lume_user_interests", + columns = "lume_user_columns", } export class Ark { - public windows: WebviewWindow[]; - public settings: Settings; - - constructor() { - this.windows = []; - this.settings = undefined; - } - - public async get_all_accounts() { - try { - const cmd: string[] = await invoke("get_accounts"); - const accounts: string[] = cmd.map((item) => item.replace(".npub", "")); - - return accounts; - } catch (e) { - throw new Error(String(e)); - } - } - - public async load_selected_account(npub: string) { - try { - const cmd: boolean = await invoke("load_selected_account", { - npub, - }); - 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 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_keys"); - return cmd; - } catch (e) { - console.error(String(e)); - } - } - - public async save_account(nsec: string, password = "") { - try { - const cmd: string = await invoke("save_key", { - 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_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 get_events_from(pubkey: string, limit: number, asOf?: number) { - try { - let until: string = undefined; - if (asOf && asOf > 0) until = asOf.toString(); - - const nostrEvents: Event[] = await invoke("get_events_from", { - publicKey: pubkey, - limit, - as_of: until, - }); - - return nostrEvents.sort((a, b) => b.created_at - a.created_at); - } catch (e) { - console.error(String(e)); - return []; - } - } - - 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 []; - } - } - - public async get_events( - limit: number, - asOf?: number, - contacts?: string[], - global?: boolean, - ) { - try { - let until: string = undefined; - const isGlobal = global ?? false; - - if (asOf && asOf > 0) until = asOf.toString(); - - const seenIds = new Set(); - const nostrEvents: Event[] = await invoke("get_events", { - limit, - until, - contacts, - global: isGlobal, - }); - - // remove duplicate event - for (const event of nostrEvents) { - if (event.kind === Kind.Repost) { - const repostId = event.tags.find((tag) => tag[0] === "e")?.[1]; - seenIds.add(repostId); - } - - const eventIds = event.tags - .filter((el) => el[0] === "e") - ?.map((item) => item[1]); - - if (eventIds && eventIds.length) { - eventIds.forEach((id) => seenIds.add(id)); - } - } - - const events = nostrEvents - .filter((event) => !seenIds.has(event.id)) - .sort((a, b) => b.created_at - a.created_at); - - if (this.settings?.nsfw) { - return events.filter( - (event) => - event.tags.filter((event) => event[0] === "content-warning") - .length > 0, - ); - } - - return events; - } catch (e) { - console.info(String(e)); - return []; - } - } - - public async get_events_from_interests( - hashtags: string[], - limit: number, - asOf?: number, - ) { - let until: string = undefined; - if (asOf && asOf > 0) until = asOf.toString(); - - const seenIds = new Set(); - const dedupQueue = new Set(); - const nostrTags = hashtags.map((tag) => tag.replace("#", "").toLowerCase()); - - const nostrEvents: Event[] = await invoke("get_events_from_interests", { - hashtags: nostrTags, - limit, - until, - }); - - for (const event of nostrEvents) { - 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 nostrEvents - .filter((event) => !dedupQueue.has(event.id)) - .sort((a, b) => b.created_at - a.created_at); - } - - 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"]); - } 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 upvote(id: string, author: string) { - try { - const cmd: string = await invoke("upvote", { id, pubkey: author }); - return cmd; - } catch (e) { - throw new Error(String(e)); - } - } - - public async downvote(id: string, author: string) { - try { - const cmd: string = await invoke("downvote", { 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_event_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 parse_event_thread(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 (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 get_contact_list() { - try { - const cmd: string[] = await invoke("get_contact_list"); - return cmd; - } catch (e) { - console.error(e); - return []; - } - } - - public async get_contact_metadata() { - try { - const cmd: Contact[] = await invoke("get_contact_metadata"); - 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_interest() { - try { - const cmd: string = await invoke("get_nstore", { - key: NSTORE_KEYS.interests, - }); - const interests: Interests = cmd ? JSON.parse(cmd) : null; - return interests; - } catch { - return null; - } - } - - public async set_interest( - words: string[], - users: string[], - hashtags: string[], - ) { - try { - const interests: Interests = { - words: words ?? [], - users: users ?? [], - hashtags: hashtags ?? [], - }; - const cmd: string = await invoke("set_nstore", { - key: NSTORE_KEYS.interests, - content: JSON.stringify(interests), - }); - return cmd; - } catch (e) { - throw new Error(String(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: 500, - height: 360, - }); - } 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: 750, - height: 470, - }); - } 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)); - } - } + public windows: WebviewWindow[]; + public settings: Settings; + public accounts: string[]; + + constructor() { + this.windows = []; + this.settings = undefined; + } + + public async get_all_accounts() { + try { + const cmd: string[] = await invoke("get_accounts"); + const accounts: string[] = cmd.map((item) => item.replace(".npub", "")); + + if (!this.accounts) this.accounts = accounts; + + return accounts; + } catch (e) { + throw new Error(String(e)); + } + } + + public async load_selected_account(npub: string) { + try { + const cmd: boolean = await invoke("load_selected_account", { + npub, + }); + 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 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_keys"); + return cmd; + } catch (e) { + console.error(String(e)); + } + } + + public async save_account(nsec: string, password = "") { + try { + const cmd: string = await invoke("save_key", { + 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_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 get_events_from(pubkey: string, limit: number, asOf?: number) { + try { + let until: string = undefined; + if (asOf && asOf > 0) until = asOf.toString(); + + const nostrEvents: Event[] = await invoke("get_events_from", { + publicKey: pubkey, + limit, + as_of: until, + }); + + return nostrEvents.sort((a, b) => b.created_at - a.created_at); + } catch (e) { + console.error(String(e)); + return []; + } + } + + 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 []; + } + } + + public async get_events( + limit: number, + asOf?: number, + contacts?: string[], + global?: boolean, + ) { + try { + let until: string = undefined; + const isGlobal = global ?? false; + + if (asOf && asOf > 0) until = asOf.toString(); + + const seenIds = new Set(); + const nostrEvents: Event[] = await invoke("get_events", { + limit, + until, + contacts, + global: isGlobal, + }); + + // remove duplicate event + for (const event of nostrEvents) { + if (event.kind === Kind.Repost) { + const repostId = event.tags.find((tag) => tag[0] === "e")?.[1]; + seenIds.add(repostId); + } + + const eventIds = event.tags + .filter((el) => el[0] === "e") + ?.map((item) => item[1]); + + if (eventIds && eventIds.length) { + eventIds.forEach((id) => seenIds.add(id)); + } + } + + const events = nostrEvents + .filter((event) => !seenIds.has(event.id)) + .sort((a, b) => b.created_at - a.created_at); + + if (this.settings?.nsfw) { + return events.filter( + (event) => + event.tags.filter((event) => event[0] === "content-warning") + .length > 0, + ); + } + + return events; + } catch (e) { + console.info(String(e)); + return []; + } + } + + public async get_events_from_interests( + hashtags: string[], + limit: number, + asOf?: number, + ) { + let until: string = undefined; + if (asOf && asOf > 0) until = asOf.toString(); + + const seenIds = new Set(); + const dedupQueue = new Set(); + const nostrTags = hashtags.map((tag) => tag.replace("#", "").toLowerCase()); + + const nostrEvents: Event[] = await invoke("get_events_from_interests", { + hashtags: nostrTags, + limit, + until, + }); + + for (const event of nostrEvents) { + 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 nostrEvents + .filter((event) => !dedupQueue.has(event.id)) + .sort((a, b) => b.created_at - a.created_at); + } + + 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"]); + } 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 upvote(id: string, author: string) { + try { + const cmd: string = await invoke("upvote", { id, pubkey: author }); + return cmd; + } catch (e) { + throw new Error(String(e)); + } + } + + public async downvote(id: string, author: string) { + try { + const cmd: string = await invoke("downvote", { 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_event_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 parse_event_thread(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 (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 get_contact_list() { + try { + const cmd: string[] = await invoke("get_contact_list"); + return cmd; + } catch (e) { + console.error(e); + return []; + } + } + + public async get_contact_metadata() { + try { + const cmd: Contact[] = await invoke("get_contact_metadata"); + 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_interest() { + try { + const cmd: string = await invoke("get_nstore", { + key: NSTORE_KEYS.interests, + }); + const interests: Interests = cmd ? JSON.parse(cmd) : null; + return interests; + } catch { + return null; + } + } + + public async set_interest( + words: string[], + users: string[], + hashtags: string[], + ) { + try { + const interests: Interests = { + words: words ?? [], + users: users ?? [], + hashtags: hashtags ?? [], + }; + const cmd: string = await invoke("set_nstore", { + key: NSTORE_KEYS.interests, + content: JSON.stringify(interests), + }); + return cmd; + } catch (e) { + throw new Error(String(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: 500, + height: 360, + }); + } 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: 750, + height: 470, + }); + } 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)); + } + } } diff --git a/packages/types/index.d.ts b/packages/types/index.d.ts index c45fcbe7..c5c16e40 100644 --- a/packages/types/index.d.ts +++ b/packages/types/index.d.ts @@ -1,166 +1,167 @@ export interface Settings { - notification: boolean; - enhancedPrivacy: boolean; - autoUpdate: boolean; - zap: boolean; - nsfw: boolean; - [key: string]: string | number | boolean; + notification: boolean; + enhancedPrivacy: boolean; + autoUpdate: boolean; + zap: boolean; + nsfw: boolean; + [key: string]: string | number | boolean; } export interface Keys { - npub: string; - nsec: string; + 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 + Metadata = 0, + Text = 1, + RecommendRelay = 2, + Contacts = 3, + Repost = 6, + Reaction = 7, + ZapReceipt = 9735, + // 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; - relay?: string; + id: string; + pubkey: string; + created_at: number; + kind: Kind; + tags: string[][]; + content: string; + sig: string; + relay?: string; } export interface EventWithReplies extends Event { - replies: Array; + replies: Array; } export interface Metadata { - name?: string; - display_name?: string; - about?: string; - website?: string; - picture?: string; - banner?: string; - nip05?: string; - lud06?: string; - lud16?: string; + name?: string; + display_name?: string; + about?: string; + website?: string; + picture?: string; + banner?: string; + nip05?: string; + lud06?: string; + lud16?: string; } export interface Contact { - pubkey: string; - profile: Metadata; + pubkey: string; + profile: Metadata; } export interface Account { - npub: string; - nsec?: string; - contacts?: string[]; - interests?: Interests; + npub: string; + nsec?: string; + contacts?: string[]; + interests?: Interests; } export interface Interests { - hashtags: string[]; - users: string[]; - words: string[]; + hashtags: string[]; + users: string[]; + words: string[]; } export interface RichContent { - parsed: string; - images: string[]; - videos: string[]; - links: string[]; - notes: string[]; + parsed: string; + images: string[]; + videos: string[]; + links: string[]; + notes: string[]; } export interface AppRouteSearch { - account: string; + account: string; } export interface ColumnRouteSearch { - account: string; - label: string; - name: string; - redirect?: string; + account: string; + label: string; + name: string; + redirect?: string; } export interface LumeColumn { - label: string; - name: string; - content: URL | string; - description?: string; - author?: string; - logo?: string; - cover?: string; - coverRetina?: string; - featured?: boolean; + label: string; + name: string; + content: URL | string; + description?: string; + author?: string; + logo?: string; + cover?: string; + coverRetina?: string; + featured?: boolean; } export interface EventColumns { - type: "add" | "remove" | "update" | "left" | "right" | "set_title"; - label?: string; - title?: string; - column?: LumeColumn; + type: "add" | "remove" | "update" | "left" | "right" | "set_title"; + label?: string; + title?: string; + column?: LumeColumn; } export interface Opengraph { - url: string; - title?: string; - description?: string; - image?: string; + url: string; + title?: string; + description?: string; + image?: string; } export interface NostrBuildResponse { - ok: boolean; - data?: { - message: string; - status: string; - data: Array<{ - blurhash: string; - dimensions: { - width: number; - height: number; - }; - mime: string; - name: string; - sha256: string; - size: number; - url: string; - }>; - }; + ok: boolean; + data?: { + message: string; + status: string; + data: Array<{ + blurhash: string; + dimensions: { + width: number; + height: number; + }; + mime: string; + name: string; + sha256: string; + size: number; + url: string; + }>; + }; } export interface NIP11 { - name: string; - description: string; - pubkey: string; - contact: string; - supported_nips: number[]; - software: string; - version: string; - limitation: { - [key: string]: string | number | boolean; - }; - relay_countries: string[]; - language_tags: string[]; - tags: string[]; - posting_policy: string; - payments_url: string; - icon: string[]; + name: string; + description: string; + pubkey: string; + contact: string; + supported_nips: number[]; + software: string; + version: string; + limitation: { + [key: string]: string | number | boolean; + }; + relay_countries: string[]; + language_tags: string[]; + tags: string[]; + posting_policy: string; + payments_url: string; + icon: string[]; } export interface NIP05 { - names: { - [key: string]: string; - }; - nip46: { - [key: string]: { - [key: string]: string[]; - }; - }; + names: { + [key: string]: string; + }; + nip46: { + [key: string]: { + [key: string]: string[]; + }; + }; } diff --git a/src-tauri/src/nostr/metadata.rs b/src-tauri/src/nostr/metadata.rs index a015e613..94bc7168 100644 --- a/src-tauri/src/nostr/metadata.rs +++ b/src-tauri/src/nostr/metadata.rs @@ -23,12 +23,7 @@ pub fn run_notification(accounts: Vec, app: tauri::AppHandle) -> Result< .collect(); let subscription = Filter::new() .pubkeys(pubkeys) - .kinds(vec![ - Kind::TextNote, - Kind::Repost, - Kind::ZapReceipt, - Kind::EncryptedDirectMessage, - ]) + .kinds(vec![Kind::TextNote, Kind::Repost, Kind::ZapReceipt]) .since(Timestamp::now()); let activity_id = SubscriptionId::new("activity"); @@ -47,7 +42,8 @@ pub fn run_notification(accounts: Vec, app: tauri::AppHandle) -> Result< } = notification { if subscription_id == activity_id { - let _ = app.emit_to("main", "activity", event.as_json()); + println!("new notification: {}", event.as_json()); + let _ = app.emit("activity", event.as_json()); } } Ok(false)