diff --git a/src-tauri/src/commands/account.rs b/src-tauri/src/commands/account.rs index 61bced99..372e36ad 100644 --- a/src-tauri/src/commands/account.rs +++ b/src-tauri/src/commands/account.rs @@ -157,6 +157,26 @@ pub fn delete_account(id: String) -> Result<(), String> { Ok(()) } +#[tauri::command] +#[specta::specta] +pub async fn has_signer(id: String, state: State<'_, Nostr>) -> Result { + let client = &state.client; + let public_key = PublicKey::from_str(&id).map_err(|e| e.to_string())?; + + match client.signer().await { + Ok(signer) => { + let signer_key = signer.public_key().await.unwrap(); + + if signer_key == public_key { + Ok(true) + } else { + Ok(false) + } + } + Err(_) => Ok(false), + } +} + #[tauri::command] #[specta::specta] pub async fn set_signer( diff --git a/src-tauri/src/commands/event.rs b/src-tauri/src/commands/event.rs index 467ac9e9..8682a532 100644 --- a/src-tauri/src/commands/event.rs +++ b/src-tauri/src/commands/event.rs @@ -410,16 +410,58 @@ pub async fn repost(raw: String, state: State<'_, Nostr>) -> Result) -> Result { +pub async fn is_reposted(id: String, state: State<'_, Nostr>) -> Result { let client = &state.client; + let accounts = state.accounts.lock().unwrap().clone(); + let event_id = EventId::parse(&id).map_err(|err| err.to_string())?; - match client.delete_event(event_id).await { - Ok(event_id) => Ok(event_id.to_string()), + let authors: Vec = accounts + .iter() + .map(|acc| PublicKey::from_str(acc).unwrap()) + .collect(); + + let filter = Filter::new() + .event(event_id) + .kind(Kind::Repost) + .authors(authors); + + match client.database().query(vec![filter]).await { + Ok(events) => Ok(!events.is_empty()), Err(err) => Err(err.to_string()), } } +#[tauri::command] +#[specta::specta] +pub async fn request_delete(id: String, state: State<'_, Nostr>) -> Result<(), String> { + let client = &state.client; + let event_id = EventId::from_str(&id).map_err(|err| err.to_string())?; + + match client.delete_event(event_id).await { + Ok(_) => Ok(()), + Err(e) => Err(e.to_string()), + } +} + +#[tauri::command] +#[specta::specta] +pub async fn is_deleted_event(id: String, state: State<'_, Nostr>) -> Result { + let client = &state.client; + let signer = client.signer().await.map_err(|err| err.to_string())?; + let public_key = signer.public_key().await.map_err(|err| err.to_string())?; + let event_id = EventId::from_str(&id).map_err(|err| err.to_string())?; + let filter = Filter::new() + .author(public_key) + .event(event_id) + .kind(Kind::EventDeletion); + + match client.database().query(vec![filter]).await { + Ok(events) => Ok(!events.is_empty()), + Err(e) => Err(e.to_string()), + } +} + #[tauri::command] #[specta::specta] pub async fn event_to_bech32(id: String, state: State<'_, Nostr>) -> Result { @@ -498,34 +540,3 @@ pub async fn search(query: String, state: State<'_, Nostr>) -> Result Err(e.to_string()), } } - -#[tauri::command] -#[specta::specta] -pub async fn is_deleted_event(id: String, state: State<'_, Nostr>) -> Result { - let client = &state.client; - let signer = client.signer().await.map_err(|err| err.to_string())?; - let public_key = signer.public_key().await.map_err(|err| err.to_string())?; - let event_id = EventId::from_str(&id).map_err(|err| err.to_string())?; - let filter = Filter::new() - .author(public_key) - .event(event_id) - .kind(Kind::EventDeletion); - - match client.database().query(vec![filter]).await { - Ok(events) => Ok(!events.is_empty()), - Err(e) => Err(e.to_string()), - } -} - -#[tauri::command] -#[specta::specta] -pub async fn request_delete(id: String, state: State<'_, Nostr>) -> Result<(), String> { - let client = &state.client; - let event_id = EventId::from_str(&id).map_err(|err| err.to_string())?; - let builder = EventBuilder::delete(vec![event_id]); - - match client.send_event_builder(builder).await { - Ok(_) => Ok(()), - Err(e) => Err(e.to_string()), - } -} diff --git a/src-tauri/src/commands/window.rs b/src-tauri/src/commands/window.rs index 41259f35..4395513f 100644 --- a/src-tauri/src/commands/window.rs +++ b/src-tauri/src/commands/window.rs @@ -22,6 +22,7 @@ pub struct Window { maximizable: bool, minimizable: bool, hidden_title: bool, + closable: bool, } #[derive(Serialize, Deserialize, Type)] @@ -109,7 +110,7 @@ pub fn reload_column(label: String, app_handle: tauri::AppHandle) -> Result Result<(), String> { +pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result { if let Some(current_window) = app_handle.get_window(&window.label) { if current_window.is_visible().unwrap_or_default() { let _ = current_window.set_focus(); @@ -117,6 +118,8 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), S let _ = current_window.show(); let _ = current_window.set_focus(); }; + + Ok(current_window.label().to_string()) } else { let new_window = WebviewWindowBuilder::new( &app_handle, @@ -131,6 +134,7 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), S .minimizable(window.minimizable) .maximizable(window.maximizable) .transparent(true) + .closable(window.closable) .effects(WindowEffectsConfig { state: None, effects: vec![Effect::UnderWindowBackground], @@ -142,24 +146,26 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), S // Restore native border new_window.add_border(None); - } - Ok(()) + Ok(new_window.label().to_string()) + } } #[tauri::command(async)] #[specta::specta] #[cfg(target_os = "windows")] -pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), String> { - if let Some(window) = app_handle.get_window(&window.label) { - if window.is_visible().unwrap_or_default() { - let _ = window.set_focus(); +pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result { + if let Some(current_window) = app_handle.get_window(&window.label) { + if current_window.is_visible().unwrap_or_default() { + let _ = current_window.set_focus(); } else { - let _ = window.show(); - let _ = window.set_focus(); + let _ = current_window.show(); + let _ = current_window.set_focus(); }; + + Ok(current_window.label().to_string()) } else { - let window = WebviewWindowBuilder::new( + let new_window = WebviewWindowBuilder::new( &app_handle, &window.label, WebviewUrl::App(PathBuf::from(window.url)), @@ -171,6 +177,7 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), S .maximizable(window.maximizable) .transparent(true) .decorations(false) + .closable(window.closable) .effects(WindowEffectsConfig { state: None, effects: vec![Effect::Mica], @@ -181,7 +188,9 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), S .unwrap(); // Set decoration - window.create_overlay_titlebar().unwrap(); + new_window.create_overlay_titlebar().unwrap(); + + Ok(new_window.label().to_string()) } Ok(()) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 8c9bd78a..79c266c8 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -105,6 +105,7 @@ fn main() { get_private_key, delete_account, reset_password, + has_signer, set_signer, get_profile, set_profile, @@ -139,12 +140,13 @@ fn main() { get_all_events_by_hashtags, get_local_events, get_global_events, - is_deleted_event, - request_delete, search, publish, reply, repost, + is_reposted, + request_delete, + is_deleted_event, event_to_bech32, user_to_bech32, create_column, diff --git a/src/commands.gen.ts b/src/commands.gen.ts index cf62c559..05e11e50 100644 --- a/src/commands.gen.ts +++ b/src/commands.gen.ts @@ -99,6 +99,14 @@ async resetPassword(key: string, password: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("has_signer", { id }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, async setSigner(account: string, password: string) : Promise> { try { return { status: "ok", data: await TAURI_INVOKE("set_signer", { account, password }) }; @@ -371,22 +379,6 @@ async getGlobalEvents(until: string | null) : Promise> { - try { - return { status: "ok", data: await TAURI_INVOKE("is_deleted_event", { id }) }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, -async requestDelete(id: string) : Promise> { - try { - return { status: "ok", data: await TAURI_INVOKE("request_delete", { id }) }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, async search(query: string) : Promise> { try { return { status: "ok", data: await TAURI_INVOKE("search", { query }) }; @@ -419,6 +411,30 @@ async repost(raw: string) : Promise> { else return { status: "error", error: e as any }; } }, +async isReposted(id: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("is_reposted", { id }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async requestDelete(id: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("request_delete", { id }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async isDeletedEvent(id: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("is_deleted_event", { id }) }; +} catch (e) { + if(e instanceof Error) throw e; + 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 }) }; @@ -467,7 +483,7 @@ async closeColumn(label: string) : Promise> { else return { status: "error", error: e as any }; } }, -async openWindow(window: Window) : Promise> { +async openWindow(window: Window) : Promise> { try { return { status: "ok", data: await TAURI_INVOKE("open_window", { window }) }; } catch (e) { @@ -511,7 +527,7 @@ export type RichEvent = { raw: string; parsed: Meta | null } export type Settings = { proxy: string | null; image_resize_service: string | null; use_relay_hint: boolean; content_warning: boolean; trusted_only: boolean; display_avatar: boolean; display_zap_button: boolean; display_repost_button: boolean; display_media: boolean; transparent: boolean } export type SubKind = "Subscribe" | "Unsubscribe" export type Subscription = { label: string; kind: SubKind; event_id: string | null; contacts: string[] | null } -export type Window = { label: string; title: string; url: string; width: number; height: number; maximizable: boolean; minimizable: boolean; hidden_title: boolean } +export type Window = { label: string; title: string; url: string; width: number; height: number; maximizable: boolean; minimizable: boolean; hidden_title: boolean; closable: boolean } /** tauri-specta globals **/ diff --git a/src/components/conversation.tsx b/src/components/conversation.tsx deleted file mode 100644 index 09ed64e6..00000000 --- a/src/components/conversation.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { cn } from "@/commons"; -import { Note } from "@/components/note"; -import type { LumeEvent } from "@/system"; -import { ChatsTeardrop } from "@phosphor-icons/react"; -import { memo, useMemo } from "react"; - -export const Conversation = memo(function Conversation({ - event, - className, -}: { - event: LumeEvent; - className?: string; -}) { - const thread = useMemo(() => event.thread, [event]); - - return ( - - -
- {thread?.root?.id ? : null} -
-
- - Thread -
-
-
- {thread?.reply?.id ? : null} -
-
- -
- -
-
-
- -
- - - ); -}); diff --git a/src/components/frame.tsx b/src/components/frame.tsx index bcc54c62..c04ee3be 100644 --- a/src/components/frame.tsx +++ b/src/components/frame.tsx @@ -1,5 +1,4 @@ import { cn } from "@/commons"; -import { useRouteContext } from "@tanstack/react-router"; import type { ReactNode } from "react"; export function Frame({ @@ -7,8 +6,6 @@ export function Frame({ shadow, className, }: { children: ReactNode; shadow?: boolean; className?: string }) { - const { platform } = useRouteContext({ strict: false }); - return (
) => ( + + + + +); diff --git a/src/components/index.ts b/src/components/index.ts index 38f30d82..504c0a89 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -4,10 +4,8 @@ export * from "./spinner"; export * from "./column"; // Newsfeed -export * from "./repost"; -export * from "./conversation"; -export * from "./quote"; export * from "./text"; +export * from "./repost"; export * from "./reply"; // Global components @@ -18,3 +16,4 @@ export * from "./user"; export * from "./icons/reply"; export * from "./icons/repost"; export * from "./icons/zap"; +export * from "./icons/quote"; diff --git a/src/components/note/buttons/open.tsx b/src/components/note/buttons/open.tsx index 9eb18333..d0da9e75 100644 --- a/src/components/note/buttons/open.tsx +++ b/src/components/note/buttons/open.tsx @@ -8,7 +8,7 @@ export function NoteOpenThread() { return ( - + + + + + Quote + + + + + + ); +} diff --git a/src/components/note/buttons/reply.tsx b/src/components/note/buttons/reply.tsx index 00e94ac2..8e091553 100644 --- a/src/components/note/buttons/reply.tsx +++ b/src/components/note/buttons/reply.tsx @@ -18,10 +18,8 @@ export function NoteReply({ type="button" onClick={() => LumeWindow.openEditor(event.id)} className={cn( - "inline-flex items-center justify-center text-neutral-800 dark:text-neutral-200", - label - ? "rounded-full h-7 gap-1.5 w-20 text-sm font-medium hover:bg-black/10 dark:hover:bg-white/10" - : "size-7", + "h-7 rounded-full inline-flex items-center justify-center text-neutral-800 hover:bg-black/5 dark:hover:bg-white/5 dark:text-neutral-200 text-sm font-medium", + label ? "w-24 gap-1.5" : "w-14", )} > diff --git a/src/components/note/buttons/repost.tsx b/src/components/note/buttons/repost.tsx index 233e474e..1d7f34db 100644 --- a/src/components/note/buttons/repost.tsx +++ b/src/components/note/buttons/repost.tsx @@ -1,88 +1,174 @@ -import { appSettings, cn } from "@/commons"; +import { commands } from "@/commands.gen"; +import { appSettings, cn, displayNpub } from "@/commons"; import { RepostIcon, Spinner } from "@/components"; import { LumeWindow } from "@/system"; +import type { Metadata } from "@/types"; +import * as Tooltip from "@radix-ui/react-tooltip"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useStore } from "@tanstack/react-store"; import { Menu, MenuItem } from "@tauri-apps/api/menu"; -import { message } from "@tauri-apps/plugin-dialog"; -import { useCallback, useState } from "react"; +import type { Window } from "@tauri-apps/api/window"; +import { useCallback, useEffect, useState, useTransition } from "react"; import { useNoteContext } from "../provider"; export function NoteRepost({ label = false, smol = false, }: { label?: boolean; smol?: boolean }) { - const visible = useStore(appSettings, (state) => state.display_repost_button); const event = useNoteContext(); + const visible = useStore(appSettings, (state) => state.display_repost_button); + const queryClient = useQueryClient(); - const [loading, setLoading] = useState(false); - const [isRepost, setIsRepost] = useState(false); + const { isLoading, data: status } = useQuery({ + queryKey: ["is-reposted", event.id], + queryFn: async () => { + const res = await commands.isReposted(event.id); + if (res.status === "ok") { + return res.data; + } else { + return false; + } + }, + enabled: visible, + refetchOnMount: false, + refetchOnWindowFocus: false, + refetchOnReconnect: false, + staleTime: Number.POSITIVE_INFINITY, + retry: false, + }); - const repost = async () => { - if (isRepost) return; - - try { - setLoading(true); - - // repost - await event.repost(); - - // update state - setLoading(false); - setIsRepost(true); - } catch { - setLoading(false); - await message("Repost failed, try again later", { - title: "Lume", - kind: "info", - }); - } - }; + const [isPending, startTransition] = useTransition(); + const [popup, setPopup] = useState(null); const showContextMenu = useCallback(async (e: React.MouseEvent) => { e.preventDefault(); - const menuItems = await Promise.all([ - MenuItem.new({ - text: "Repost", - action: async () => repost(), - }), - MenuItem.new({ - text: "Quote", - action: () => LumeWindow.openEditor(null, event.id), - }), - ]); + const accounts = await commands.getAccounts(); + const list = []; - const menu = await Menu.new({ - items: menuItems, - }); + for (const account of accounts) { + const res = await commands.getProfile(account); + let name = "unknown"; + + if (res.status === "ok") { + const profile: Metadata = JSON.parse(res.data); + name = profile.display_name ?? profile.name; + } + + list.push( + MenuItem.new({ + text: `Repost as ${name} (${displayNpub(account, 16)})`, + action: async () => submit(account), + }), + ); + } + + const items = await Promise.all(list); + const menu = await Menu.new({ items }); await menu.popup().catch((e) => console.error(e)); }, []); + const repost = useMutation({ + mutationFn: async () => { + // Cancel any outgoing refetches + await queryClient.cancelQueries({ queryKey: ["is-reposted", event.id] }); + + // Optimistically update to the new value + queryClient.setQueryData(["is-reposted", event.id], true); + + const res = await commands.repost(JSON.stringify(event.raw)); + + if (res.status === "ok") { + return; + } else { + throw new Error(res.error); + } + }, + onError: () => { + queryClient.setQueryData(["is-reposted", event.id], false); + }, + onSettled: async () => { + return await queryClient.invalidateQueries({ + queryKey: ["is-reposted", event.id], + }); + }, + }); + + const submit = (account: string) => { + startTransition(async () => { + if (!status) { + const signer = await commands.hasSigner(account); + + if (signer.status === "ok") { + if (!signer.data) { + const newPopup = await LumeWindow.openPopup( + `/set-signer?account=${account}`, + undefined, + false, + ); + + setPopup(newPopup); + return; + } + + repost.mutate(); + } else { + return; + } + } else { + return; + } + }); + }; + + useEffect(() => { + if (!visible) return; + if (!popup) return; + + const unlisten = popup.listen("signer-updated", async () => { + repost.mutate(); + }); + + return () => { + unlisten.then((f) => f()); + }; + }, [popup]); + if (!visible) return null; return ( - + + + + + + + + Repost + + + + + ); } diff --git a/src/components/note/buttons/zap.tsx b/src/components/note/buttons/zap.tsx index fb581da5..5d49ebc6 100644 --- a/src/components/note/buttons/zap.tsx +++ b/src/components/note/buttons/zap.tsx @@ -20,10 +20,8 @@ export function NoteZap({ type="button" onClick={() => LumeWindow.openZap(event.id, search.account)} className={cn( - "inline-flex items-center justify-center text-neutral-800 dark:text-neutral-200", - label - ? "rounded-full h-7 gap-1.5 w-20 text-sm font-medium hover:bg-black/10 dark:hover:bg-white/10" - : "size-7", + "h-7 rounded-full inline-flex items-center justify-center text-neutral-800 hover:bg-black/5 dark:hover:bg-white/5 dark:text-neutral-200 text-sm font-medium", + label ? "w-24 gap-1.5" : "w-14", )} > diff --git a/src/components/note/index.ts b/src/components/note/index.ts index 43069be3..158c407b 100644 --- a/src/components/note/index.ts +++ b/src/components/note/index.ts @@ -1,4 +1,5 @@ import { NoteOpenThread } from "./buttons/open"; +import { NoteQuote } from "./buttons/quote"; import { NoteReply } from "./buttons/reply"; import { NoteRepost } from "./buttons/repost"; import { NoteZap } from "./buttons/zap"; @@ -16,6 +17,7 @@ export const Note = { User: NoteUser, Menu: NoteMenu, Reply: NoteReply, + Quote: NoteQuote, Repost: NoteRepost, Content: NoteContent, ContentLarge: NoteContentLarge, diff --git a/src/components/note/mentions/note.tsx b/src/components/note/mentions/note.tsx index aa900d80..63e6df99 100644 --- a/src/components/note/mentions/note.tsx +++ b/src/components/note/mentions/note.tsx @@ -51,11 +51,11 @@ export const MentionNote = memo(function MentionNote({ {replyTime(event.created_at)} -
+
diff --git a/src/components/quote.tsx b/src/components/quote.tsx deleted file mode 100644 index f848fa8e..00000000 --- a/src/components/quote.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { cn } from "@/commons"; -import { Note } from "@/components/note"; -import type { LumeEvent } from "@/system"; -import { Quotes } from "@phosphor-icons/react"; -import { memo } from "react"; - -export const Quote = memo(function Quote({ - event, - className, -}: { - event: LumeEvent; - className?: string; -}) { - return ( - - -
- -
-
- - Quote -
-
-
-
-
- -
- -
-
-
- -
- - - ); -}); diff --git a/src/components/reply.tsx b/src/components/reply.tsx index 2cd0a1c3..16ec2800 100644 --- a/src/components/reply.tsx +++ b/src/components/reply.tsx @@ -1,21 +1,12 @@ -import { commands } from "@/commands.gen"; -import { appSettings, cn, replyTime } from "@/commons"; +import { cn, replyTime } from "@/commons"; import { Note } from "@/components/note"; import { type LumeEvent, LumeWindow } from "@/system"; import { CaretDown } from "@phosphor-icons/react"; import { Link, useSearch } from "@tanstack/react-router"; -import { useStore } from "@tanstack/react-store"; import { Menu, MenuItem } from "@tauri-apps/api/menu"; import { writeText } from "@tauri-apps/plugin-clipboard-manager"; import { nip19 } from "nostr-tools"; -import { - type ReactNode, - memo, - useCallback, - useEffect, - useMemo, - useState, -} from "react"; +import { type ReactNode, memo, useCallback, useMemo } from "react"; import reactStringReplace from "react-string-replace"; import { Hashtag } from "./note/mentions/hashtag"; import { MentionUser } from "./note/mentions/user"; @@ -28,11 +19,7 @@ export const ReplyNote = memo(function ReplyNote({ event: LumeEvent; className?: string; }) { - const trustedOnly = useStore(appSettings, (state) => state.trusted_only); const search = useSearch({ strict: false }); - - const [isTrusted, setIsTrusted] = useState(null); - const showContextMenu = useCallback(async (e: React.MouseEvent) => { e.preventDefault(); @@ -57,24 +44,6 @@ export const ReplyNote = memo(function ReplyNote({ await menu.popup().catch((e) => console.error(e)); }, []); - useEffect(() => { - async function check() { - const res = await commands.isTrustedUser(event.pubkey); - - if (res.status === "ok") { - setIsTrusted(res.data); - } - } - - if (trustedOnly) { - check(); - } - }, []); - - if (isTrusted !== null && isTrusted === false) { - return null; - } - return ( @@ -99,7 +68,7 @@ export const ReplyNote = memo(function ReplyNote({ {replyTime(event.created_at)} -
+
@@ -180,7 +149,7 @@ function ChildReply({ event }: { event: LumeEvent }) { {replyTime(event.created_at)} -
+
diff --git a/src/components/repost.tsx b/src/components/repost.tsx index ad2c7522..bbac35f3 100644 --- a/src/components/repost.tsx +++ b/src/components/repost.tsx @@ -36,7 +36,7 @@ export const RepostNote = memo(function RepostNote({
-
+
diff --git a/src/components/text.tsx b/src/components/text.tsx index c491c57e..b1e83e11 100644 --- a/src/components/text.tsx +++ b/src/components/text.tsx @@ -18,7 +18,7 @@ export const TextNote = memo(function TextNote({
-
+
diff --git a/src/routes.gen.ts b/src/routes.gen.ts index d94f62e1..4d7bec38 100644 --- a/src/routes.gen.ts +++ b/src/routes.gen.ts @@ -13,6 +13,7 @@ import { createFileRoute } from '@tanstack/react-router' // Import Routes import { Route as rootRoute } from './routes/__root' +import { Route as SetSignerImport } from './routes/set-signer' import { Route as SetInterestImport } from './routes/set-interest' import { Route as SetGroupImport } from './routes/set-group' import { Route as BootstrapRelaysImport } from './routes/bootstrap-relays' @@ -91,6 +92,11 @@ const SettingsLazyRoute = SettingsLazyImport.update({ getParentRoute: () => rootRoute, } as any).lazy(() => import('./routes/_settings.lazy').then((d) => d.Route)) +const SetSignerRoute = SetSignerImport.update({ + path: '/set-signer', + getParentRoute: () => rootRoute, +} as any).lazy(() => import('./routes/set-signer.lazy').then((d) => d.Route)) + const SetInterestRoute = SetInterestImport.update({ path: '/set-interest', getParentRoute: () => rootRoute, @@ -330,6 +336,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof SetInterestImport parentRoute: typeof rootRoute } + '/set-signer': { + id: '/set-signer' + path: '/set-signer' + fullPath: '/set-signer' + preLoaderRoute: typeof SetSignerImport + parentRoute: typeof rootRoute + } '/_settings': { id: '/_settings' path: '' @@ -662,6 +675,7 @@ export interface FileRoutesByFullPath { '/bootstrap-relays': typeof BootstrapRelaysRoute '/set-group': typeof SetGroupRoute '/set-interest': typeof SetInterestRoute + '/set-signer': typeof SetSignerRoute '/new': typeof NewLazyRoute '/reset': typeof ResetLazyRoute '/bitcoin-connect': typeof SettingsBitcoinConnectRoute @@ -698,6 +712,7 @@ export interface FileRoutesByTo { '/bootstrap-relays': typeof BootstrapRelaysRoute '/set-group': typeof SetGroupRoute '/set-interest': typeof SetInterestRoute + '/set-signer': typeof SetSignerRoute '': typeof SettingsLazyRouteWithChildren '/new': typeof NewLazyRoute '/reset': typeof ResetLazyRoute @@ -737,6 +752,7 @@ export interface FileRoutesById { '/bootstrap-relays': typeof BootstrapRelaysRoute '/set-group': typeof SetGroupRoute '/set-interest': typeof SetInterestRoute + '/set-signer': typeof SetSignerRoute '/_settings': typeof SettingsLazyRouteWithChildren '/new': typeof NewLazyRoute '/reset': typeof ResetLazyRoute @@ -778,6 +794,7 @@ export interface FileRouteTypes { | '/bootstrap-relays' | '/set-group' | '/set-interest' + | '/set-signer' | '/new' | '/reset' | '/bitcoin-connect' @@ -813,6 +830,7 @@ export interface FileRouteTypes { | '/bootstrap-relays' | '/set-group' | '/set-interest' + | '/set-signer' | '' | '/new' | '/reset' @@ -850,6 +868,7 @@ export interface FileRouteTypes { | '/bootstrap-relays' | '/set-group' | '/set-interest' + | '/set-signer' | '/_settings' | '/new' | '/reset' @@ -890,6 +909,7 @@ export interface RootRouteChildren { BootstrapRelaysRoute: typeof BootstrapRelaysRoute SetGroupRoute: typeof SetGroupRoute SetInterestRoute: typeof SetInterestRoute + SetSignerRoute: typeof SetSignerRoute SettingsLazyRoute: typeof SettingsLazyRouteWithChildren NewLazyRoute: typeof NewLazyRoute ResetLazyRoute: typeof ResetLazyRoute @@ -906,6 +926,7 @@ const rootRouteChildren: RootRouteChildren = { BootstrapRelaysRoute: BootstrapRelaysRoute, SetGroupRoute: SetGroupRoute, SetInterestRoute: SetInterestRoute, + SetSignerRoute: SetSignerRoute, SettingsLazyRoute: SettingsLazyRouteWithChildren, NewLazyRoute: NewLazyRoute, ResetLazyRoute: ResetLazyRoute, @@ -933,6 +954,7 @@ export const routeTree = rootRoute "/bootstrap-relays", "/set-group", "/set-interest", + "/set-signer", "/_settings", "/new", "/reset", @@ -959,6 +981,9 @@ export const routeTree = rootRoute "/set-interest": { "filePath": "set-interest.tsx" }, + "/set-signer": { + "filePath": "set-signer.tsx" + }, "/_settings": { "filePath": "_settings.lazy.tsx", "children": [ diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 7882d0d9..9baa947f 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -10,6 +10,7 @@ import { useEffect } from "react"; interface RouterContext { queryClient: QueryClient; platform: OsType; + account: string[]; } export const Route = createRootRouteWithContext()({ diff --git a/src/routes/_layout.lazy.tsx b/src/routes/_layout.lazy.tsx index 8873717b..4b4c7a87 100644 --- a/src/routes/_layout.lazy.tsx +++ b/src/routes/_layout.lazy.tsx @@ -126,24 +126,20 @@ const Account = memo(function Account({ pubkey }: { pubkey: string }) { async (e: React.MouseEvent) => { e.preventDefault(); - const menuItems = await Promise.all([ + const items = await Promise.all([ MenuItem.new({ - text: "New Post", - action: () => LumeWindow.openEditor(), + text: "View Profile", + action: () => LumeWindow.openProfile(pubkey), }), MenuItem.new({ - text: "Profile", - action: () => LumeWindow.openProfile(pubkey), + text: "Copy Public Key", + action: async () => await writeText(pubkey), }), MenuItem.new({ text: "Settings", action: () => LumeWindow.openSettings(pubkey), }), PredefinedMenuItem.new({ item: "Separator" }), - MenuItem.new({ - text: "Copy Public Key", - action: async () => await writeText(pubkey), - }), MenuItem.new({ text: "Logout", action: async () => { @@ -162,9 +158,7 @@ const Account = memo(function Account({ pubkey }: { pubkey: string }) { }), ]); - const menu = await Menu.new({ - items: menuItems, - }); + const menu = await Menu.new({ items }); await menu.popup().catch((e) => console.error(e)); }, diff --git a/src/routes/columns/_layout/launchpad.lazy.tsx b/src/routes/columns/_layout/launchpad.lazy.tsx index e12986e1..1174bb41 100644 --- a/src/routes/columns/_layout/launchpad.lazy.tsx +++ b/src/routes/columns/_layout/launchpad.lazy.tsx @@ -134,7 +134,7 @@ function Groups() { +
+
+
+ +
+
+ ); +} diff --git a/src/routes/set-signer.tsx b/src/routes/set-signer.tsx new file mode 100644 index 00000000..f73f6680 --- /dev/null +++ b/src/routes/set-signer.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from "@tanstack/react-router"; + +export interface RouteSearch { + account?: string; +} + +export const Route = createFileRoute("/set-signer")({ + validateSearch: (search: Record): RouteSearch => { + return { + account: search.account, + }; + }, +}); diff --git a/src/system/event.ts b/src/system/event.ts index 1c2f55c8..ab280fd9 100644 --- a/src/system/event.ts +++ b/src/system/event.ts @@ -12,10 +12,10 @@ export class LumeEvent { public meta: Meta; public relay?: string; public replies?: LumeEvent[]; - #raw: NostrEvent; + public raw: NostrEvent; constructor(event: NostrEvent) { - this.#raw = event; + this.raw = event; Object.assign(this, event); } @@ -134,16 +134,6 @@ export class LumeEvent { } } - 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); - } - } - static async publish( content: string, warning?: string, diff --git a/src/system/window.ts b/src/system/window.ts index 87867448..3a84bf6e 100644 --- a/src/system/window.ts +++ b/src/system/window.ts @@ -1,6 +1,6 @@ import { commands } from "@/commands.gen"; import type { LumeColumn, NostrEvent } from "@/types"; -import { getCurrentWindow } from "@tauri-apps/api/window"; +import { Window, getCurrentWindow } from "@tauri-apps/api/window"; import { nanoid } from "nanoid"; import type { LumeEvent } from "./event"; @@ -120,6 +120,7 @@ export const LumeWindow = { maximizable: false, minimizable: false, hidden_title: true, + closable: true, }); if (query.status === "ok") { @@ -141,6 +142,7 @@ export const LumeWindow = { maximizable: false, minimizable: false, hidden_title: true, + closable: true, }); } else { await LumeWindow.openSettings(account, "bitcoin-connect"); @@ -156,6 +158,7 @@ export const LumeWindow = { maximizable: false, minimizable: false, hidden_title: true, + closable: true, }); if (query.status === "ok") { @@ -164,20 +167,21 @@ export const LumeWindow = { throw new Error(query.error); } }, - openPopup: async (title: string, url: string) => { + openPopup: async (url: string, title?: string, closable = true) => { const query = await commands.openWindow({ label: `popup-${nanoid()}`, url, - title, + title: title ?? "", width: 400, height: 500, maximizable: false, minimizable: false, - hidden_title: false, + hidden_title: !!title, + closable, }); if (query.status === "ok") { - return query.data; + return await Window.getByLabel(query.data); } else { throw new Error(query.error); }