feat(directory): update peer profile to include public key and improve placeholder handling in user directory

This commit is contained in:
cloudwithax 2026-02-15 13:49:30 -05:00
parent 718bd9557e
commit 42f3f86f08
3 changed files with 98 additions and 7 deletions

View File

@ -730,8 +730,46 @@ pub async fn start(
continue; continue;
} }
// never expose relay infrastructure in the user directory
if Some(discovered_peer) == relay_peer {
continue;
}
log::info!("discovered peer {} via rendezvous", discovered_peer); log::info!("discovered peer {} via rendezvous", discovered_peer);
// cache a placeholder entry so global discovery is visible
// before we receive the peer's signed profile announcement
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
let discovered_peer_str = discovered_peer.to_string();
let already_known = storage
.load_directory()
.ok()
.map(|d| d.contains_key(&discovered_peer_str))
.unwrap_or(false);
// add a lightweight placeholder if we have not learned this peer's profile yet
if !already_known {
let placeholder = DirectoryEntry {
peer_id: discovered_peer_str.clone(),
display_name: "discovered peer".to_string(),
bio: String::new(),
public_key: String::new(),
last_seen: now,
is_friend: false,
};
let _ = storage.save_directory_entry(&placeholder);
let _ = app_handle.emit("dusk-event", DuskEvent::ProfileReceived {
peer_id: placeholder.peer_id,
display_name: placeholder.display_name,
bio: placeholder.bio,
public_key: placeholder.public_key,
});
}
// connect through the relay circuit so neither peer reveals their IP // connect through the relay circuit so neither peer reveals their IP
if let Some(ref relay_addr) = relay_multiaddr { if let Some(ref relay_addr) = relay_multiaddr {
let circuit_addr = relay_addr.clone() let circuit_addr = relay_addr.clone()

View File

@ -3,6 +3,7 @@ import {
createSignal, createSignal,
createMemo, createMemo,
createEffect, createEffect,
onCleanup,
For, For,
Show, Show,
} from "solid-js"; } from "solid-js";
@ -29,6 +30,7 @@ import { identity } from "../../stores/identity";
import { setActiveDM } from "../../stores/dms"; import { setActiveDM } from "../../stores/dms";
import { addDMConversation } from "../../stores/dms"; import { addDMConversation } from "../../stores/dms";
import * as tauri from "../../lib/tauri"; import * as tauri from "../../lib/tauri";
import type { DirectoryEntry } from "../../lib/types";
interface UserDirectoryModalProps { interface UserDirectoryModalProps {
isOpen: boolean; isOpen: boolean;
@ -41,10 +43,18 @@ const UserDirectoryModal: Component<UserDirectoryModalProps> = (props) => {
const [searchQuery, setSearchQuery] = createSignal(""); const [searchQuery, setSearchQuery] = createSignal("");
const [activeTab, setActiveTab] = createSignal<DirectoryTab>("all"); const [activeTab, setActiveTab] = createSignal<DirectoryTab>("all");
const [copiedId, setCopiedId] = createSignal<string | null>(null); const [copiedId, setCopiedId] = createSignal<string | null>(null);
const [isSearching, setIsSearching] = createSignal(false);
const [searchResults, setSearchResults] = createSignal<DirectoryEntry[] | null>(
null,
);
// reload directory from disk and trigger fresh discovery when modal opens // reload directory from disk and trigger fresh discovery when modal opens
createEffect(() => { createEffect(() => {
if (props.isOpen) { if (props.isOpen) {
setSearchQuery("");
setIsSearching(false);
setSearchResults(null);
// refresh the in-memory peer list from disk so any profiles received // refresh the in-memory peer list from disk so any profiles received
// while the modal was closed are visible immediately // while the modal was closed are visible immediately
tauri.getKnownPeers().then((peers) => { tauri.getKnownPeers().then((peers) => {
@ -59,13 +69,46 @@ const UserDirectoryModal: Component<UserDirectoryModalProps> = (props) => {
} }
}); });
createEffect(() => {
if (!props.isOpen) return;
const query = searchQuery().trim();
if (!query) {
setIsSearching(false);
setSearchResults(null);
return;
}
let cancelled = false;
setIsSearching(true);
const searchTimeout = window.setTimeout(async () => {
try {
const results = await tauri.searchDirectory(query);
if (!cancelled) {
setSearchResults(results);
setIsSearching(false);
}
} catch {
if (!cancelled) {
setIsSearching(false);
setSearchResults(null);
}
}
}, 180);
onCleanup(() => {
cancelled = true;
window.clearTimeout(searchTimeout);
});
});
// filter out our own peer id from the directory // filter out our own peer id from the directory
const filteredPeers = createMemo(() => { const filteredPeers = createMemo(() => {
const myId = identity()?.peer_id; const myId = identity()?.peer_id;
const query = searchQuery().toLowerCase().trim(); const query = searchQuery().toLowerCase().trim();
const tab = activeTab(); const tab = activeTab();
let peers = knownPeers(); let peers = (searchResults() ?? knownPeers()).filter((p) => p.peer_id !== myId);
if (tab === "friends") { if (tab === "friends") {
peers = peers.filter((p) => p.is_friend); peers = peers.filter((p) => p.is_friend);
@ -77,9 +120,6 @@ const UserDirectoryModal: Component<UserDirectoryModalProps> = (props) => {
p.display_name.toLowerCase().includes(query) || p.display_name.toLowerCase().includes(query) ||
p.peer_id.toLowerCase().includes(query), p.peer_id.toLowerCase().includes(query),
); );
} else {
// if not searching, hide self from the list to avoid confusion
peers = peers.filter((p) => p.peer_id !== myId);
} }
return peers; return peers;
@ -195,6 +235,11 @@ const UserDirectoryModal: Component<UserDirectoryModalProps> = (props) => {
onInput={(e) => setSearchQuery(e.currentTarget.value)} onInput={(e) => setSearchQuery(e.currentTarget.value)}
/> />
</div> </div>
<Show when={isSearching() && searchQuery().trim().length > 0}>
<p class="mt-2 text-[11px] font-mono text-white/35">
searching directory...
</p>
</Show>
</div> </div>
<Divider class="mx-6" /> <Divider class="mx-6" />
@ -207,8 +252,10 @@ const UserDirectoryModal: Component<UserDirectoryModalProps> = (props) => {
<div class="flex flex-col items-center justify-center py-16"> <div class="flex flex-col items-center justify-center py-16">
<Users size={48} class="text-white/10 mb-4" /> <Users size={48} class="text-white/10 mb-4" />
<p class="text-[16px] text-white/30 mb-1"> <p class="text-[16px] text-white/30 mb-1">
{searchQuery() {searchQuery().trim().length > 0
? "no peers matching your search" ? isSearching()
? "searching directory"
: "no peers matching your search"
: activeTab() === "friends" : activeTab() === "friends"
? "no friends added yet" ? "no friends added yet"
: "no peers discovered yet"} : "no peers discovered yet"}

View File

@ -31,7 +31,13 @@ export function updatePeerProfile(
// update existing peer // update existing peer
return prev.map((p) => return prev.map((p) =>
p.peer_id === peerId p.peer_id === peerId
? { ...p, display_name: displayName, bio, last_seen: now } ? {
...p,
display_name: displayName,
bio,
public_key: publicKey || p.public_key,
last_seen: now,
}
: p, : p,
); );
} else { } else {