feat: add notification for nip42
This commit is contained in:
1
src-tauri/Cargo.lock
generated
1
src-tauri/Cargo.lock
generated
@@ -47,6 +47,7 @@ dependencies = [
|
|||||||
"tauri-plugin-window-state",
|
"tauri-plugin-window-state",
|
||||||
"tauri-specta",
|
"tauri-specta",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tracing-subscriber",
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Ok(false)
|
RelayPoolNotification::Shutdown => break,
|
||||||
})
|
_ => (),
|
||||||
.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
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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">
|
||||||
<User.Avatar className="size-8 rounded-full" />
|
<button type="button" onClick={(e) => showContextMenu(e)}>
|
||||||
|
<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">
|
||||||
<User.Name className="font-medium text-blue-500" suffix=":" />
|
<button type="button" onClick={(e) => showContextMenu(e)}>
|
||||||
|
<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}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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));
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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") {
|
||||||
|
|||||||
Reference in New Issue
Block a user