feat: publish with multi account

This commit is contained in:
reya 2024-10-22 09:25:41 +07:00
parent 16e85c437d
commit 62ba8a985f
15 changed files with 287 additions and 394 deletions

View File

@ -415,7 +415,7 @@ pub async fn get_all_interests(state: State<'_, Nostr>) -> Result<Vec<RichEvent>
#[tauri::command]
#[specta::specta]
pub async fn get_mention_list(state: State<'_, Nostr>) -> Result<Vec<Mention>, String> {
pub async fn get_all_profiles(state: State<'_, Nostr>) -> Result<Vec<Mention>, String> {
let client = &state.client;
let filter = Filter::new().kind(Kind::Metadata);

View File

@ -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<Tag> {
let hashtags = words
.iter()
.filter(|&&word| word.starts_with('#'))
.map(|&s| s.to_string())
.map(|&s| s.to_string().replace("#", "").to_lowercase())
.collect::<Vec<_>>();
for mention in mentions {
@ -227,38 +227,6 @@ pub async fn process_event(client: &Client, events: Events) -> Vec<RichEvent> {
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);

View File

@ -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<PublicKey> = 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<PublicKey> = 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)
}
}
}
}

View File

@ -160,9 +160,9 @@ async toggleContact(id: string, alias: string | null) : Promise<Result<string, s
else return { status: "error", error: e as any };
}
},
async getMentionList() : Promise<Result<Mention[], string>> {
async getAllProfiles() : Promise<Result<Mention[], string>> {
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 };

View File

@ -0,0 +1,19 @@
import type { SVGProps } from "react";
export const PublishIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
fill="currentColor"
fillRule="evenodd"
d="M19.432 2.738c.505.54.728 1.327.443 2.133-.606 1.713-1.798 3.124-2.797 4.087a15.74 15.74 0 01-1.045.921l.137.1c.93.684 1.416 1.975.757 3.118-1.221 2.12-4.356 5.803-11.192 5.803a.753.753 0 01-.15-.015A32.702 32.702 0 005.5 21.25a.75.75 0 01-1.5 0c0-4.43.821-8.93 2.909-12.485 2.106-3.587 5.49-6.182 10.492-6.749a2.404 2.404 0 012.031.722z"
clipRule="evenodd"
/>
</svg>
);

View File

@ -17,3 +17,4 @@ export * from "./icons/reply";
export * from "./icons/repost";
export * from "./icons/zap";
export * from "./icons/quote";
export * from "./icons/publish";

View File

@ -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",

View File

