From 62ba8a985f4e6a9ef7b58230499e26eb33016e8b Mon Sep 17 00:00:00 2001 From: reya Date: Tue, 22 Oct 2024 09:25:41 +0700 Subject: [PATCH] feat: publish with multi account --- src-tauri/src/commands/metadata.rs | 2 +- src-tauri/src/common.rs | 36 +-- src-tauri/src/main.rs | 98 +++---- src/commands.gen.ts | 4 +- src/components/icons/publish.tsx | 19 ++ src/components/index.ts | 1 + src/routes.gen.ts | 38 +-- src/routes/__root.tsx | 2 +- src/routes/_layout.lazy.tsx | 7 +- src/routes/_layout/index.lazy.tsx | 220 ---------------- .../-components/media.tsx | 5 - .../{editor => new-post}/-components/pow.tsx | 0 .../-components/warning.tsx | 0 src/routes/{editor => new-post}/index.tsx | 241 +++++++++++++----- src/system/window.ts | 8 +- 15 files changed, 287 insertions(+), 394 deletions(-) create mode 100644 src/components/icons/publish.tsx rename src/routes/{editor => new-post}/-components/media.tsx (90%) rename src/routes/{editor => new-post}/-components/pow.tsx (100%) rename src/routes/{editor => new-post}/-components/warning.tsx (100%) rename src/routes/{editor => new-post}/index.tsx (64%) diff --git a/src-tauri/src/commands/metadata.rs b/src-tauri/src/commands/metadata.rs index 4efcc765..0e7c3b01 100644 --- a/src-tauri/src/commands/metadata.rs +++ b/src-tauri/src/commands/metadata.rs @@ -415,7 +415,7 @@ pub async fn get_all_interests(state: State<'_, Nostr>) -> Result #[tauri::command] #[specta::specta] -pub async fn get_mention_list(state: State<'_, Nostr>) -> Result, String> { +pub async fn get_all_profiles(state: State<'_, Nostr>) -> Result, String> { let client = &state.client; let filter = Filter::new().kind(Kind::Metadata); diff --git a/src-tauri/src/common.rs b/src-tauri/src/common.rs index ba447845..ae6d7c57 100644 --- a/src-tauri/src/common.rs +++ b/src-tauri/src/common.rs @@ -5,7 +5,7 @@ use nostr_sdk::prelude::*; use reqwest::Client as ReqClient; use serde::Serialize; use specta::Type; -use std::{collections::HashSet, str::FromStr, time::Duration}; +use std::{collections::HashSet, str::FromStr}; use crate::RichEvent; @@ -78,7 +78,7 @@ pub fn create_tags(content: &str) -> Vec { let hashtags = words .iter() .filter(|&&word| word.starts_with('#')) - .map(|&s| s.to_string()) + .map(|&s| s.to_string().replace("#", "").to_lowercase()) .collect::>(); for mention in mentions { @@ -227,38 +227,6 @@ pub async fn process_event(client: &Client, events: Events) -> Vec { join_all(futures).await } -pub async fn init_nip65(client: &Client, public_key: &str) { - let author = PublicKey::from_str(public_key).unwrap(); - let filter = Filter::new().author(author).kind(Kind::RelayList).limit(1); - - // client.add_relay("ws://127.0.0.1:1984").await.unwrap(); - // client.connect_relay("ws://127.0.0.1:1984").await.unwrap(); - - if let Ok(events) = client - .fetch_events(vec![filter], Some(Duration::from_secs(5))) - .await - { - if let Some(event) = events.first() { - let relay_list = nip65::extract_relay_list(event); - for (url, metadata) in relay_list { - let opts = match metadata { - Some(RelayMetadata::Read) => RelayOptions::new().read(true).write(false), - Some(_) => RelayOptions::new().write(true).read(false), - None => RelayOptions::default(), - }; - if let Err(e) = client.pool().add_relay(&url.to_string(), opts).await { - eprintln!("Failed to add relay {}: {:?}", url, e); - } - if let Err(e) = client.connect_relay(url.to_string()).await { - eprintln!("Failed to connect to relay {}: {:?}", url, e); - } else { - println!("Connecting to relay: {} - {:?}", url, metadata); - } - } - } - } -} - pub async fn parse_event(content: &str) -> Meta { let mut finder = LinkFinder::new(); finder.url_must_have_scheme(false); diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 0d28fbb4..8b55b361 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -121,7 +121,7 @@ fn main() { set_contact_list, is_contact, toggle_contact, - get_mention_list, + get_all_profiles, set_group, get_group, get_all_groups, @@ -320,54 +320,66 @@ fn main() { SubKind::Subscribe => { let subscription_id = SubscriptionId::new(payload.label); - // Update state - state - .subscriptions - .lock() - .unwrap() - .push(subscription_id.clone()); + if !client + .pool() + .subscriptions() + .await + .contains_key(&subscription_id) + { + // Update state + state + .subscriptions + .lock() + .unwrap() + .push(subscription_id.clone()); - println!( - "Total subscriptions: {}", - state.subscriptions.lock().unwrap().len() - ); + println!( + "Total subscriptions: {}", + state.subscriptions.lock().unwrap().len() + ); - if let Some(id) = payload.event_id { - let event_id = EventId::from_str(&id).unwrap(); - let filter = Filter::new().event(event_id).since(Timestamp::now()); + if let Some(id) = payload.event_id { + let event_id = EventId::from_str(&id).unwrap(); + let filter = + Filter::new().event(event_id).since(Timestamp::now()); - if let Err(e) = client - .subscribe_with_id(subscription_id.clone(), vec![filter], None) - .await - { - println!("Subscription error: {}", e) + if let Err(e) = client + .subscribe_with_id( + subscription_id.clone(), + vec![filter], + None, + ) + .await + { + println!("Subscription error: {}", e) + } } - } - if let Some(ids) = payload.contacts { - let authors: Vec = ids - .iter() - .filter_map(|item| { - if let Ok(pk) = PublicKey::from_str(item) { - Some(pk) - } else { - None - } - }) - .collect(); + if let Some(ids) = payload.contacts { + let authors: Vec = ids + .iter() + .filter_map(|item| { + if let Ok(pk) = PublicKey::from_str(item) { + Some(pk) + } else { + None + } + }) + .collect(); - if let Err(e) = client - .subscribe_with_id( - subscription_id, - vec![Filter::new() - .kinds(vec![Kind::TextNote, Kind::Repost]) - .authors(authors) - .since(Timestamp::now())], - None, - ) - .await - { - println!("Subscription error: {}", e) + if let Err(e) = client + .subscribe_with_id( + subscription_id, + vec![Filter::new() + .kinds(vec![Kind::TextNote, Kind::Repost]) + .authors(authors) + .since(Timestamp::now())], + None, + ) + .await + { + println!("Subscription error: {}", e) + } } } } diff --git a/src/commands.gen.ts b/src/commands.gen.ts index 3dfd3478..9ebd3087 100644 --- a/src/commands.gen.ts +++ b/src/commands.gen.ts @@ -160,9 +160,9 @@ async toggleContact(id: string, alias: string | null) : Promise> { +async getAllProfiles() : Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("get_mention_list") }; + return { status: "ok", data: await TAURI_INVOKE("get_all_profiles") }; } catch (e) { if(e instanceof Error) throw e; else return { status: "error", error: e as any }; diff --git a/src/components/icons/publish.tsx b/src/components/icons/publish.tsx new file mode 100644 index 00000000..a3c702fb --- /dev/null +++ b/src/components/icons/publish.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from "react"; + +export const PublishIcon = (props: SVGProps) => ( + + + +); diff --git a/src/components/index.ts b/src/components/index.ts index 504c0a89..630d77ff 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -17,3 +17,4 @@ export * from "./icons/reply"; export * from "./icons/repost"; export * from "./icons/zap"; export * from "./icons/quote"; +export * from "./icons/publish"; diff --git a/src/routes.gen.ts b/src/routes.gen.ts index 4d7bec38..d6ab846c 100644 --- a/src/routes.gen.ts +++ b/src/routes.gen.ts @@ -18,7 +18,7 @@ import { Route as SetInterestImport } from './routes/set-interest' import { Route as SetGroupImport } from './routes/set-group' import { Route as BootstrapRelaysImport } from './routes/bootstrap-relays' import { Route as LayoutImport } from './routes/_layout' -import { Route as EditorIndexImport } from './routes/editor/index' +import { Route as NewPostIndexImport } from './routes/new-post/index' import { Route as LayoutIndexImport } from './routes/_layout/index' import { Route as ZapIdImport } from './routes/zap.$id' import { Route as ColumnsLayoutImport } from './routes/columns/_layout' @@ -119,8 +119,8 @@ const LayoutRoute = LayoutImport.update({ getParentRoute: () => rootRoute, } as any).lazy(() => import('./routes/_layout.lazy').then((d) => d.Route)) -const EditorIndexRoute = EditorIndexImport.update({ - path: '/editor/', +const NewPostIndexRoute = NewPostIndexImport.update({ + path: '/new-post/', getParentRoute: () => rootRoute, } as any) @@ -448,11 +448,11 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LayoutIndexImport parentRoute: typeof LayoutImport } - '/editor/': { - id: '/editor/' - path: '/editor' - fullPath: '/editor' - preLoaderRoute: typeof EditorIndexImport + '/new-post/': { + id: '/new-post/' + path: '/new-post' + fullPath: '/new-post' + preLoaderRoute: typeof NewPostIndexImport parentRoute: typeof rootRoute } '/columns/_layout/create-newsfeed': { @@ -689,7 +689,7 @@ export interface FileRoutesByFullPath { '/auth/import': typeof AuthImportLazyRoute '/auth/watch': typeof AuthWatchLazyRoute '/': typeof LayoutIndexRoute - '/editor': typeof EditorIndexRoute + '/new-post': typeof NewPostIndexRoute '/columns/create-newsfeed': typeof ColumnsLayoutCreateNewsfeedRouteWithChildren '/columns/global': typeof ColumnsLayoutGlobalRoute '/columns/launchpad': typeof ColumnsLayoutLaunchpadLazyRoute @@ -727,7 +727,7 @@ export interface FileRoutesByTo { '/auth/import': typeof AuthImportLazyRoute '/auth/watch': typeof AuthWatchLazyRoute '/': typeof LayoutIndexRoute - '/editor': typeof EditorIndexRoute + '/new-post': typeof NewPostIndexRoute '/columns/create-newsfeed': typeof ColumnsLayoutCreateNewsfeedRouteWithChildren '/columns/global': typeof ColumnsLayoutGlobalRoute '/columns/launchpad': typeof ColumnsLayoutLaunchpadLazyRoute @@ -768,7 +768,7 @@ export interface FileRoutesById { '/auth/import': typeof AuthImportLazyRoute '/auth/watch': typeof AuthWatchLazyRoute '/_layout/': typeof LayoutIndexRoute - '/editor/': typeof EditorIndexRoute + '/new-post/': typeof NewPostIndexRoute '/columns/_layout/create-newsfeed': typeof ColumnsLayoutCreateNewsfeedRouteWithChildren '/columns/_layout/global': typeof ColumnsLayoutGlobalRoute '/columns/_layout/launchpad': typeof ColumnsLayoutLaunchpadLazyRoute @@ -808,7 +808,7 @@ export interface FileRouteTypes { | '/auth/import' | '/auth/watch' | '/' - | '/editor' + | '/new-post' | '/columns/create-newsfeed' | '/columns/global' | '/columns/launchpad' @@ -845,7 +845,7 @@ export interface FileRouteTypes { | '/auth/import' | '/auth/watch' | '/' - | '/editor' + | '/new-post' | '/columns/create-newsfeed' | '/columns/global' | '/columns/launchpad' @@ -884,7 +884,7 @@ export interface FileRouteTypes { | '/auth/import' | '/auth/watch' | '/_layout/' - | '/editor/' + | '/new-post/' | '/columns/_layout/create-newsfeed' | '/columns/_layout/global' | '/columns/_layout/launchpad' @@ -918,7 +918,7 @@ export interface RootRouteChildren { AuthConnectLazyRoute: typeof AuthConnectLazyRoute AuthImportLazyRoute: typeof AuthImportLazyRoute AuthWatchLazyRoute: typeof AuthWatchLazyRoute - EditorIndexRoute: typeof EditorIndexRoute + NewPostIndexRoute: typeof NewPostIndexRoute } const rootRouteChildren: RootRouteChildren = { @@ -935,7 +935,7 @@ const rootRouteChildren: RootRouteChildren = { AuthConnectLazyRoute: AuthConnectLazyRoute, AuthImportLazyRoute: AuthImportLazyRoute, AuthWatchLazyRoute: AuthWatchLazyRoute, - EditorIndexRoute: EditorIndexRoute, + NewPostIndexRoute: NewPostIndexRoute, } export const routeTree = rootRoute @@ -963,7 +963,7 @@ export const routeTree = rootRoute "/auth/connect", "/auth/import", "/auth/watch", - "/editor/" + "/new-post/" ] }, "/_layout": { @@ -1062,8 +1062,8 @@ export const routeTree = rootRoute "filePath": "_layout/index.tsx", "parent": "/_layout" }, - "/editor/": { - "filePath": "editor/index.tsx" + "/new-post/": { + "filePath": "new-post/index.tsx" }, "/columns/_layout/create-newsfeed": { "filePath": "columns/_layout/create-newsfeed.tsx", diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 1cb016b7..09056444 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -9,7 +9,7 @@ import { useEffect } from "react"; interface RouterContext { queryClient: QueryClient; platform: OsType; - account: string[]; + accounts: string[]; } export const Route = createRootRouteWithContext()({ diff --git a/src/routes/_layout.lazy.tsx b/src/routes/_layout.lazy.tsx index af9e9ebb..0eaa8381 100644 --- a/src/routes/_layout.lazy.tsx +++ b/src/routes/_layout.lazy.tsx @@ -1,8 +1,9 @@ import { commands } from "@/commands.gen"; import { cn } from "@/commons"; +import { PublishIcon } from "@/components"; import { User } from "@/components/user"; import { LumeWindow } from "@/system"; -import { Feather, MagnifyingGlass, Plus } from "@phosphor-icons/react"; +import { MagnifyingGlass, Plus } from "@phosphor-icons/react"; import { Link, Outlet, createLazyFileRoute } from "@tanstack/react-router"; import { listen } from "@tauri-apps/api/event"; import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu"; @@ -59,9 +60,9 @@ function Topbar() { - ) - ) : ( - - )} - - - ))} - -
-
- -
- - New account - -
-
- - - - - ) -} -*/ diff --git a/src/routes/editor/-components/media.tsx b/src/routes/new-post/-components/media.tsx similarity index 90% rename from src/routes/editor/-components/media.tsx rename to src/routes/new-post/-components/media.tsx index 29a171f3..6c5a0b6d 100644 --- a/src/routes/editor/-components/media.tsx +++ b/src/routes/new-post/-components/media.tsx @@ -12,10 +12,8 @@ import { export function MediaButton({ setText, - setAttaches, }: { setText: Dispatch>; - setAttaches: Dispatch>; }) { const [isPending, startTransition] = useTransition(); @@ -24,8 +22,6 @@ export function MediaButton({ try { const image = await upload(); setText((prev) => `${prev}\n${image}`); - setAttaches((prev) => [...prev, image]); - return; } catch (e) { await message(String(e), { title: "Upload", kind: "error" }); return; @@ -44,7 +40,6 @@ export function MediaButton({ if (isImagePath(item)) { const image = await upload(item); setText((prev) => `${prev}\n${image}`); - setAttaches((prev) => [...prev, image]); } } diff --git a/src/routes/editor/-components/pow.tsx b/src/routes/new-post/-components/pow.tsx similarity index 100% rename from src/routes/editor/-components/pow.tsx rename to src/routes/new-post/-components/pow.tsx diff --git a/src/routes/editor/-components/warning.tsx b/src/routes/new-post/-components/warning.tsx similarity index 100% rename from src/routes/editor/-components/warning.tsx rename to src/routes/new-post/-components/warning.tsx diff --git a/src/routes/editor/index.tsx b/src/routes/new-post/index.tsx similarity index 64% rename from src/routes/editor/index.tsx rename to src/routes/new-post/index.tsx index b46d30e6..9324db80 100644 --- a/src/routes/editor/index.tsx +++ b/src/routes/new-post/index.tsx @@ -1,15 +1,23 @@ -// @ts-nocheck -import { type Mention, commands } from "@/commands.gen"; -import { cn } from "@/commons"; -import { Spinner } from "@/components"; +import { type Mention, type Result, commands } from "@/commands.gen"; +import { cn, displayNpub } from "@/commons"; +import { PublishIcon, Spinner } from "@/components"; import { Note } from "@/components/note"; import { User } from "@/components/user"; -import { LumeEvent, useEvent } from "@/system"; -import { Feather } from "@phosphor-icons/react"; +import { LumeWindow, useEvent } from "@/system"; +import type { Metadata } from "@/types"; +import { CaretDown } from "@phosphor-icons/react"; import { createFileRoute } from "@tanstack/react-router"; -import { getCurrentWindow } from "@tauri-apps/api/window"; +import { Menu, MenuItem } from "@tauri-apps/api/menu"; +import type { Window } from "@tauri-apps/api/window"; import { nip19 } from "nostr-tools"; -import { useEffect, useMemo, useRef, useState, useTransition } from "react"; +import { + useCallback, + useEffect, + useMemo, + useRef, + useState, + useTransition, +} from "react"; import { createPortal } from "react-dom"; import { RichTextarea, @@ -45,7 +53,15 @@ const renderer = createRegexRenderer([ ], [ /(?:^|\W)nostr:(\w+)(?!\w)/g, - ({ children, key, value }) => ( + ({ children, key }) => ( + + {children} + + ), + ], + [ + /(?:^|\W)#(\w+)(?!\w)/g, + ({ children, key }) => ( {children} @@ -53,7 +69,7 @@ const renderer = createRegexRenderer([ ], ]); -export const Route = createFileRoute("/editor/")({ +export const Route = createFileRoute("/new-post/")({ validateSearch: (search: Record): EditorSearch => { return { reply_to: search.reply_to, @@ -70,25 +86,28 @@ export const Route = createFileRoute("/editor/")({ initialValue = ""; } - const res = await commands.getMentionList(); + const res = await commands.getAllProfiles(); + const accounts = await commands.getAccounts(); if (res.status === "ok") { users = res.data; } - return { users, initialValue }; + return { accounts, users, initialValue }; }, component: Screen, }); function Screen() { const { reply_to } = Route.useSearch(); - const { users, initialValue } = Route.useRouteContext(); + const { accounts, users, initialValue } = Route.useRouteContext(); const [text, setText] = useState(""); + const [currentUser, setCurrentUser] = useState(null); + const [popup, setPopup] = useState(null); + const [isPublish, setIsPublish] = useState(false); const [error, setError] = useState(""); const [isPending, startTransition] = useTransition(); - const [attaches, setAttaches] = useState(null); const [warning, setWarning] = useState({ enable: false, reason: "" }); const [difficulty, setDifficulty] = useState({ enable: false, num: 21 }); const [index, setIndex] = useState(0); @@ -110,6 +129,34 @@ function Screen() { [name], ); + const showContextMenu = useCallback(async (e: React.MouseEvent) => { + e.preventDefault(); + + const list = []; + + 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: `Publish as ${name} (${displayNpub(account, 16)})`, + action: async () => setCurrentUser(account), + }), + ); + } + + const items = await Promise.all(list); + const menu = await Menu.new({ items }); + + await menu.popup().catch((e) => console.error(e)); + }, []); + const insert = (i: number) => { if (!ref.current || !pos) return; @@ -126,41 +173,84 @@ function Screen() { setIndex(0); }; - const publish = async () => { + const publish = () => { startTransition(async () => { - try { - // Temporary hide window - await getCurrentWindow().hide(); + const content = text.trim(); + const warn = warning.enable ? warning.reason : undefined; + const diff = difficulty.enable ? difficulty.num : undefined; - let res: Result; + let res: Result; - if (reply_to) { - res = await commands.reply(content, reply_to, root_to); - } else { - res = await commands.publish(content, warning, difficulty); - } + if (reply_to?.length) { + res = await commands.reply(content, reply_to, undefined); + } else { + res = await commands.publish(content, warn, diff); + } - if (res.status === "ok") { - setText(""); - // Close window - await getCurrentWindow().close(); - } else { - setError(res.error); - // Focus window - await getCurrentWindow().setFocus(); - } - } catch { - return; + if (res.status === "ok") { + setText(""); + setIsPublish(true); + } else { + setError(res.error); } }); }; + const submit = async () => { + if (currentUser) { + const signer = await commands.hasSigner(currentUser); + + if (signer.status === "ok") { + if (!signer.data) { + const newPopup = await LumeWindow.openPopup( + `/set-signer?account=${currentUser}`, + undefined, + false, + ); + + setPopup(newPopup); + return; + } + + publish(); + } + } + }; + + useEffect(() => { + if (!popup) return; + + const unlisten = popup.listen("signer-updated", () => { + publish(); + }); + + return () => { + unlisten.then((f) => f()); + }; + }, [popup]); + + useEffect(() => { + if (isPublish) { + const timer = setTimeout(() => setIsPublish((prev) => !prev), 5000); + + return () => { + clearTimeout(timer); + }; + } + }, [isPublish]); + useEffect(() => { if (initialValue?.length) { setText(initialValue); } }, [initialValue]); + useEffect(() => { + if (accounts?.length) { + setCurrentUser(accounts[0]); + } + }, [accounts]); + return (
@@ -229,21 +319,24 @@ function Screen() { setIndex(0); } }} + disabled={isPending} > {renderer} - {pos - ? createPortal( - , - document.body, - ) - : null} + {pos ? ( + createPortal( + , + document.body, + ) + ) : ( + <> + )}
{warning.enable ? ( @@ -289,20 +382,44 @@ function Screen() { data-tauri-drag-region className="flex items-center w-full h-16 gap-4 px-4 border-t divide-x divide-black/5 dark:divide-white/5 shrink-0 border-black/5 dark:border-white/5" > - -
- +
+ + {currentUser ? ( + + ) : null} +
+
+
@@ -311,7 +428,7 @@ function Screen() { ); } -function Menu({ +function MentionPopup({ users, index, top, diff --git a/src/system/window.ts b/src/system/window.ts index 3a84bf6e..708ab197 100644 --- a/src/system/window.ts +++ b/src/system/window.ts @@ -99,22 +99,22 @@ export const LumeWindow = { let url: string; if (reply_to) { - url = `/editor?reply_to=${reply_to}`; + url = `/new-post?reply_to=${reply_to}`; } if (quote?.length) { - url = `/editor?quote=${quote}`; + url = `/new-post?quote=${quote}`; } if (!reply_to?.length && !quote?.length) { - url = "/editor"; + url = "/new-post"; } const label = `editor-${reply_to ? reply_to : 0}`; const query = await commands.openWindow({ label, url, - title: "Editor", + title: "New Post", width: 560, height: 340, maximizable: false,