diff --git a/apps/desktop2/src/components/note/buttons/repost.tsx b/apps/desktop2/src/components/note/buttons/repost.tsx index 66730cce..b8e840bf 100644 --- a/apps/desktop2/src/components/note/buttons/repost.tsx +++ b/apps/desktop2/src/components/note/buttons/repost.tsx @@ -1,24 +1,23 @@ -import { QuoteIcon, RepostIcon } from "@lume/icons"; +import { RepostIcon } from "@lume/icons"; import { cn } from "@lume/utils"; -import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; -import * as Tooltip from "@radix-ui/react-tooltip"; -import { useState } from "react"; +import { useCallback, useState } from "react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { Spinner } from "@lume/ui"; import { useNoteContext } from "../provider"; import { LumeWindow } from "@lume/system"; +import { Menu, MenuItem } from "@tauri-apps/api/menu"; export function NoteRepost({ large = false }: { large?: boolean }) { const event = useNoteContext(); - const [t] = useTranslation(); const [loading, setLoading] = useState(false); const [isRepost, setIsRepost] = useState(false); const repost = async () => { + if (isRepost) return; + try { - if (isRepost) return; setLoading(true); // repost @@ -30,71 +29,50 @@ export function NoteRepost({ large = false }: { large?: boolean }) { // notify toast.success("You've reposted this post successfully"); - } catch (e) { + } catch { setLoading(false); toast.error("Repost failed, try again later"); } }; + const showContextMenu = useCallback(async (e: React.MouseEvent) => { + e.preventDefault(); + + const menuItems = await Promise.all([ + MenuItem.new({ + text: "Quote", + action: async () => repost(), + }), + MenuItem.new({ + text: "Repost", + action: () => LumeWindow.openEditor(null, event.id), + }), + ]); + + const menu = await Menu.new({ + items: menuItems, + }); + + await menu.popup().catch((e) => console.error(e)); + }, []); + return ( - - - - - - - - - - - {t("note.buttons.repost")} - - - - - - - - - - - - - - - - - + ); } diff --git a/apps/desktop2/src/components/note/menu.tsx b/apps/desktop2/src/components/note/menu.tsx index a0728a41..4ab542cd 100644 --- a/apps/desktop2/src/components/note/menu.tsx +++ b/apps/desktop2/src/components/note/menu.tsx @@ -1,100 +1,62 @@ import { HorizontalDotsIcon } from "@lume/icons"; -import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; import { writeText } from "@tauri-apps/plugin-clipboard-manager"; -import { useTranslation } from "react-i18next"; import { useNoteContext } from "./provider"; -import { LumeWindow } from "@lume/system"; +import { useCallback } from "react"; +import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu"; export function NoteMenu() { - const { t } = useTranslation(); const event = useNoteContext(); - const copyID = async () => { - await writeText(await event.idAsBech32()); - }; + const showContextMenu = useCallback(async (e: React.MouseEvent) => { + e.preventDefault(); - const copyRaw = async () => { - await writeText(JSON.stringify(event)); - }; + const menuItems = await Promise.all([ + MenuItem.new({ + text: "Copy Sharable Link", + action: async () => { + const eventId = await event.idAsBech32(); + await writeText(`https://njump.me/${eventId}`); + }, + }), + MenuItem.new({ + text: "Copy Event ID", + action: async () => { + const eventId = await event.idAsBech32(); + await writeText(eventId); + }, + }), + MenuItem.new({ + text: "Copy Public Key", + action: async () => { + const pubkey = await event.pubkeyAsBech32(); + await writeText(pubkey); + }, + }), + PredefinedMenuItem.new({ item: "Separator" }), + MenuItem.new({ + text: "Copy Raw Event", + action: async () => { + event.meta = undefined; + const raw = JSON.stringify(event); + await writeText(raw); + }, + }), + ]); - const copyNpub = async () => { - await writeText(await event.pubkeyAsBech32()); - }; + const menu = await Menu.new({ + items: menuItems, + }); - const copyLink = async () => { - await writeText(`https://njump.me/${await event.idAsBech32()}`); - }; + await menu.popup().catch((e) => console.error(e)); + }, []); return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + ); } diff --git a/apps/desktop2/src/routes/editor/-components/mention.tsx b/apps/desktop2/src/routes/editor/-components/mention.tsx deleted file mode 100644 index 09c8b7b4..00000000 --- a/apps/desktop2/src/routes/editor/-components/mention.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { MentionIcon } from "@lume/icons"; -import { cn, insertMention } from "@lume/utils"; -import * as Tooltip from "@radix-ui/react-tooltip"; -import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; -import { useEffect, useState } from "react"; -import { useSlateStatic } from "slate-react"; -import type { Contact } from "@lume/types"; -import { toast } from "sonner"; -import { User } from "@/components/user"; -import { NostrAccount, NostrQuery } from "@lume/system"; - -export function MentionButton({ className }: { className?: string }) { - const editor = useSlateStatic(); - const [contacts, setContacts] = useState([]); - - const select = async (user: string) => { - try { - const metadata = await NostrQuery.getProfile(user); - const contact: Contact = { pubkey: user, profile: metadata }; - - insertMention(editor, contact); - } catch (e) { - toast.error(String(e)); - } - }; - - useEffect(() => { - async function getContacts() { - const data = await NostrAccount.getContactList(); - setContacts(data); - } - - getContacts(); - }, []); - - return ( - - - - - - - - - - - Mention - - - - - - - - {contacts.length < 1 ? ( -
-