@ -9,7 +9,7 @@ import { useEffect } from "react";
interface RouterContext {
queryClient: QueryClient;
platform: OsType;
account: string[];
accounts: string[];
}
export const Route = createRootRouteWithContext<RouterContext>()({

View File

@ -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() {
<button
type="button"
onClick={() => LumeWindow.openEditor()}
className="inline-flex items-center justify-center h-7 gap-1.5 px-2 text-sm font-medium bg-black/5 dark:bg-white/5 rounded-full w-max hover:bg-blue-500 hover:text-white"
className="inline-flex items-center justify-center h-7 gap-1 px-2 text-sm font-medium bg-black/5 dark:bg-white/5 rounded-full w-max hover:bg-blue-500 hover:text-white"
>
<Feather className="size-4" weight="fill" />
<PublishIcon className="size-4" />
New Post
</button>
<button

View File

@ -236,223 +236,3 @@ function Toolbar({ children }: { children: ReactNode[] }) {
<></>
);
}
/*
function Screen() {
const context = Route.useRouteContext()
const navigate = Route.useNavigate()
const currentDate = useMemo(
() =>
new Date().toLocaleString('default', {
weekday: 'long',
month: 'long',
day: 'numeric',
}),
[],
)
const [accounts, setAccounts] = useState([])
const [value, setValue] = useState('')
const [autoLogin, setAutoLogin] = useState(false)
const [password, setPassword] = useState('')
const [isPending, startTransition] = useTransition()
const showContextMenu = useCallback(
async (e: React.MouseEvent, account: string) => {
e.stopPropagation()
const menuItems = await Promise.all([
MenuItem.new({
text: 'Reset password',
enabled: !account.includes('_nostrconnect'),
// @ts-ignore, this is tanstack router bug
action: () => navigate({ to: '/reset', search: { account } }),
}),
MenuItem.new({
text: 'Delete account',
action: async () => await deleteAccount(account),
}),
])
const menu = await Menu.new({
items: menuItems,
})
await menu.popup().catch((e) => console.error(e))
},
[],
)
const deleteAccount = async (account: string) => {
const res = await commands.deleteAccount(account)
if (res.status === 'ok') {
setAccounts((prev) => prev.filter((item) => item !== account))
}
}
const selectAccount = (account: string) => {
setValue(account)
if (account.includes('_nostrconnect')) {
setAutoLogin(true)
}
}
const loginWith = () => {
startTransition(async () => {
const res = await commands.login(value, password)
if (res.status === 'ok') {
const settings = await commands.getUserSettings()
if (settings.status === 'ok') {
appSettings.setState(() => settings.data)
}
const status = await commands.isAccountSync(res.data)
if (status) {
navigate({
to: '/$account/home',
// @ts-ignore, this is tanstack router bug
params: { account: res.data },
replace: true,
})
} else {
navigate({
to: '/loading',
// @ts-ignore, this is tanstack router bug
search: { account: res.data },
replace: true,
})
}
} else {
await message(res.error, { title: 'Login', kind: 'error' })
return
}
})
}
useEffect(() => {
if (autoLogin) {
loginWith()
}
}, [autoLogin, value])
useEffect(() => {
setAccounts(context.accounts)
}, [context.accounts])
return (
<div
data-tauri-drag-region
className="relative size-full flex items-center justify-center"
>
<div className="w-[320px] flex flex-col gap-8">
<div className="flex flex-col gap-1 text-center">
<h3 className="leading-tight text-neutral-700 dark:text-neutral-300">
{currentDate}
</h3>
<h1 className="leading-tight text-xl font-semibold">Welcome back!</h1>
</div>
<Frame
className="flex flex-col w-full divide-y divide-neutral-100 dark:divide-white/5 rounded-xl overflow-hidden"
shadow
>
{accounts.map((account) => (
<div
key={account}
onClick={() => selectAccount(account)}
onKeyDown={() => selectAccount(account)}
className="group flex items-center gap-2 hover:bg-black/5 dark:hover:bg-white/5 p-3"
>
<User.Provider pubkey={account.replace('_nostrconnect', '')}>
<User.Root className="flex-1 flex items-center gap-2.5">
<User.Avatar className="rounded-full size-10" />
{value === account && !value.includes('_nostrconnect') ? (
<div className="flex-1 flex items-center gap-2">
<input
name="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') loginWith()
}}
disabled={isPending}
placeholder="Password"
className="px-3 rounded-full w-full h-10 bg-transparent border border-neutral-200 dark:border-neutral-500 focus:border-blue-500 focus:outline-none placeholder:text-neutral-400"
/>
</div>
) : (
<div className="inline-flex flex-col items-start">
<div className="inline-flex items-center gap-1.5">
<User.Name className="max-w-[6rem] truncate font-medium leading-tight" />
{account.includes('_nostrconnect') ? (
<div className="text-[8px] border border-blue-500 text-blue-500 px-1.5 rounded-full">
Nostr Connect
</div>
) : null}
</div>
<span className="text-sm text-neutral-700 dark:text-neutral-300">
{displayNpub(account.replace('_nostrconnect', ''), 16)}
</span>
</div>
)}
</User.Root>
</User.Provider>
<div className="inline-flex items-center justify-center size-8 shrink-0">
{value === account ? (
isPending ? (
<Spinner />
) : (
<button
type="button"
onClick={() => loginWith()}
className="rounded-full size-10 inline-flex items-center justify-center"
>
<ArrowRight className="size-5" />
</button>
)
) : (
<button
type="button"
onClick={(e) => showContextMenu(e, account)}
className="rounded-full size-10 hidden group-hover:inline-flex items-center justify-center"
>
<DotsThree className="size-5" />
</button>
)}
</div>
</div>
))}
<a
href="/new"
className="flex items-center justify-between hover:bg-black/5 dark:hover:bg-white/5"
>
<div className="flex items-center gap-2.5 p-3">
<div className="inline-flex items-center justify-center rounded-full size-10 bg-neutral-200 dark:bg-white/10">
<Plus className="size-5" />
</div>
<span className="truncate text-sm font-medium leading-tight">
New account
</span>
</div>
</a>
</Frame>
</div>
<div className="absolute bottom-2 right-2">
<a
href="/bootstrap-relays"
className="h-8 w-max text-xs px-3 inline-flex items-center justify-center gap-1.5 bg-black/5 hover:bg-black/10 dark:bg-white/5 dark:hover:bg-white/10 rounded-full"
>
<GearSix className="size-4" />
Manage Relays
</a>
</div>
</div>
)
}
*/

View File

@ -12,10 +12,8 @@ import {
export function MediaButton({
setText,
setAttaches,
}: {
setText: Dispatch<SetStateAction<string>>;
setAttaches: Dispatch<SetStateAction<string[]>>;
}) {
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]);
}
}

