mirror of
https://github.com/lumehq/lume.git
synced 2025-06-22 13:00:47 +02:00
parent
174a3cc74e
commit
f027eae52d
@ -12,21 +12,13 @@ import { routeTree } from "./router.gen"; // auto generated file
|
|||||||
import { CancelCircleIcon, CheckCircleIcon, InfoCircleIcon } from "@lume/icons";
|
import { CancelCircleIcon, CheckCircleIcon, InfoCircleIcon } from "@lume/icons";
|
||||||
import { Ark } from "@lume/ark";
|
import { Ark } from "@lume/ark";
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const ark = new Ark();
|
||||||
defaultOptions: {
|
const queryClient = new QueryClient();
|
||||||
queries: {
|
|
||||||
gcTime: 1000 * 60 * 60 * 24, // 24 hours
|
|
||||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const persister = createSyncStoragePersister({
|
const persister = createSyncStoragePersister({
|
||||||
storage: window.localStorage,
|
storage: window.localStorage,
|
||||||
});
|
});
|
||||||
|
|
||||||
const ark = new Ark();
|
|
||||||
|
|
||||||
// Set up a Router instance
|
// Set up a Router instance
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
routeTree,
|
routeTree,
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { LumeColumn } from "@lume/types";
|
import { LumeColumn } from "@lume/types";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { Spinner } from "@lume/ui";
|
|
||||||
import { cn } from "@lume/utils";
|
|
||||||
|
|
||||||
export function Col({
|
export function Col({
|
||||||
column,
|
column,
|
||||||
@ -81,20 +79,5 @@ export function Col({
|
|||||||
};
|
};
|
||||||
}, [webview]);
|
}, [webview]);
|
||||||
|
|
||||||
return (
|
return <div ref={container} className="h-full w-[440px] shrink-0 p-2" />;
|
||||||
<div ref={container} className="h-full w-[440px] shrink-0 p-2">
|
|
||||||
{column.label !== "open" ? (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"w-full h-full flex items-center justify-center rounded-xl flex-col",
|
|
||||||
!webview ? "bg-black/5 dark:bg-white/5 backdrop-blur-lg" : "",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<button type="button" className="size-5" disabled>
|
|
||||||
<Spinner className="size-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,13 @@ import { Outlet, createRootRouteWithContext } from "@tanstack/react-router";
|
|||||||
import { type Ark } from "@lume/ark";
|
import { type Ark } from "@lume/ark";
|
||||||
import { type QueryClient } from "@tanstack/react-query";
|
import { type QueryClient } from "@tanstack/react-query";
|
||||||
import { type Platform } from "@tauri-apps/plugin-os";
|
import { type Platform } from "@tauri-apps/plugin-os";
|
||||||
import type { Account, Interests, Metadata, Settings } from "@lume/types";
|
import type {
|
||||||
|
Account,
|
||||||
|
Contact,
|
||||||
|
Interests,
|
||||||
|
Metadata,
|
||||||
|
Settings,
|
||||||
|
} from "@lume/types";
|
||||||
import { Spinner } from "@lume/ui";
|
import { Spinner } from "@lume/ui";
|
||||||
import { type Descendant } from "slate";
|
import { type Descendant } from "slate";
|
||||||
|
|
||||||
@ -22,6 +28,7 @@ interface RouterContext {
|
|||||||
accounts?: Account[];
|
accounts?: Account[];
|
||||||
initialValue?: EditorElement[];
|
initialValue?: EditorElement[];
|
||||||
profile?: Metadata;
|
profile?: Metadata;
|
||||||
|
contacts?: Contact[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Route = createRootRouteWithContext<RouterContext>()({
|
export const Route = createRootRouteWithContext<RouterContext>()({
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ComposeFilledIcon, NsfwIcon, TrashIcon } from "@lume/icons";
|
import { ComposeFilledIcon, TrashIcon } from "@lume/icons";
|
||||||
import {
|
import {
|
||||||
Portal,
|
Portal,
|
||||||
cn,
|
cn,
|
||||||
@ -33,7 +33,6 @@ import {
|
|||||||
import { Contact } from "@lume/types";
|
import { Contact } from "@lume/types";
|
||||||
import { Spinner, User } from "@lume/ui";
|
import { Spinner, User } from "@lume/ui";
|
||||||
import { nip19 } from "nostr-tools";
|
import { nip19 } from "nostr-tools";
|
||||||
import { queryOptions, useSuspenseQuery } from "@tanstack/react-query";
|
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { NsfwToggle } from "./-components/nsfw";
|
import { NsfwToggle } from "./-components/nsfw";
|
||||||
|
|
||||||
@ -42,14 +41,6 @@ type EditorSearch = {
|
|||||||
quote: boolean;
|
quote: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const contactQueryOptions = queryOptions({
|
|
||||||
queryKey: ["contacts"],
|
|
||||||
queryFn: () => invoke("get_contact_metadata"),
|
|
||||||
refetchOnMount: false,
|
|
||||||
refetchOnReconnect: false,
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/editor/")({
|
export const Route = createFileRoute("/editor/")({
|
||||||
validateSearch: (search: Record<string, string>): EditorSearch => {
|
validateSearch: (search: Record<string, string>): EditorSearch => {
|
||||||
return {
|
return {
|
||||||
@ -58,7 +49,10 @@ export const Route = createFileRoute("/editor/")({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
beforeLoad: async ({ search }) => {
|
beforeLoad: async ({ search }) => {
|
||||||
|
const contacts: Contact[] = await invoke("get_contact_metadata");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
contacts,
|
||||||
initialValue: search.quote
|
initialValue: search.quote
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
@ -83,16 +77,14 @@ export const Route = createFileRoute("/editor/")({
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
loader: ({ context }) => {
|
|
||||||
context.queryClient.ensureQueryData(contactQueryOptions);
|
|
||||||
},
|
|
||||||
component: Screen,
|
component: Screen,
|
||||||
pendingComponent: Pending,
|
pendingComponent: Pending,
|
||||||
});
|
});
|
||||||
|
|
||||||
function Screen() {
|
function Screen() {
|
||||||
|
const ref = useRef<HTMLDivElement | null>();
|
||||||
const { reply_to, quote } = Route.useSearch();
|
const { reply_to, quote } = Route.useSearch();
|
||||||
const { ark, initialValue } = Route.useRouteContext();
|
const { ark, initialValue, contacts } = Route.useRouteContext();
|
||||||
|
|
||||||
const [t] = useTranslation();
|
const [t] = useTranslation();
|
||||||
const [editorValue, setEditorValue] = useState(initialValue);
|
const [editorValue, setEditorValue] = useState(initialValue);
|
||||||
@ -105,9 +97,6 @@ function Screen() {
|
|||||||
withMentions(withNostrEvent(withImages(withReact(createEditor())))),
|
withMentions(withNostrEvent(withImages(withReact(createEditor())))),
|
||||||
);
|
);
|
||||||
|
|
||||||
const ref = useRef<HTMLDivElement | null>();
|
|
||||||
const contacts = useSuspenseQuery(contactQueryOptions).data as Contact[];
|
|
||||||
|
|
||||||
const filters = contacts
|
const filters = contacts
|
||||||
?.filter((c) =>
|
?.filter((c) =>
|
||||||
c?.profile.name?.toLowerCase().startsWith(search.toLowerCase()),
|
c?.profile.name?.toLowerCase().startsWith(search.toLowerCase()),
|
||||||
|
@ -27,8 +27,14 @@ export const Route = createFileRoute("/newsfeed")({
|
|||||||
export function Screen() {
|
export function Screen() {
|
||||||
const { label, name, account } = Route.useSearch();
|
const { label, name, account } = Route.useSearch();
|
||||||
const { ark } = Route.useRouteContext();
|
const { ark } = Route.useRouteContext();
|
||||||
const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage } =
|
const {
|
||||||
useInfiniteQuery({
|
data,
|
||||||
|
isLoading,
|
||||||
|
isFetching,
|
||||||
|
isFetchingNextPage,
|
||||||
|
hasNextPage,
|
||||||
|
fetchNextPage,
|
||||||
|
} = useInfiniteQuery({
|
||||||
queryKey: [label, account],
|
queryKey: [label, account],
|
||||||
initialPageParam: 0,
|
initialPageParam: 0,
|
||||||
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
||||||
@ -40,7 +46,6 @@ export function Screen() {
|
|||||||
return lastEvent ? lastEvent.created_at - 1 : null;
|
return lastEvent ? lastEvent.created_at - 1 : null;
|
||||||
},
|
},
|
||||||
select: (data) => data?.pages.flatMap((page) => page),
|
select: (data) => data?.pages.flatMap((page) => page),
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderItem = (event: Event) => {
|
const renderItem = (event: Event) => {
|
||||||
@ -57,9 +62,18 @@ export function Screen() {
|
|||||||
<Column.Root>
|
<Column.Root>
|
||||||
<Column.Header label={label} name={name} />
|
<Column.Header label={label} name={name} />
|
||||||
<Column.Content>
|
<Column.Content>
|
||||||
{isLoading ? (
|
{isFetching && !isLoading && !isFetchingNextPage ? (
|
||||||
<div className="flex h-20 w-full flex-col items-center justify-center gap-1">
|
<div className="w-full h-16 flex items-center justify-center border-b border-neutral-100 dark:border-neutral-900">
|
||||||
|
<div className="flex items-center justify-center gap-2">
|
||||||
<Spinner className="size-5" />
|
<Spinner className="size-5" />
|
||||||
|
<span className="text-sm font-medium">Fetching new notes...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex h-16 w-full items-center justify-center gap-2">
|
||||||
|
<Spinner className="size-5" />
|
||||||
|
<span className="text-sm font-medium">Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
) : !data.length ? (
|
) : !data.length ? (
|
||||||
<Empty />
|
<Empty />
|
||||||
|
@ -122,15 +122,15 @@ export class Ark {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async get_events_from(id: string, limit: number, asOf?: number) {
|
public async get_events_from(pubkey: string, limit: number, asOf?: number) {
|
||||||
try {
|
try {
|
||||||
let until: string = undefined;
|
let until: string = undefined;
|
||||||
if (asOf && asOf > 0) until = asOf.toString();
|
if (asOf && asOf > 0) until = asOf.toString();
|
||||||
|
|
||||||
const nostrEvents: Event[] = await invoke("get_events_from", {
|
const nostrEvents: Event[] = await invoke("get_events_from", {
|
||||||
id,
|
public_key: pubkey,
|
||||||
limit,
|
limit,
|
||||||
until,
|
as_of: until,
|
||||||
});
|
});
|
||||||
|
|
||||||
return nostrEvents.sort((a, b) => b.created_at - a.created_at);
|
return nostrEvents.sort((a, b) => b.created_at - a.created_at);
|
||||||
|
@ -11,7 +11,7 @@ export function ColumnContent({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex-1 overflow-y-auto overflow-x-hidden scrollbar-none",
|
"relative flex-1 overflow-y-auto overflow-x-hidden scrollbar-none",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
@ -54,6 +54,10 @@ fn main() {
|
|||||||
|
|
||||||
// Add some bootstrap relays
|
// Add some bootstrap relays
|
||||||
// #TODO: Pull bootstrap relays from user's settings
|
// #TODO: Pull bootstrap relays from user's settings
|
||||||
|
client
|
||||||
|
.add_relay("wss://relay.damus.io")
|
||||||
|
.await
|
||||||
|
.expect("Cannot connect to relay.damus.io, please try again later.");
|
||||||
client
|
client
|
||||||
.add_relay("wss://relayable.org")
|
.add_relay("wss://relayable.org")
|
||||||
.await
|
.await
|
||||||
|
@ -40,34 +40,33 @@ pub async fn get_event(id: &str, state: State<'_, Nostr>) -> Result<String, Stri
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_events_from(
|
pub async fn get_events_from(
|
||||||
id: &str,
|
public_key: &str,
|
||||||
limit: usize,
|
limit: usize,
|
||||||
until: Option<&str>,
|
as_of: Option<&str>,
|
||||||
state: State<'_, Nostr>,
|
state: State<'_, Nostr>,
|
||||||
) -> Result<Vec<Event>, String> {
|
) -> Result<Vec<Event>, String> {
|
||||||
let client = &state.client;
|
let client = &state.client;
|
||||||
let f_until = match until {
|
|
||||||
|
if let Ok(author) = PublicKey::from_str(public_key) {
|
||||||
|
let until = match as_of {
|
||||||
Some(until) => Timestamp::from_str(until).unwrap(),
|
Some(until) => Timestamp::from_str(until).unwrap(),
|
||||||
None => Timestamp::now(),
|
None => Timestamp::now(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Ok(author) = PublicKey::from_str(id) {
|
|
||||||
let filter = Filter::new()
|
let filter = Filter::new()
|
||||||
.kinds(vec![Kind::TextNote, Kind::Repost])
|
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||||
.authors(vec![author])
|
.authors(vec![author])
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.until(f_until);
|
.until(until);
|
||||||
|
let _ = client
|
||||||
|
.reconcile(filter.clone(), NegentropyOptions::default())
|
||||||
|
.await;
|
||||||
|
|
||||||
if let Ok(events) = client
|
match client.database().query(vec![filter], Order::Asc).await {
|
||||||
.get_events_of(vec![filter], Some(Duration::from_secs(10)))
|
Ok(events) => Ok(events),
|
||||||
.await
|
Err(err) => Err(err.to_string()),
|
||||||
{
|
|
||||||
Ok(events)
|
|
||||||
} else {
|
|
||||||
Err("Get text event failed".into())
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Err("Parse author failed".into())
|
Err("Public Key is not valid, please check again.".into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,14 +91,12 @@ pub async fn get_events(
|
|||||||
.limit(limit)
|
.limit(limit)
|
||||||
.until(as_of);
|
.until(as_of);
|
||||||
|
|
||||||
if let Ok(events) = client
|
match client
|
||||||
.get_events_of(vec![filter], Some(Duration::from_secs(15)))
|
.get_events_of(vec![filter], Some(Duration::from_secs(15)))
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
println!("total global events: {}", events.len());
|
Ok(events) => Ok(events),
|
||||||
Ok(events)
|
Err(err) => Err(err.to_string()),
|
||||||
} else {
|
|
||||||
Err("Get events failed".into())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
false => {
|
false => {
|
||||||
@ -132,15 +129,13 @@ pub async fn get_events(
|
|||||||
.limit(limit)
|
.limit(limit)
|
||||||
.authors(val)
|
.authors(val)
|
||||||
.until(as_of);
|
.until(as_of);
|
||||||
|
let _ = client
|
||||||
|
.reconcile(filter.clone(), NegentropyOptions::default())
|
||||||
|
.await;
|
||||||
|
|
||||||
if let Ok(events) = client
|
match client.database().query(vec![filter], Order::Asc).await {
|
||||||
.get_events_of(vec![filter], Some(Duration::from_secs(15)))
|
Ok(events) => Ok(events),
|
||||||
.await
|
Err(err) => Err(err.to_string()),
|
||||||
{
|
|
||||||
println!("total local events: {}", events.len());
|
|
||||||
Ok(events)
|
|
||||||
} else {
|
|
||||||
Err("Get events failed".into())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -167,31 +162,36 @@ pub async fn get_events_from_interests(
|
|||||||
.limit(limit)
|
.limit(limit)
|
||||||
.until(as_of)
|
.until(as_of)
|
||||||
.hashtags(hashtags);
|
.hashtags(hashtags);
|
||||||
|
let _ = client
|
||||||
|
.reconcile(filter.clone(), NegentropyOptions::default())
|
||||||
|
.await;
|
||||||
|
|
||||||
if let Ok(events) = client
|
match client.database().query(vec![filter], Order::Asc).await {
|
||||||
.get_events_of(vec![filter], Some(Duration::from_secs(15)))
|
Ok(events) => Ok(events),
|
||||||
.await
|
Err(err) => Err(err.to_string()),
|
||||||
{
|
|
||||||
println!("total events: {}", events.len());
|
|
||||||
Ok(events)
|
|
||||||
} else {
|
|
||||||
Err("Get text event failed".into())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_event_thread(id: &str, state: State<'_, Nostr>) -> Result<Vec<Event>, ()> {
|
pub async fn get_event_thread(id: &str, state: State<'_, Nostr>) -> Result<Vec<Event>, String> {
|
||||||
let client = &state.client;
|
let client = &state.client;
|
||||||
let event_id = EventId::from_hex(id).unwrap();
|
|
||||||
let filter = Filter::new().kinds(vec![Kind::TextNote]).event(event_id);
|
|
||||||
|
|
||||||
if let Ok(events) = client
|
match EventId::from_hex(id) {
|
||||||
|
Ok(event_id) => {
|
||||||
|
let filter = Filter::new().kinds(vec![Kind::TextNote]).event(event_id);
|
||||||
|
let _ = client
|
||||||
|
.reconcile(filter.clone(), NegentropyOptions::default())
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match client
|
||||||
.get_events_of(vec![filter], Some(Duration::from_secs(10)))
|
.get_events_of(vec![filter], Some(Duration::from_secs(10)))
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(events)
|
Ok(events) => Ok(events),
|
||||||
} else {
|
Err(err) => Err(err.to_string()),
|
||||||
Err(())
|
}
|
||||||
|
}
|
||||||
|
Err(_) => Err("Event ID is not valid".into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -152,7 +152,8 @@ pub async fn load_selected_account(npub: &str, state: State<'_, Nostr>) -> Resul
|
|||||||
let client = &state.client;
|
let client = &state.client;
|
||||||
let keyring = Entry::new("Lume Secret Storage", npub).unwrap();
|
let keyring = Entry::new("Lume Secret Storage", npub).unwrap();
|
||||||
|
|
||||||
if let Ok(password) = keyring.get_password() {
|
match keyring.get_password() {
|
||||||
|
Ok(password) => {
|
||||||
if password.starts_with("bunker://") {
|
if password.starts_with("bunker://") {
|
||||||
let app_keys = Keys::generate();
|
let app_keys = Keys::generate();
|
||||||
let bunker_uri = NostrConnectURI::parse(password).unwrap();
|
let bunker_uri = NostrConnectURI::parse(password).unwrap();
|
||||||
@ -162,8 +163,6 @@ pub async fn load_selected_account(npub: &str, state: State<'_, Nostr>) -> Resul
|
|||||||
|
|
||||||
// Update signer
|
// Update signer
|
||||||
client.set_signer(Some(signer.into())).await;
|
client.set_signer(Some(signer.into())).await;
|
||||||
// Done
|
|
||||||
Ok(true)
|
|
||||||
} else {
|
} else {
|
||||||
let secret_key = SecretKey::from_bech32(password).expect("Get secret key failed");
|
let secret_key = SecretKey::from_bech32(password).expect("Get secret key failed");
|
||||||
let keys = Keys::new(secret_key);
|
let keys = Keys::new(secret_key);
|
||||||
@ -171,11 +170,11 @@ pub async fn load_selected_account(npub: &str, state: State<'_, Nostr>) -> Resul
|
|||||||
|
|
||||||
// Update signer
|
// Update signer
|
||||||
client.set_signer(Some(signer)).await;
|
client.set_signer(Some(signer)).await;
|
||||||
// Done
|
}
|
||||||
|
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
} else {
|
Err(err) => Err(err.to_string()),
|
||||||
Err("nsec not found".into())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user