Contact List is empty.

-
- ) : ( - contacts.map((contact) => ( - select(contact)} - className="flex items-center px-2 shrink-0 h-11 hover:bg-white/10" - > - - - - - - - - )) - )} - -
-
-
- ); -} diff --git a/packages/system/src/commands.ts b/packages/system/src/commands.ts index 01f2bd63..e7313866 100644 --- a/packages/system/src/commands.ts +++ b/packages/system/src/commands.ts @@ -100,22 +100,6 @@ try { else return { status: "error", error: e as any }; } }, -async eventToBech32(id: string, relays: string[]) : Promise> { -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> { -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 verifyNip05(key: string, nip05: string) : Promise> { try { return { status: "ok", data: await TAURI_INVOKE("verify_nip05", { key, nip05 }) }; @@ -356,6 +340,22 @@ try { else return { status: "error", error: e as any }; } }, +async eventToBech32(id: string) : Promise> { +try { + return { status: "ok", data: await TAURI_INVOKE("event_to_bech32", { id }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async userToBech32(user: string) : Promise> { +try { + return { status: "ok", data: await TAURI_INVOKE("user_to_bech32", { user }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, async showInFolder(path: string) : Promise { await TAURI_INVOKE("show_in_folder", { path }); }, diff --git a/packages/system/src/event.ts b/packages/system/src/event.ts index e889e03c..a86f885b 100644 --- a/packages/system/src/event.ts +++ b/packages/system/src/event.ts @@ -150,7 +150,7 @@ export class LumeEvent { } public async idAsBech32() { - const query = await commands.eventToBech32(this.id, []); + const query = await commands.eventToBech32(this.id); if (query.status === "ok") { return query.data; @@ -160,7 +160,7 @@ export class LumeEvent { } public async pubkeyAsBech32() { - const query = await commands.userToBech32(this.pubkey, []); + const query = await commands.userToBech32(this.pubkey); if (query.status === "ok") { return query.data; diff --git a/src-tauri/capabilities/main.json b/src-tauri/capabilities/main.json index 34462be5..534b8867 100644 --- a/src-tauri/capabilities/main.json +++ b/src-tauri/capabilities/main.json @@ -59,6 +59,8 @@ "fs:allow-read-file", "theme:allow-set-theme", "theme:allow-get-theme", + "menu:allow-new", + "menu:allow-popup", "http:default", "shell:allow-open", { diff --git a/src-tauri/gen/schemas/capabilities.json b/src-tauri/gen/schemas/capabilities.json index d74af3be..fa5faed2 100644 --- a/src-tauri/gen/schemas/capabilities.json +++ b/src-tauri/gen/schemas/capabilities.json @@ -1 +1 @@ -{"desktop-capability":{"identifier":"desktop-capability","description":"Capability for the desktop","local":true,"windows":["main","panel","splash","settings","search","nwc","activity","zap-*","event-*","user-*","editor-*","column-*"],"permissions":["path:default","event:default","window:default","app:default","resources:default","menu:default","tray:default","notification:allow-is-permission-granted","notification:allow-request-permission","notification:default","os:allow-locale","os:allow-platform","os:allow-os-type","updater:default","updater:allow-check","updater:allow-download-and-install","window:allow-start-dragging","window:allow-create","window:allow-close","window:allow-set-focus","window:allow-center","window:allow-minimize","window:allow-maximize","window:allow-set-size","window:allow-set-focus","window:allow-start-dragging","decorum:allow-show-snap-overlay","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","webview:allow-create-webview-window","webview:allow-create-webview","webview:allow-set-webview-size","webview:allow-set-webview-position","webview:allow-webview-close","dialog:allow-open","dialog:allow-ask","dialog:allow-message","process:allow-restart","fs:allow-read-file","theme:allow-set-theme","theme:allow-get-theme","http:default","shell:allow-open",{"identifier":"http:default","allow":[{"url":"http://**/"},{"url":"https://**/"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"$RESOURCE/locales/*"},{"path":"$RESOURCE/resources/*"}]}],"platforms":["linux","macOS","windows"]}} \ No newline at end of file +{"desktop-capability":{"identifier":"desktop-capability","description":"Capability for the desktop","local":true,"windows":["main","panel","splash","settings","search","nwc","activity","zap-*","event-*","user-*","editor-*","column-*"],"permissions":["path:default","event:default","window:default","app:default","resources:default","menu:default","tray:default","notification:allow-is-permission-granted","notification:allow-request-permission","notification:default","os:allow-locale","os:allow-platform","os:allow-os-type","updater:default","updater:allow-check","updater:allow-download-and-install","window:allow-start-dragging","window:allow-create","window:allow-close","window:allow-set-focus","window:allow-center","window:allow-minimize","window:allow-maximize","window:allow-set-size","window:allow-set-focus","window:allow-start-dragging","decorum:allow-show-snap-overlay","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","webview:allow-create-webview-window","webview:allow-create-webview","webview:allow-set-webview-size","webview:allow-set-webview-position","webview:allow-webview-close","dialog:allow-open","dialog:allow-ask","dialog:allow-message","process:allow-restart","fs:allow-read-file","theme:allow-set-theme","theme:allow-get-theme","menu:allow-new","menu:allow-popup","http:default","shell:allow-open",{"identifier":"http:default","allow":[{"url":"http://**/"},{"url":"https://**/"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"$RESOURCE/locales/*"},{"path":"$RESOURCE/resources/*"}]}],"platforms":["linux","macOS","windows"]}} \ No newline at end of file diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index a9b27017..82c891e1 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -57,8 +57,6 @@ fn main() { nostr::keys::get_private_key, nostr::keys::connect_remote_account, nostr::keys::load_account, - nostr::keys::event_to_bech32, - nostr::keys::user_to_bech32, nostr::keys::verify_nip05, nostr::metadata::get_current_user_profile, nostr::metadata::get_profile, @@ -89,6 +87,8 @@ fn main() { nostr::event::publish, nostr::event::reply, nostr::event::repost, + nostr::event::event_to_bech32, + nostr::event::user_to_bech32, commands::folder::show_in_folder, commands::window::create_column, commands::window::close_column, diff --git a/src-tauri/src/nostr/event.rs b/src-tauri/src/nostr/event.rs index d035b969..c8e4bb56 100644 --- a/src-tauri/src/nostr/event.rs +++ b/src-tauri/src/nostr/event.rs @@ -91,7 +91,7 @@ pub async fn get_event_from( return Err(err.to_string()); } - if (client.connect_relay(relay_hint).await).is_ok() { + if client.connect_relay(relay_hint).await.is_ok() { match event_id { Some(id) => { match client @@ -522,3 +522,79 @@ pub async fn repost(raw: &str, state: State<'_, Nostr>) -> Result Err(err.to_string()), } } + +#[tauri::command] +#[specta::specta] +pub async fn event_to_bech32(id: &str, state: State<'_, Nostr>) -> Result { + let client = &state.client; + + let event_id = match EventId::from_hex(id) { + Ok(id) => id, + Err(_) => return Err("ID is not valid.".into()), + }; + + let seens = client + .database() + .event_seen_on_relays(event_id) + .await + .unwrap(); + + match seens { + Some(set) => { + let relays = set.into_iter().collect::>(); + let event = Nip19Event::new(event_id, relays); + + match event.to_bech32() { + Ok(id) => Ok(id), + Err(err) => Err(err.to_string()), + } + } + None => match event_id.to_bech32() { + Ok(id) => Ok(id), + Err(err) => Err(err.to_string()), + }, + } +} + +#[tauri::command] +#[specta::specta] +pub async fn user_to_bech32(user: &str, state: State<'_, Nostr>) -> Result { + let client = &state.client; + + let public_key = match PublicKey::from_str(user) { + Ok(pk) => pk, + Err(_) => return Err("Public Key is not valid.".into()), + }; + + match client + .get_events_of( + vec![Filter::new() + .author(public_key) + .kind(Kind::RelayList) + .limit(1)], + Some(Duration::from_secs(10)), + ) + .await + { + Ok(events) => match events.first() { + Some(event) => { + let relay_list = nip65::extract_relay_list(event); + let relays = relay_list + .into_iter() + .map(|i| i.0.to_string()) + .collect::>(); + let profile = Nip19Profile::new(public_key, relays).unwrap(); + + Ok(profile.to_bech32().unwrap()) + } + None => match public_key.to_bech32() { + Ok(pk) => Ok(pk), + Err(err) => Err(err.to_string()), + }, + }, + Err(_) => match public_key.to_bech32() { + Ok(pk) => Ok(pk), + Err(err) => Err(err.to_string()), + }, + } +} diff --git a/src-tauri/src/nostr/keys.rs b/src-tauri/src/nostr/keys.rs index 75538e2a..36f4a0a7 100644 --- a/src-tauri/src/nostr/keys.rs +++ b/src-tauri/src/nostr/keys.rs @@ -383,24 +383,6 @@ pub fn get_private_key(npub: &str) -> Result { } } -#[tauri::command] -#[specta::specta] -pub fn event_to_bech32(id: &str, relays: Vec) -> Result { - let event_id = EventId::from_hex(id).unwrap(); - let event = Nip19Event::new(event_id, relays); - - Ok(event.to_bech32().unwrap()) -} - -#[tauri::command] -#[specta::specta] -pub fn user_to_bech32(key: &str, relays: Vec) -> Result { - let pubkey = PublicKey::from_str(key).unwrap(); - let profile = Nip19Profile::new(pubkey, relays).unwrap(); - - Ok(profile.to_bech32().unwrap()) -} - #[tauri::command] #[specta::specta] pub async fn verify_nip05(key: &str, nip05: &str) -> Result {