feat: add notification for nip42

This commit is contained in:
2024-09-22 09:40:07 +07:00
parent 2c7f3685b6
commit a5574bef6c
10 changed files with 156 additions and 56 deletions

1
src-tauri/Cargo.lock generated
View File

@@ -47,6 +47,7 @@ dependencies = [
"tauri-plugin-window-state", "tauri-plugin-window-state",
"tauri-specta", "tauri-specta",
"tokio", "tokio",
"tracing-subscriber",
"url", "url",
] ]

View File

@@ -47,6 +47,7 @@ linkify = "0.10.0"
regex = "1.10.4" regex = "1.10.4"
keyring = { version = "3", features = ["apple-native", "windows-native"] } keyring = { version = "3", features = ["apple-native", "windows-native"] }
keyring-search = "1.2.0" keyring-search = "1.2.0"
tracing-subscriber = "0.3.18"
[target.'cfg(target_os = "macos")'.dependencies] [target.'cfg(target_os = "macos")'.dependencies]
cocoa = "0.25.0" cocoa = "0.25.0"

View File

@@ -77,12 +77,15 @@ struct Subscription {
#[derive(Serialize, Deserialize, Type, Clone, TauriEvent)] #[derive(Serialize, Deserialize, Type, Clone, TauriEvent)]
struct NewSettings(Settings); struct NewSettings(Settings);
pub const FETCH_LIMIT: usize = 44; pub const FETCH_LIMIT: usize = 20;
pub const NEWSFEED_NEG_LIMIT: usize = 256; pub const NEWSFEED_NEG_LIMIT: usize = 256;
pub const NOTIFICATION_NEG_LIMIT: usize = 64; pub const NOTIFICATION_NEG_LIMIT: usize = 64;
pub const NOTIFICATION_SUB_ID: &str = "lume_notification"; pub const NOTIFICATION_SUB_ID: &str = "lume_notification";
fn main() { fn main() {
#[cfg(debug_assertions)]
tracing_subscriber::fmt::init();
let builder = Builder::<tauri::Wry>::new() let builder = Builder::<tauri::Wry>::new()
// Then register them (separated by a comma) // Then register them (separated by a comma)
.commands(collect_commands![ .commands(collect_commands![
@@ -204,9 +207,10 @@ fn main() {
let opts = Options::new() let opts = Options::new()
.gossip(true) .gossip(true)
.max_avg_latency(Duration::from_millis(500)) .max_avg_latency(Duration::from_millis(500))
.automatic_authentication(true) .automatic_authentication(false)
.connection_timeout(Some(Duration::from_secs(5))) .connection_timeout(Some(Duration::from_secs(5)))
.timeout(Duration::from_secs(20)); .send_timeout(Some(Duration::from_secs(5)))
.timeout(Duration::from_secs(5));
// Setup nostr client // Setup nostr client
let client = ClientBuilder::default() let client = ClientBuilder::default()
@@ -321,11 +325,52 @@ fn main() {
}; };
let notification_id = SubscriptionId::new(NOTIFICATION_SUB_ID); let notification_id = SubscriptionId::new(NOTIFICATION_SUB_ID);
let mut notifications = client.pool().notifications();
client while let Ok(notification) = notifications.recv().await {
.handle_notifications(|notification| async { match notification {
if let RelayPoolNotification::Message { message, .. } = notification { RelayPoolNotification::Message { relay_url, message } => {
if let RelayMessage::Event { if let RelayMessage::Auth { challenge } = message {
match client.auth(challenge, relay_url.clone()).await {
Ok(..) => {
if let Ok(relay) = client.relay(&relay_url).await {
let msg =
format!("Authenticated to {} relay.", relay_url);
let opts = RelaySendOptions::new()
.skip_send_confirmation(true);
if let Err(e) = relay.resubscribe(opts).await {
println!("Error: {}", e);
}
if allow_notification {
if let Err(e) = &handle_clone
.notification()
.builder()
.body(&msg)
.title("Lume")
.show()
{
println!("Error: {}", e);
}
}
}
}
Err(e) => {
if allow_notification {
if let Err(e) = &handle_clone
.notification()
.builder()
.body(e.to_string())
.title("Lume")
.show()
{
println!("Error: {}", e);
}
}
}
}
} else if let RelayMessage::Event {
subscription_id, subscription_id,
event, event,
} = message } = message
@@ -337,12 +382,12 @@ fn main() {
let author = client let author = client
.fetch_metadata( .fetch_metadata(
event.pubkey, event.pubkey,
Some(Duration::from_secs(5)), Some(Duration::from_secs(3)),
) )
.await .await
.unwrap_or_else(|_| Metadata::new()); .unwrap_or_else(|_| Metadata::new());
send_notification(&event, author, &handle_clone); send_event_notification(&event, author, &handle_clone);
} }
} }
@@ -361,13 +406,12 @@ fn main() {
RichEvent { raw, parsed }, RichEvent { raw, parsed },
) )
.unwrap(); .unwrap();
} else { };
println!("new message: {}", message.as_json()) }
RelayPoolNotification::Shutdown => break,
_ => (),
} }
} }
Ok(false)
})
.await
}); });
Ok(()) Ok(())
@@ -403,7 +447,7 @@ fn prevent_default() -> tauri::plugin::TauriPlugin<tauri::Wry> {
tauri_plugin_prevent_default::Builder::new().build() tauri_plugin_prevent_default::Builder::new().build()
} }
fn send_notification(event: &Event, author: Metadata, handle: &tauri::AppHandle) { fn send_event_notification(event: &Event, author: Metadata, handle: &tauri::AppHandle) {
match event.kind { match event.kind {
Kind::TextNote => { Kind::TextNote => {
if let Err(e) = handle if let Err(e) = handle

View File

@@ -1,15 +1,47 @@
import { cn } from "@/commons"; import { cn } from "@/commons";
import { LumeWindow } from "@/system";
import { Menu, MenuItem } from "@tauri-apps/api/menu";
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
import { useCallback } from "react";
import { User } from "../user"; import { User } from "../user";
import { useNoteContext } from "./provider"; import { useNoteContext } from "./provider";
export function NoteUser({ className }: { className?: string }) { export function NoteUser({ className }: { className?: string }) {
const event = useNoteContext(); const event = useNoteContext();
const showContextMenu = useCallback(async (e: React.MouseEvent) => {
e.preventDefault();
const menuItems = await Promise.all([
MenuItem.new({
text: "View Profile",
action: () => LumeWindow.openProfile(event.pubkey),
}),
MenuItem.new({
text: "Copy Public Key",
action: async () => {
const pubkey = await event.pubkeyAsBech32();
await writeText(pubkey);
},
}),
]);
const menu = await Menu.new({
items: menuItems,
});
await menu.popup().catch((e) => console.error(e));
}, []);
return ( return (
<User.Provider pubkey={event.pubkey}> <User.Provider pubkey={event.pubkey}>
<User.Root className={cn("flex items-start justify-between", className)}> <User.Root className={cn("flex items-start justify-between", className)}>
<div className="flex w-full gap-2"> <div className="flex w-full gap-2">
<button type="button" className="shrink-0"> <button
type="button"
onClick={(e) => showContextMenu(e)}
className="shrink-0"
>
<User.Avatar className="rounded-full size-8" /> <User.Avatar className="rounded-full size-8" />
</button> </button>
<div className="flex items-center w-full gap-3"> <div className="flex items-center w-full gap-3">

View File

@@ -1,9 +1,11 @@
import { cn, replyTime } from "@/commons"; import { cn, replyTime } from "@/commons";
import { Note } from "@/components/note"; import { Note } from "@/components/note";
import type { LumeEvent } from "@/system"; import { type LumeEvent, LumeWindow } from "@/system";
import { CaretDown } from "@phosphor-icons/react"; import { CaretDown } from "@phosphor-icons/react";
import { Link, useSearch } from "@tanstack/react-router"; import { Link, useSearch } from "@tanstack/react-router";
import { memo } from "react"; import { Menu, MenuItem } from "@tauri-apps/api/menu";
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
import { memo, useCallback } from "react";
import { User } from "./user"; import { User } from "./user";
export const ReplyNote = memo(function ReplyNote({ export const ReplyNote = memo(function ReplyNote({
@@ -15,12 +17,38 @@ export const ReplyNote = memo(function ReplyNote({
}) { }) {
const search = useSearch({ strict: false }); const search = useSearch({ strict: false });
const showContextMenu = useCallback(async (e: React.MouseEvent) => {
e.preventDefault();
const menuItems = await Promise.all([
MenuItem.new({
text: "View Profile",
action: () => LumeWindow.openProfile(event.pubkey),
}),
MenuItem.new({
text: "Copy Public Key",
action: async () => {
const pubkey = await event.pubkeyAsBech32();
await writeText(pubkey);
},
}),
]);
const menu = await Menu.new({
items: menuItems,
});
await menu.popup().catch((e) => console.error(e));
}, []);
return ( return (
<Note.Provider event={event}> <Note.Provider event={event}>
<User.Provider pubkey={event.pubkey}> <User.Provider pubkey={event.pubkey}>
<Note.Root className={cn("flex gap-2.5", className)}> <Note.Root className={cn("flex gap-2.5", className)}>
<User.Root className="shrink-0"> <User.Root className="shrink-0">
<button type="button" onClick={(e) => showContextMenu(e)}>
<User.Avatar className="size-8 rounded-full" /> <User.Avatar className="size-8 rounded-full" />
</button>
</User.Root> </User.Root>
<div className="flex-1 flex flex-col gap-1"> <div className="flex-1 flex flex-col gap-1">
<div> <div>
@@ -74,13 +102,39 @@ export const ReplyNote = memo(function ReplyNote({
function ChildReply({ event }: { event: LumeEvent }) { function ChildReply({ event }: { event: LumeEvent }) {
const search = useSearch({ strict: false }); const search = useSearch({ strict: false });
const showContextMenu = useCallback(async (e: React.MouseEvent) => {
e.preventDefault();
const menuItems = await Promise.all([
MenuItem.new({
text: "View Profile",
action: () => LumeWindow.openProfile(event.pubkey),
}),
MenuItem.new({
text: "Copy Public Key",
action: async () => {
const pubkey = await event.pubkeyAsBech32();
await writeText(pubkey);
},
}),
]);
const menu = await Menu.new({
items: menuItems,
});
await menu.popup().catch((e) => console.error(e));
}, []);
return ( return (
<Note.Provider event={event}> <Note.Provider event={event}>
<User.Provider pubkey={event.pubkey}> <User.Provider pubkey={event.pubkey}>
<div className="group flex flex-col gap-1"> <div className="group flex flex-col gap-1">
<div> <div>
<User.Root className="inline"> <User.Root className="inline">
<button type="button" onClick={(e) => showContextMenu(e)}>
<User.Name className="font-medium text-blue-500" suffix=":" /> <User.Name className="font-medium text-blue-500" suffix=":" />
</button>
</User.Root> </User.Root>
<div className="pl-2 inline select-text text-balance content-break overflow-hidden"> <div className="pl-2 inline select-text text-balance content-break overflow-hidden">
{event.content} {event.content}

View File

@@ -1,11 +1,8 @@
import { appSettings, cn } from "@/commons"; import { appSettings, cn } from "@/commons";
import { LumeWindow } from "@/system";
import * as Avatar from "@radix-ui/react-avatar"; import * as Avatar from "@radix-ui/react-avatar";
import { useStore } from "@tanstack/react-store"; import { useStore } from "@tanstack/react-store";
import { Menu, MenuItem } from "@tauri-apps/api/menu";
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
import { minidenticon } from "minidenticons"; import { minidenticon } from "minidenticons";
import { useCallback, useMemo } from "react"; import { useMemo } from "react";
import { useUserContext } from "./provider"; import { useUserContext } from "./provider";
export function UserAvatar({ className }: { className?: string }) { export function UserAvatar({ className }: { className?: string }) {
@@ -33,32 +30,8 @@ export function UserAvatar({ className }: { className?: string }) {
[user.pubkey], [user.pubkey],
); );
const showContextMenu = useCallback(async (e: React.MouseEvent) => {
e.preventDefault();
const menuItems = await Promise.all([
MenuItem.new({
text: "View Profile",
action: () => LumeWindow.openProfile(user.pubkey),
}),
MenuItem.new({
text: "Copy Public Key",
action: async () => {
await writeText(user.pubkey);
},
}),
]);
const menu = await Menu.new({
items: menuItems,
});
await menu.popup().catch((e) => console.error(e));
}, []);
return ( return (
<Avatar.Root <Avatar.Root
onClick={(e) => showContextMenu(e)}
className={cn( className={cn(
"shrink-0 block overflow-hidden bg-neutral-200 dark:bg-neutral-800", "shrink-0 block overflow-hidden bg-neutral-200 dark:bg-neutral-800",
className, className,

View File

@@ -182,7 +182,7 @@ function ReplyList() {
useEffect(() => { useEffect(() => {
events.subscription events.subscription
.emit({ label, kind: "Subscribe", event_id: id, local_only: undefined }) .emit({ label, kind: "Subscribe", event_id: id })
.then(() => console.log("Subscribe: ", label)); .then(() => console.log("Subscribe: ", label));
return () => { return () => {
@@ -191,7 +191,6 @@ function ReplyList() {
label, label,
kind: "Unsubscribe", kind: "Unsubscribe",
event_id: id, event_id: id,
local_only: undefined,
}) })
.then(() => console.log("Unsubscribe: ", label)); .then(() => console.log("Unsubscribe: ", label));
}; };

View File

@@ -147,6 +147,7 @@ function Screen() {
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter") loginWith(); if (e.key === "Enter") loginWith();
}} }}
disabled={isPending}
placeholder="Password" 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" 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"
/> />

View File

@@ -27,8 +27,6 @@ export function useEvent(id: string) {
} }
} }
console.log(relayHint);
// Build query // Build query
if (relayHint?.length) { if (relayHint?.length) {
query = await commands.getEventFrom(normalizeId, relayHint); query = await commands.getEventFrom(normalizeId, relayHint);

View File

@@ -17,18 +17,15 @@ export function useProfile(pubkey: string, embed?: string) {
} }
let normalizeId = pubkey.replace("nostr:", "").replace(/[^\w\s]/gi, ""); let normalizeId = pubkey.replace("nostr:", "").replace(/[^\w\s]/gi, "");
let relayHint: string;
if (normalizeId.startsWith("nprofile")) { if (normalizeId.startsWith("nprofile")) {
const decoded = nip19.decode(normalizeId); const decoded = nip19.decode(normalizeId);
if (decoded.type === "nprofile") { if (decoded.type === "nprofile") {
relayHint = decoded.data.relays[0];
normalizeId = decoded.data.pubkey; normalizeId = decoded.data.pubkey;
} }
} }
console.log(relayHint);
const query = await commands.getProfile(normalizeId); const query = await commands.getProfile(normalizeId);
if (query.status === "ok") { if (query.status === "ok") {