feat: allow user enter custom relay for relayfeeds

This commit is contained in:
reya 2024-11-05 15:04:39 +07:00
parent 0b97248fb8
commit d80534c51f
6 changed files with 231 additions and 25 deletions

View File

@ -381,6 +381,35 @@ pub async fn get_all_local_interests(
Ok(alt_events)
}
#[tauri::command]
#[specta::specta]
pub async fn get_relay_list(id: String, state: State<'_, Nostr>) -> Result<String, String> {
let client = &state.client;
let public_key = PublicKey::parse(&id).map_err(|e| e.to_string())?;
let filter = Filter::new()
.author(public_key)
.kind(Kind::RelayList)
.limit(1);
let mut events = Events::new(&[filter.clone()]);
let mut rx = client
.stream_events(vec![filter], Some(Duration::from_secs(3)))
.await
.map_err(|e| e.to_string())?;
while let Some(event) = rx.next().await {
events.insert(event);
}
if let Some(event) = events.first() {
Ok(event.as_json())
} else {
Err("Relay list not found".into())
}
}
#[tauri::command]
#[specta::specta]
pub async fn get_all_profiles(state: State<'_, Nostr>) -> Result<Vec<Mention>, String> {

View File

@ -67,11 +67,11 @@ impl Default for Settings {
pub const DEFAULT_DIFFICULTY: u8 = 0;
pub const FETCH_LIMIT: usize = 50;
pub const QUEUE_DELAY: u64 = 300;
pub const QUEUE_DELAY: u64 = 150;
pub const NOTIFICATION_SUB_ID: &str = "lume_notification";
fn main() {
// tracing_subscriber::fmt::init();
tracing_subscriber::fmt::init();
let builder = Builder::<tauri::Wry>::new().commands(collect_commands![
get_relays,
@ -105,6 +105,7 @@ fn main() {
get_interest,
get_all_interests,
get_all_local_interests,
get_relay_list,
set_wallet,
load_wallet,
remove_wallet,

View File

@ -248,6 +248,14 @@ async getAllLocalInterests(until: string | null) : Promise<Result<RichEvent[], s
else return { status: "error", error: e as any };
}
},
async getRelayList(id: string) : Promise<Result<string, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("get_relay_list", { id }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async setWallet(uri: string) : Promise<Result<boolean, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("set_wallet", { uri }) };

View File

@ -21,6 +21,15 @@ export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export function isValidRelayUrl(string: string) {
try {
const newUrl = new URL(string);
return newUrl.protocol === "ws:" || newUrl.protocol === "wss:";
} catch (err) {
return false;
}
}
export const isImagePath = (path: string) => {
const exts = ["jpg", "jpeg", "gif", "png", "webp", "avif", "tiff"];

View File

@ -16,7 +16,7 @@ export function NoteQuote({
<Tooltip.Trigger asChild>
<button
type="button"
onClick={() => LumeWindow.openEditor(null, event.id)}
onClick={() => LumeWindow.openEditor(undefined, event.id)}
className={cn(
"inline-flex items-center justify-center text-neutral-800 dark:text-neutral-200",
label

View File

@ -1,5 +1,5 @@
import { commands } from "@/commands.gen";
import { cn, toLumeEvents } from "@/commons";
import { cn, isValidRelayUrl, toLumeEvents } from "@/commons";
import { Spinner, User } from "@/components";
import { LumeWindow } from "@/system";
import type { LumeColumn, NostrEvent } from "@/types";
@ -8,9 +8,10 @@ import * as ScrollArea from "@radix-ui/react-scroll-area";
import { useQuery } from "@tanstack/react-query";
import { createLazyFileRoute } from "@tanstack/react-router";
import { resolveResource } from "@tauri-apps/api/path";
import { message } from "@tauri-apps/plugin-dialog";
import { readTextFile } from "@tauri-apps/plugin-fs";
import { nanoid } from "nanoid";
import { useCallback } from "react";
import { useCallback, useState, useTransition } from "react";
export const Route = createLazyFileRoute("/columns/_layout/launchpad/$id")({
component: Screen,
@ -25,6 +26,7 @@ function Screen() {
>
<ScrollArea.Viewport className="relative h-full px-3 pb-3">
<Newsfeeds />
<Relayfeeds />
<Interests />
<Core />
</ScrollArea.Viewport>
@ -178,14 +180,14 @@ function Newsfeeds() {
type="button"
onClick={() =>
LumeWindow.openColumn({
name: "Newsfeeds",
name: "Browse Newsfeeds",
url: "/columns/discover-newsfeeds",
label: "discover_newsfeeds",
})
}
className="h-9 w-full px-3 flex items-center justify-between bg-neutral-200/50 hover:bg-neutral-200 rounded-lg dark:bg-neutral-800/50 dark:hover:bg-neutral-800"
>
<span className="text-xs font-medium">Discover newsfeeds</span>
<span className="text-xs font-medium">Browse newsfeeds</span>
<ArrowRight className="size-4" weight="bold" />
</button>
</div>
@ -193,6 +195,179 @@ function Newsfeeds() {
);
}
function Relayfeeds() {
const { id } = Route.useParams();
const { isLoading, isError, error, data, refetch, isRefetching } = useQuery({
queryKey: ["relays", id],
queryFn: async () => {
const res = await commands.getRelayList(id);
if (res.status === "ok") {
const event: NostrEvent = JSON.parse(res.data);
return event;
} else {
throw new Error(res.error);
}
},
refetchOnWindowFocus: false,
});
return (
<div className="mb-12 flex flex-col gap-3">
<div className="flex items-center justify-between px-2">
<h3 className="font-semibold">Relayfeeds</h3>
<div className="inline-flex items-center justify-center gap-2">
<button
type="button"
onClick={() => refetch()}
className={cn(
"size-7 inline-flex items-center justify-center rounded-full",
isRefetching ? "animate-spin" : "",
)}
>
<ArrowClockwise className="size-4" />
</button>
<button
type="button"
onClick={() => LumeWindow.openPopup(`${id}/set-group`, "New group")}
className="h-7 w-max px-2 inline-flex items-center justify-center gap-1 text-sm font-medium rounded-full bg-neutral-300 dark:bg-neutral-700 hover:bg-blue-500 hover:text-white"
>
<Plus className="size-3" weight="bold" />
New
</button>
</div>
</div>
<div className="flex flex-col gap-3">
{isLoading ? (
<div className="inline-flex items-center gap-1.5">
<Spinner className="size-4" />
Loading...
</div>
) : isError ? (
<div className="flex flex-col items-center justify-center h-16 w-full rounded-xl overflow-hidden bg-neutral-200/50 dark:bg-neutral-800/50">
<p className="text-center">{error?.message ?? "Error"}</p>
</div>
) : !data ? (
<div className="flex flex-col items-center justify-center h-16 w-full rounded-xl overflow-hidden bg-neutral-200/50 dark:bg-neutral-800/50">
<p className="text-center">You don't have any relay list yet.</p>
</div>
) : (
<div className="flex flex-col rounded-xl overflow-hidden bg-white dark:bg-neutral-800/50 shadow-lg shadow-primary dark:ring-1 dark:ring-neutral-800">
<div className="flex flex-col gap-2 p-2">
{data?.tags.map((tag) =>
tag[1]?.startsWith("wss://") ? (
<div
key={tag[1]}
className="group px-3 flex items-center justify-between h-11 rounded-lg bg-neutral-100 dark:bg-neutral-800"
>
<div className="flex-1 truncate select-text text-sm font-medium">
{tag[1]}
</div>
<button
type="button"
onClick={() =>
LumeWindow.openColumn({
name: tag[1],
label: `relays_${tag[1].replace(/[^\w\s]/gi, "")}`,
url: `/columns/relays/${encodeURIComponent(tag[1])}`,
})
}
className="h-6 w-16 hidden group-hover:inline-flex items-center justify-center gap-1 text-xs font-semibold rounded-full bg-neutral-200 dark:bg-neutral-700 hover:bg-blue-500 hover:text-white"
>
Add
</button>
</div>
) : null,
)}
</div>
<div className="p-2 flex items-center">
<User.Provider pubkey={data?.pubkey}>
<User.Root className="inline-flex items-center gap-2">
<User.Avatar className="size-7 rounded-full" />
<User.Name className="text-xs font-medium" />
</User.Root>
</User.Provider>
</div>
</div>
)}
<div className="flex flex-col rounded-xl overflow-hidden bg-white dark:bg-neutral-800/50 shadow-lg shadow-primary dark:ring-1 dark:ring-neutral-800">
<RelayForm />
</div>
<button
type="button"
onClick={() =>
LumeWindow.openColumn({
name: "Browse Relays",
url: "/columns/discover-relays",
label: "discover_relays",
})
}
className="h-9 w-full px-3 flex items-center justify-between bg-neutral-200/50 hover:bg-neutral-200 rounded-lg dark:bg-neutral-800/50 dark:hover:bg-neutral-800"
>
<span className="text-xs font-medium">Browse relays</span>
<ArrowRight className="size-4" weight="bold" />
</button>
</div>
</div>
);
}
function RelayForm() {
const [url, setUrl] = useState("");
const [isPending, startTransition] = useTransition();
const submit = () => {
startTransition(async () => {
if (!isValidRelayUrl(url)) {
await message("Relay URL is not valid", { kind: "error" });
return;
}
await LumeWindow.openColumn({
name: url,
label: `relays_${url.replace(/[^\w\s]/gi, "")}`,
url: `/columns/relays/${encodeURIComponent(url)}`,
});
setUrl("");
});
};
return (
<div className="flex flex-col gap-2 p-2">
<label
htmlFor="url"
className="text-xs font-semibold text-neutral-700 dark:text-neutral-300"
>
Add custom relay
</label>
<div className="flex gap-2">
<input
name="url"
type="url"
onChange={(e) => setUrl(e.currentTarget.value)}
onKeyDown={(event) => {
if (event.key === "Enter") submit();
}}
value={url}
disabled={isPending}
placeholder="wss://..."
spellCheck={false}
className="flex-1 px-3 bg-neutral-100 border-transparent rounded-lg h-9 dark:bg-neutral-900 placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:placeholder:text-neutral-400"
/>
<button
type="button"
disabled={isPending}
onClick={() => submit()}
className="shrink-0 h-9 w-16 inline-flex items-center justify-center gap-1 text-xs font-semibold rounded-lg bg-neutral-200 dark:bg-neutral-700 hover:bg-blue-500 hover:text-white"
>
Add
</button>
</div>
</div>
);
}
function Interests() {
const { id } = Route.useParams();
const { isLoading, isError, error, data, refetch, isRefetching } = useQuery({
@ -332,14 +507,14 @@ function Interests() {
type="button"
onClick={() =>
LumeWindow.openColumn({
name: "Interests",
name: "Browse Interests",
url: "/columns/discover-interests",
label: "discover_interests",
})
}
className="h-9 w-full px-3 flex items-center justify-between bg-neutral-200/50 hover:bg-neutral-200 rounded-lg dark:bg-neutral-800/50 dark:hover:bg-neutral-800"
>
<span className="text-xs font-medium">Discover interests</span>
<span className="text-xs font-medium">Browse interests</span>
<ArrowRight className="size-4" weight="bold" />
</button>
</div>
@ -391,22 +566,6 @@ function Core() {
Add
</button>
</div>
<div className="px-3 flex items-center justify-between h-11 rounded-lg bg-neutral-100 dark:bg-neutral-800">
<div className="text-sm font-medium">Relays</div>
<button
type="button"
onClick={() =>
LumeWindow.openColumn({
name: "Relays",
label: "relays",
url: "/columns/discover-relays",
})
}
className="h-6 w-16 inline-flex items-center justify-center gap-1 text-xs font-semibold rounded-full bg-neutral-200 dark:bg-neutral-700 hover:bg-blue-500 hover:text-white"
>
Add
</button>
</div>
{data?.map((column) => (
<div
key={column.label}