diff --git a/src-tauri/src/commands/chat.rs b/src-tauri/src/commands/chat.rs index 6fa01b5..88ea71a 100644 --- a/src-tauri/src/commands/chat.rs +++ b/src-tauri/src/commands/chat.rs @@ -157,6 +157,16 @@ pub async fn start_node(app: tauri::AppHandle, state: State<'_, AppState>) -> Re namespace: personal_ns, }) .await; + + // register under the global "dusk/peers" namespace so any peer can + // discover us via the relay tracker, enabling global peer discovery + // without exposing ip addresses (all connections use relay circuit) + let _ = handle + .command_tx + .send(NodeCommand::RegisterRendezvous { + namespace: "dusk/peers".to_string(), + }) + .await; } Ok(()) diff --git a/src-tauri/src/commands/identity.rs b/src-tauri/src/commands/identity.rs index 4ffde8a..93fdef5 100644 --- a/src-tauri/src/commands/identity.rs +++ b/src-tauri/src/commands/identity.rs @@ -258,6 +258,22 @@ pub async fn remove_friend(state: State<'_, AppState>, peer_id: String) -> Resul .map_err(|e| format!("failed to remove friend: {}", e)) } +// discover online peers via the global relay tracker namespace +// this allows finding peers without sharing a community or knowing their peer_id +#[tauri::command] +pub async fn discover_global_peers(state: State<'_, AppState>) -> Result<(), String> { + let node_handle = state.node_handle.lock().await; + if let Some(ref handle) = *node_handle { + let _ = handle + .command_tx + .send(crate::node::NodeCommand::DiscoverRendezvous { + namespace: "dusk/peers".to_string(), + }) + .await; + } + Ok(()) +} + // broadcast a revocation to all peers, stop the node, and wipe all local data #[tauri::command] pub async fn reset_identity(state: State<'_, AppState>) -> Result<(), String> { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index c2cb315..77bd45c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -89,6 +89,7 @@ pub fn run() { commands::identity::get_friends, commands::identity::add_friend, commands::identity::remove_friend, + commands::identity::discover_global_peers, commands::identity::reset_identity, commands::chat::send_message, commands::chat::get_messages, diff --git a/src-tauri/src/node/mod.rs b/src-tauri/src/node/mod.rs index d642f31..30e4c79 100644 --- a/src-tauri/src/node/mod.rs +++ b/src-tauri/src/node/mod.rs @@ -118,6 +118,7 @@ pub enum DuskEvent { peer_id: String, display_name: String, bio: String, + public_key: String, }, #[serde(rename = "profile_revoked")] ProfileRevoked { peer_id: String }, @@ -396,6 +397,7 @@ pub async fn start( peer_id: profile.peer_id, display_name: profile.display_name, bio: profile.bio, + public_key: profile.public_key, }); } crate::protocol::messages::GossipMessage::ProfileRevoke(revocation) => { diff --git a/src/App.tsx b/src/App.tsx index 5f0c003..3a9a041 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -342,6 +342,7 @@ const App: Component = () => { event.payload.peer_id, event.payload.display_name, event.payload.bio, + event.payload.public_key, ); // keep dm conversation names in sync updateDMPeerDisplayName( diff --git a/src/components/directory/UserDirectoryModal.tsx b/src/components/directory/UserDirectoryModal.tsx index 75cea2b..60e2583 100644 --- a/src/components/directory/UserDirectoryModal.tsx +++ b/src/components/directory/UserDirectoryModal.tsx @@ -1,4 +1,11 @@ -import { Component, createSignal, createMemo, For, Show } from "solid-js"; +import { + Component, + createSignal, + createMemo, + createEffect, + For, + Show, +} from "solid-js"; import { Portal } from "solid-js/web"; import { X, @@ -34,6 +41,17 @@ const UserDirectoryModal: Component = (props) => { const [activeTab, setActiveTab] = createSignal("all"); const [copiedId, setCopiedId] = createSignal(null); + // trigger global peer discovery when modal opens + createEffect(() => { + if (props.isOpen) { + // discover peers registered on the relay's global "dusk/peers" namespace + // this allows finding online peers without sharing a community + tauri.discoverGlobalPeers().catch((e) => { + console.error("failed to discover global peers:", e); + }); + } + }); + // filter out our own peer id from the directory const filteredPeers = createMemo(() => { const myId = identity()?.peer_id; diff --git a/src/lib/tauri.ts b/src/lib/tauri.ts index fc96750..12a9dd7 100644 --- a/src/lib/tauri.ts +++ b/src/lib/tauri.ts @@ -202,6 +202,10 @@ export async function removeFriend(peerId: string): Promise { return invoke("remove_friend", { peerId }); } +export async function discoverGlobalPeers(): Promise { + return invoke("discover_global_peers"); +} + export async function resetIdentity(): Promise { return invoke("reset_identity"); } diff --git a/src/lib/types.ts b/src/lib/types.ts index a90bfed..2e5c19f 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -174,7 +174,12 @@ export type DuskEvent = | { kind: "sync_complete"; payload: { community_id: string } } | { kind: "profile_received"; - payload: { peer_id: string; display_name: string; bio: string }; + payload: { + peer_id: string; + display_name: string; + bio: string; + public_key: string; + }; } | { kind: "profile_revoked"; payload: { peer_id: string } } | { kind: "relay_status"; payload: { connected: boolean } } diff --git a/src/stores/directory.ts b/src/stores/directory.ts index 07c9652..d5063f2 100644 --- a/src/stores/directory.ts +++ b/src/stores/directory.ts @@ -21,17 +21,34 @@ export function updatePeerProfile( peerId: string, displayName: string, bio: string, + publicKey: string, ) { const now = Date.now(); - setKnownPeers((prev) => - prev.map((p) => - p.peer_id === peerId - ? { ...p, display_name: displayName, bio, last_seen: now } - : p, - ), - ); + setKnownPeers((prev) => { + const existing = prev.find((p) => p.peer_id === peerId); + if (existing) { + // update existing peer + return prev.map((p) => + p.peer_id === peerId + ? { ...p, display_name: displayName, bio, last_seen: now } + : p, + ); + } else { + // add new peer that just announced themselves + const newEntry: DirectoryEntry = { + peer_id: peerId, + display_name: displayName, + bio, + public_key: publicKey, + last_seen: now, + is_friend: false, + }; + return [...prev, newEntry]; + } + }); + // update friends list if this peer is a friend setFriends((prev) => prev.map((p) => p.peer_id === peerId