feat: improve tauri commands

This commit is contained in:
reya 2024-05-20 07:05:30 +07:00
parent cac774a0c1
commit 9b5867f80c
9 changed files with 1682 additions and 1647 deletions

View File

@ -1,3 +1,5 @@
import { Conversation } from "@/components/conversation";
import { Quote } from "@/components/quote";
import { RepostNote } from "@/components/repost";
import { TextNote } from "@/components/text";
import { ArrowRightCircleIcon, ArrowRightIcon } from "@lume/icons";
@ -39,7 +41,7 @@ export const Route = createFileRoute("/foryou")({
});
export function Screen() {
const { name, account } = Route.useSearch();
const { label, account } = Route.useSearch();
const { ark, interests } = Route.useRouteContext();
const {
data,
@ -49,20 +51,17 @@ export function Screen() {
hasNextPage,
fetchNextPage,
} = useInfiniteQuery({
queryKey: [name, account],
queryKey: [label, account],
initialPageParam: 0,
queryFn: async ({ pageParam }: { pageParam: number }) => {
const events = await ark.get_events_from_interests(
const events = await ark.get_hashtag_events(
interests.hashtags,
20,
pageParam,
);
return events;
},
getNextPageParam: (lastPage) => {
const lastEvent = lastPage?.at(-1);
return lastEvent ? lastEvent.created_at - 1 : null;
},
getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1,
select: (data) => data?.pages.flatMap((page) => page),
refetchOnWindowFocus: false,
});
@ -72,8 +71,22 @@ export function Screen() {
switch (event.kind) {
case Kind.Repost:
return <RepostNote key={event.id} event={event} />;
default:
return <TextNote key={event.id} event={event} />;
default: {
const isConversation =
event.tags.filter((tag) => tag[0] === "e" && tag[3] !== "mention")
.length > 0;
const isQuote = event.tags.filter((tag) => tag[0] === "q").length > 0;
if (isConversation) {
return <Conversation key={event.id} event={event} className="mb-3" />;
}
if (isQuote) {
return <Quote key={event.id} event={event} className="mb-3" />;
}
return <TextNote key={event.id} event={event} className="mb-3" />;
}
}
};

View File

@ -27,7 +27,7 @@ export const Route = createFileRoute("/global")({
});
export function Screen() {
const { account } = Route.useSearch();
const { label, account } = Route.useSearch();
const { ark } = Route.useRouteContext();
const {
data,
@ -37,16 +37,13 @@ export function Screen() {
hasNextPage,
fetchNextPage,
} = useInfiniteQuery({
queryKey: ["global", account],
queryKey: [label, account],
initialPageParam: 0,
queryFn: async ({ pageParam }: { pageParam: number }) => {
const events = await ark.get_events(20, pageParam, undefined, true);
const events = await ark.get_global_events(20, pageParam);
return events;
},
getNextPageParam: (lastPage) => {
const lastEvent = lastPage?.at(-1);
return lastEvent ? lastEvent.created_at - 1 : null;
},
getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1,
select: (data) => data?.pages.flatMap((page) => page),
refetchOnWindowFocus: false,
});

View File

@ -1,3 +1,5 @@
import { Conversation } from "@/components/conversation";
import { Quote } from "@/components/quote";
import { RepostNote } from "@/components/repost";
import { TextNote } from "@/components/text";
import { ArrowRightCircleIcon, ArrowRightIcon } from "@lume/icons";
@ -41,7 +43,7 @@ export const Route = createFileRoute("/group")({
});
export function Screen() {
const { name, account } = Route.useSearch();
const { label, account } = Route.useSearch();
const { ark, groups } = Route.useRouteContext();
const {
data,
@ -51,16 +53,13 @@ export function Screen() {
hasNextPage,
fetchNextPage,
} = useInfiniteQuery({
queryKey: [name, account],
queryKey: [label, account],
initialPageParam: 0,
queryFn: async ({ pageParam }: { pageParam: number }) => {
const events = await ark.get_events(20, pageParam, groups);
const events = await ark.get_group_events(groups, 20, pageParam);
return events;
},
getNextPageParam: (lastPage) => {
const lastEvent = lastPage?.at(-1);
return lastEvent ? lastEvent.created_at - 1 : null;
},
getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1,
select: (data) =>
data?.pages.flatMap((page) => page.filter((ev) => ev.kind === Kind.Text)),
refetchOnWindowFocus: false,
@ -71,8 +70,22 @@ export function Screen() {
switch (event.kind) {
case Kind.Repost:
return <RepostNote key={event.id} event={event} />;
default:
return <TextNote key={event.id} event={event} />;
default: {
const isConversation =
event.tags.filter((tag) => tag[0] === "e" && tag[3] !== "mention")
.length > 0;
const isQuote = event.tags.filter((tag) => tag[0] === "q").length > 0;
if (isConversation) {
return <Conversation key={event.id} event={event} className="mb-3" />;
}
if (isQuote) {
return <Quote key={event.id} event={event} className="mb-3" />;
}
return <TextNote key={event.id} event={event} className="mb-3" />;
}
}
};

View File

@ -41,13 +41,10 @@ export function Screen() {
queryKey: [label, account],
initialPageParam: 0,
queryFn: async ({ pageParam }: { pageParam: number }) => {
const events = await ark.get_events(20, pageParam);
const events = await ark.get_local_events(20, pageParam);
return events;
},
getNextPageParam: (lastPage) => {
const lastEvent = lastPage?.at(-1);
return lastEvent ? lastEvent.created_at - 1 : null;
},
getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1,
select: (data) => data?.pages.flatMap((page) => page),
refetchOnWindowFocus: false,
});
@ -79,7 +76,7 @@ export function Screen() {
return (
<div className="p-2 w-full h-full overflow-y-auto scrollbar-none">
{isFetching && !isLoading && !isFetchingNextPage ? (
<div className="w-full h-11 flex items-center justify-center">
<div className="w-full h-11 flex items-center justify-center bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50">
<div className="flex items-center justify-center gap-2">
<Spinner className="size-5" />
<span className="text-sm font-medium">Fetching new notes...</span>

View File

@ -19,7 +19,7 @@ export const Route = createFileRoute("/users/$pubkey")({
},
loader: async ({ params, context }) => {
const ark = context.ark;
return { data: defer(ark.get_events_from(params.pubkey, 50)) };
return { data: defer(ark.get_events_by(params.pubkey, 50)) };
},
component: Screen,
});

View File

@ -14,7 +14,7 @@ export function EventList({ id }: { id: string }) {
queryKey: ["events", id],
initialPageParam: 0,
queryFn: async ({ pageParam }: { pageParam: number }) => {
const events = await ark.get_events_from(id, FETCH_LIMIT, pageParam);
const events = await ark.get_events_by(id, FETCH_LIMIT, pageParam);
return events;
},
getNextPageParam: (lastPage) => {

View File

@ -164,24 +164,6 @@ export class Ark {
}
}
public async get_events_from(pubkey: string, limit: number, asOf?: number) {
try {
let until: string = undefined;
if (asOf && asOf > 0) until = asOf.toString();
const nostrEvents: Event[] = await invoke("get_events_from", {
publicKey: pubkey,
limit,
as_of: until,
});
return nostrEvents.sort((a, b) => b.created_at - a.created_at);
} catch (e) {
console.error(String(e));
return [];
}
}
public async search(content: string, limit: number) {
try {
if (content.length < 1) return [];
@ -198,24 +180,8 @@ export class Ark {
}
}
public async get_events(
limit: number,
asOf?: number,
contacts?: string[],
global?: boolean,
) {
try {
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
const isGlobal = global ?? false;
private dedup_events(nostrEvents: Event[]) {
const seens = new Set<string>();
const nostrEvents: Event[] = await invoke("get_events", {
limit,
until,
contacts,
global: isGlobal,
});
const events = nostrEvents.filter((event) => {
const eTags = event.tags.filter((el) => el[0] === "e");
const ids = eTags.map((item) => item[1]);
@ -238,50 +204,98 @@ export class Ark {
return !isDup;
});
return events;
}
public async get_local_events(limit: number, asOf?: number) {
try {
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
const nostrEvents: Event[] = await invoke("get_local_events", {
limit,
until,
});
const events = this.dedup_events(nostrEvents);
return events;
} catch (e) {
console.error("[get_events] failed", String(e));
console.error("[get_local_events] failed", String(e));
return [];
}
}
public async get_events_from_interests(
public async get_global_events(limit: number, asOf?: number) {
try {
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
const nostrEvents: Event[] = await invoke("get_global_events", {
limit,
until,
});
const events = this.dedup_events(nostrEvents);
return events;
} catch (e) {
console.error("[get_global_events] failed", String(e));
return [];
}
}
public async get_hashtag_events(
hashtags: string[],
limit: number,
asOf?: number,
) {
let until: string = undefined;
if (asOf && asOf > 0) until = asOf.toString();
const seenIds = new Set<string>();
const dedupQueue = new Set<string>();
const nostrTags = hashtags.map((tag) => tag.replace("#", "").toLowerCase());
const nostrEvents: Event[] = await invoke("get_events_from_interests", {
try {
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
const nostrTags = hashtags.map((tag) => tag.replace("#", ""));
const nostrEvents: Event[] = await invoke("get_hashtag_events", {
hashtags: nostrTags,
limit,
until,
});
const events = this.dedup_events(nostrEvents);
for (const event of nostrEvents) {
const tags = event.tags
.filter((el) => el[0] === "e")
?.map((item) => item[1]);
if (tags.length) {
for (const tag of tags) {
if (seenIds.has(tag)) {
dedupQueue.add(event.id);
break;
}
seenIds.add(tag);
}
return events;
} catch (e) {
console.error("[get_hashtag_events] failed", String(e));
return [];
}
}
return nostrEvents
.filter((event) => !dedupQueue.has(event.id))
.sort((a, b) => b.created_at - a.created_at);
public async get_group_events(
contacts: string[],
limit: number,
asOf?: number,
) {
try {
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
const nostrEvents: Event[] = await invoke("get_group_events", {
list: contacts,
limit,
until,
});
const events = this.dedup_events(nostrEvents);
return events;
} catch (e) {
console.error("[get_group_events] failed", String(e));
return [];
}
}
public async get_events_by(pubkey: string, limit: number, asOf?: number) {
try {
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
const nostrEvents: Event[] = await invoke("get_events_by", {
publicKey: pubkey,
limit,
as_of: until,
});
return nostrEvents.sort((a, b) => b.created_at - a.created_at);
} catch (e) {
console.error("[get_events_by] failed", String(e));
return [];
}
}
public async publish(
@ -356,27 +370,9 @@ export class Ark {
}
}
public async upvote(id: string, author: string) {
try {
const cmd: string = await invoke("upvote", { id, pubkey: author });
return cmd;
} catch (e) {
throw new Error(String(e));
}
}
public async downvote(id: string, author: string) {
try {
const cmd: string = await invoke("downvote", { id, pubkey: author });
return cmd;
} catch (e) {
throw new Error(String(e));
}
}
public async get_event_thread(id: string) {
try {
const events: EventWithReplies[] = await invoke("get_event_thread", {
const events: EventWithReplies[] = await invoke("get_thread", {
id,
});

View File

@ -127,10 +127,12 @@ fn main() {
nostr::metadata::zap_event,
nostr::metadata::friend_to_friend,
nostr::event::get_event,
nostr::event::get_events_from,
nostr::event::get_events,
nostr::event::get_events_from_interests,
nostr::event::get_event_thread,
nostr::event::get_thread,
nostr::event::get_events_by,
nostr::event::get_local_events,
nostr::event::get_global_events,
nostr::event::get_hashtag_events,
nostr::event::get_group_events,
nostr::event::publish,
nostr::event::repost,
commands::folder::show_in_folder,

View File

@ -30,7 +30,7 @@ pub async fn get_event(id: &str, state: State<'_, Nostr>) -> Result<String, Stri
Some(id) => {
let filter = Filter::new().id(id);
match &client
match client
.get_events_of(vec![filter], Some(Duration::from_secs(10)))
.await
{
@ -49,7 +49,24 @@ pub async fn get_event(id: &str, state: State<'_, Nostr>) -> Result<String, Stri
}
#[tauri::command]
pub async fn get_events_from(
pub async fn get_thread(id: &str, state: State<'_, Nostr>) -> Result<Vec<Event>, String> {
let client = &state.client;
match EventId::from_hex(id) {
Ok(event_id) => {
let filter = Filter::new().kinds(vec![Kind::TextNote]).event(event_id);
match client.get_events_of(vec![filter], None).await {
Ok(events) => Ok(events),
Err(err) => Err(err.to_string()),
}
}
Err(_) => Err("Event ID is not valid".into()),
}
}
#[tauri::command]
pub async fn get_events_by(
public_key: &str,
limit: usize,
as_of: Option<&str>,
@ -57,7 +74,8 @@ pub async fn get_events_from(
) -> Result<Vec<Event>, String> {
let client = &state.client;
if let Ok(author) = PublicKey::from_str(public_key) {
match PublicKey::from_str(public_key) {
Ok(author) => {
let until = match as_of {
Some(until) => Timestamp::from_str(until).unwrap(),
None => Timestamp::now(),
@ -72,17 +90,15 @@ pub async fn get_events_from(
Ok(events) => Ok(events),
Err(err) => Err(err.to_string()),
}
} else {
Err("Public Key is not valid, please check again.".into())
}
Err(err) => Err(err.to_string()),
}
}
#[tauri::command]
pub async fn get_events(
pub async fn get_local_events(
limit: usize,
until: Option<&str>,
contacts: Option<Vec<&str>>,
global: bool,
state: State<'_, Nostr>,
) -> Result<Vec<Event>, String> {
let client = &state.client;
@ -91,66 +107,57 @@ pub async fn get_events(
None => Timestamp::now(),
};
match global {
true => {
let filter = Filter::new()
.kinds(vec![Kind::TextNote, Kind::Repost])
.limit(limit)
.until(as_of);
match client
.get_events_of(vec![filter], Some(Duration::from_secs(15)))
.await
{
Ok(events) => Ok(events),
Err(err) => Err(err.to_string()),
}
}
false => {
let authors = match contacts {
Some(val) => {
let c: Vec<PublicKey> = val
.into_iter()
.map(|key| PublicKey::from_str(key).unwrap())
.collect();
Some(c)
}
None => {
match client
.get_contact_list_public_keys(Some(Duration::from_secs(10)))
.await
{
Ok(val) => Some(val),
Err(_) => None,
}
}
};
match authors {
Some(val) => {
if val.is_empty() {
Err("Get local events but contact list is empty".into())
} else {
Ok(contacts) => {
let filter = Filter::new()
.kinds(vec![Kind::TextNote, Kind::Repost])
.limit(limit)
.authors(val.clone())
.authors(contacts)
.until(as_of);
match client.get_events_of(vec![filter], None).await {
match client
.get_events_of(vec![filter], Some(Duration::from_secs(8)))
.await
{
Ok(events) => Ok(events),
Err(err) => Err(err.to_string()),
}
}
}
None => Err("Get local events but contact list is empty".into()),
}
}
Err(err) => Err(err.to_string()),
}
}
#[tauri::command]
pub async fn get_events_from_interests(
pub async fn get_global_events(
limit: usize,
until: Option<&str>,
state: State<'_, Nostr>,
) -> Result<Vec<Event>, String> {
let client = &state.client;
let as_of = match until {
Some(until) => Timestamp::from_str(until).unwrap(),
None => Timestamp::now(),
};
let filter = Filter::new()
.kinds(vec![Kind::TextNote, Kind::Repost])
.limit(limit)
.until(as_of);
match client
.get_events_of(vec![filter], Some(Duration::from_secs(8)))
.await
{
Ok(events) => Ok(events),
Err(err) => Err(err.to_string()),
}
}
#[tauri::command]
pub async fn get_hashtag_events(
hashtags: Vec<&str>,
limit: usize,
until: Option<&str>,
@ -174,21 +181,32 @@ pub async fn get_events_from_interests(
}
#[tauri::command]
pub async fn get_event_thread(id: &str, state: State<'_, Nostr>) -> Result<Vec<Event>, String> {
pub async fn get_group_events(
list: Vec<&str>,
limit: usize,
until: Option<&str>,
state: State<'_, Nostr>,
) -> Result<Vec<Event>, String> {
let client = &state.client;
match EventId::from_hex(id) {
Ok(event_id) => {
let filter = Filter::new().kinds(vec![Kind::TextNote]).event(event_id);
let as_of = match until {
Some(until) => Timestamp::from_str(until).unwrap(),
None => Timestamp::now(),
};
let authors: Vec<PublicKey> = list
.into_iter()
.map(|hex| PublicKey::from_hex(hex).unwrap())
.collect();
let filter = Filter::new()
.kinds(vec![Kind::TextNote, Kind::Repost])
.limit(limit)
.until(as_of)
.authors(authors);
match client.get_events_of(vec![filter], None).await {
Ok(events) => Ok(events),
Err(err) => Err(err.to_string()),
}
}
Err(_) => Err("Event ID is not valid".into()),
}
}
#[tauri::command]
pub async fn publish(
@ -210,9 +228,8 @@ pub async fn repost(raw: &str, state: State<'_, Nostr>) -> Result<EventId, Strin
let client = &state.client;
let event = Event::from_json(raw).unwrap();
if let Ok(event_id) = client.repost(&event, None).await {
Ok(event_id)
} else {
Err("Repost failed".into())
match client.repost(&event, None).await {
Ok(event_id) => Ok(event_id),
Err(err) => Err(err.to_string()),
}
}