View File

@ -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 }) => (
<span key={key} className="text-blue-500">
{children}
</span>
),
],
[
/(?:^|\W)#(\w+)(?!\w)/g,
({ children, key }) => (
<span key={key} className="text-blue-500">
{children}
</span>
@ -53,7 +69,7 @@ const renderer = createRegexRenderer([
],
]);
export const Route = createFileRoute("/editor/")({
export const Route = createFileRoute("/new-post/")({
validateSearch: (search: Record<string, string>): 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<string>(null);
const [popup, setPopup] = useState<Window>(null);
const [isPublish, setIsPublish] = useState(false);
const [error, setError] = useState("");
const [isPending, startTransition] = useTransition();
const [attaches, setAttaches] = useState<string[]>(null);
const [warning, setWarning] = useState({ enable: false, reason: "" });
const [difficulty, setDifficulty] = useState({ enable: false, num: 21 });
const [index, setIndex] = useState<number>(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<string, string>;
let res: Result<string, string>;
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 (
<div className="flex flex-col w-full h-full">
<div data-tauri-drag-region className="h-11 shrink-0" />
@ -229,21 +319,24 @@ function Screen() {
setIndex(0);
}
}}
disabled={isPending}
>
{renderer}
</RichTextarea>
{pos
? createPortal(
<Menu
top={pos.top}
left={pos.left}
users={filtered}
index={index}
insert={insert}
/>,
document.body,
)
: null}
{pos ? (
createPortal(
<MentionPopup
top={pos.top}
left={pos.left}
users={filtered}
index={index}
insert={insert}
/>,
document.body,
)
) : (
<></>
)}
</div>
</div>
{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"
>
<button
type="button"
onClick={() => publish()}
className="inline-flex items-center justify-center h-8 gap-1 px-2.5 text-sm font-medium rounded-lg bg-black/10 w-max hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20"
>
{isPending ? (
<Spinner className="size-4" />
) : (
<Feather className="size-4" weight="fill" />
)}
Publish
</button>
<div className="inline-flex items-center flex-1 gap-2 pl-4">
<MediaButton setText={setText} setAttaches={setAttaches} />
<div className="inline-flex items-center gap-3">
<button
type="button"
onClick={() => submit()}
className={cn(
"inline-flex items-center justify-center h-8 gap-1 px-2.5 text-sm font-medium rounded-lg w-max",
isPublish
? "bg-green-500 hover:bg-green-600 dark:hover:bg-green-400 text-white"
: "bg-black/10 hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20",
)}
>
{isPending ? (
<Spinner className="size-4" />
) : (
<PublishIcon className="size-4" />
)}
{isPublish ? "Published" : "Publish"}
</button>
{currentUser ? (
<button
type="button"
onClick={(e) => showContextMenu(e)}
className="inline-flex items-center gap-1.5"
>
<User.Provider pubkey={currentUser}>
<User.Root>
<User.Avatar className="size-6 rounded-full" />
</User.Root>
</User.Provider>
<CaretDown
className="mt-px size-3 text-neutral-500"
weight="bold"
/>
</button>
) : null}
</div>
<div className="inline-flex items-center flex-1 gap-2 pl-2">
<MediaButton setText={setText} />
<WarningButton setWarning={setWarning} />
<PowButton setDifficulty={setDifficulty} />
</div>
@ -311,7 +428,7 @@ function Screen() {
);
}
function Menu({
function MentionPopup({
users,
index,
top,

View File

@ -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,