mirror of
https://github.com/lumehq/lume.git
synced 2025-03-17 21:32:32 +01:00
feat: publish with multi account
This commit is contained in:
parent
16e85c437d
commit
62ba8a985f
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 };
|
||||
|
19
src/components/icons/publish.tsx
Normal file
19
src/components/icons/publish.tsx
Normal 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>
|
||||
);
|
@ -17,3 +17,4 @@ export * from "./icons/reply";
|
||||
export * from "./icons/repost";
|
||||
export * from "./icons/zap";
|
||||
export * from "./icons/quote";
|
||||
export * from "./icons/publish";
|
||||
|
@ -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",
|
||||
|
@ -9,7 +9,7 @@ import { useEffect } from "react";
|
||||
interface RouterContext {
|
||||
queryClient: QueryClient;
|
||||
platform: OsType;
|
||||
account: string[];
|
||||
accounts: string[];
|
||||
}
|
||||
|
||||
export const Route = createRootRouteWithContext<RouterContext>()({
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
*/
|
||||
|
@ -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]);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user