.
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 1m30s
Rust / build (ubuntu-latest, stable) (pull_request) Failing after 1m15s

This commit is contained in:
2026-02-07 20:52:17 +07:00
parent 253d04f988
commit 031883c278
10 changed files with 393 additions and 745 deletions

View File

@@ -2,7 +2,7 @@ use std::collections::HashMap;
use std::time::Duration;
use anyhow::{Context as AnyhowContext, Error};
use common::{shorten_pubkey, RenderedProfile, RenderedTimestamp, BOOTSTRAP_RELAYS};
use common::{shorten_pubkey, RenderedTimestamp, BOOTSTRAP_RELAYS};
use gpui::prelude::FluentBuilder;
use gpui::{
div, px, relative, rems, uniform_list, App, AppContext, Context, Div, Entity,
@@ -11,7 +11,7 @@ use gpui::{
use nostr_sdk::prelude::*;
use person::{Person, PersonRegistry};
use smallvec::{smallvec, SmallVec};
use state::{NostrAddress, NostrRegistry};
use state::{NostrAddress, NostrRegistry, TIMEOUT};
use theme::ActiveTheme;
use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants};
@@ -22,56 +22,117 @@ pub fn init(public_key: PublicKey, window: &mut Window, cx: &mut App) -> Entity<
cx.new(|cx| Screening::new(public_key, window, cx))
}
/// Screening
pub struct Screening {
profile: Person,
/// Public Key of the person being screened.
public_key: PublicKey,
/// Whether the person's address is verified.
verified: bool,
/// Whether the person is followed by current user.
followed: bool,
/// Last time the person was active.
last_active: Option<Timestamp>,
mutual_contacts: Vec<Profile>,
_tasks: SmallVec<[Task<()>; 3]>,
/// All mutual contacts of the person being screened.
mutual_contacts: Vec<PublicKey>,
/// Async tasks
tasks: SmallVec<[Task<()>; 3]>,
}
impl Screening {
pub fn new(public_key: PublicKey, window: &mut Window, cx: &mut Context<Self>) -> Self {
let http_client = cx.http_client();
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get(&public_key, cx);
let mut tasks = smallvec![];
// Check WOT
let contact_check: Task<Result<(bool, Vec<Profile>), Error>> = cx.background_spawn({
let client = nostr.read(cx).client();
async move {
let signer = client.signer().context("Signer not found")?;
let signer_pubkey = signer.get_public_key().await?;
// Check if user is in contact list
let contacts = client.database().contacts_public_keys(signer_pubkey).await;
let followed = contacts.unwrap_or_default().contains(&public_key);
// Check mutual contacts
let contact_list = Filter::new().kind(Kind::ContactList).pubkey(public_key);
let mut mutual_contacts = vec![];
if let Ok(events) = client.database().query(contact_list).await {
for event in events.into_iter().filter(|ev| ev.pubkey != signer_pubkey) {
if let Ok(metadata) = client.database().metadata(event.pubkey).await {
let profile = Profile::new(event.pubkey, metadata.unwrap_or_default());
mutual_contacts.push(profile);
}
}
}
Ok((followed, mutual_contacts))
}
cx.defer_in(window, move |this, _window, cx| {
this.check_contact(cx);
this.check_wot(cx);
this.check_last_activity(cx);
this.verify_identifier(cx);
});
// Check the last activity
let activity_check = cx.background_spawn(async move {
Self {
public_key,
verified: false,
followed: false,
last_active: None,
mutual_contacts: vec![],
tasks: smallvec![],
}
}
fn check_contact(&mut self, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let public_key = self.public_key;
let task: Task<Result<bool, Error>> = cx.background_spawn(async move {
let signer = client.signer().context("Signer not found")?;
let signer_pubkey = signer.get_public_key().await?;
// Check if user is in contact list
let contacts = client.database().contacts_public_keys(signer_pubkey).await;
let followed = contacts.unwrap_or_default().contains(&public_key);
Ok(followed)
});
self.tasks.push(cx.spawn(async move |this, cx| {
let result = task.await.unwrap_or(false);
this.update(cx, |this, cx| {
this.followed = result;
cx.notify();
})
.ok();
}));
}
fn check_wot(&mut self, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let public_key = self.public_key;
let task: Task<Result<Vec<PublicKey>, Error>> = cx.background_spawn(async move {
let signer = client.signer().context("Signer not found")?;
let signer_pubkey = signer.get_public_key().await?;
// Check mutual contacts
let filter = Filter::new().kind(Kind::ContactList).pubkey(public_key);
let mut mutual_contacts = vec![];
if let Ok(events) = client.database().query(filter).await {
for event in events.into_iter().filter(|ev| ev.pubkey != signer_pubkey) {
mutual_contacts.push(event.pubkey);
}
}
Ok(mutual_contacts)
});
self.tasks.push(cx.spawn(async move |this, cx| {
match task.await {
Ok(contacts) => {
this.update(cx, |this, cx| {
this.mutual_contacts = contacts;
cx.notify();
})
.ok();
}
Err(e) => {
log::error!("Failed to fetch mutual contacts: {}", e);
}
};
}));
}
fn check_last_activity(&mut self, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let public_key = self.public_key;
let task: Task<Option<Timestamp>> = cx.background_spawn(async move {
let filter = Filter::new().author(public_key).limit(1);
let mut activity: Option<Timestamp> = None;
@@ -83,7 +144,7 @@ impl Screening {
if let Ok(mut stream) = client
.stream_events(target)
.timeout(Duration::from_secs(2))
.timeout(Duration::from_secs(TIMEOUT))
.await
{
while let Some((_url, event)) = stream.next().await {
@@ -96,78 +157,61 @@ impl Screening {
activity
});
// Verify the NIP05 address if available
let addr_check = profile.metadata().nip05.and_then(|address| {
Nip05Address::parse(&address).ok().map(|addr| {
cx.background_spawn(async move { addr.verify(&http_client, &public_key).await })
self.tasks.push(cx.spawn(async move |this, cx| {
let result = task.await;
this.update(cx, |this, cx| {
this.last_active = result;
cx.notify();
})
});
tasks.push(
// Run the contact check in the background
cx.spawn_in(window, async move |this, cx| {
if let Ok((followed, mutual_contacts)) = contact_check.await {
this.update(cx, |this, cx| {
this.followed = followed;
this.mutual_contacts = mutual_contacts;
cx.notify();
})
.ok();
}
}),
);
tasks.push(
// Run the activity check in the background
cx.spawn_in(window, async move |this, cx| {
let active = activity_check.await;
this.update(cx, |this, cx| {
this.last_active = active;
cx.notify();
})
.ok();
}),
);
tasks.push(
// Run the NIP-05 verification in the background
cx.spawn_in(window, async move |this, cx| {
if let Some(task) = addr_check {
if let Ok(verified) = task.await {
this.update(cx, |this, cx| {
this.verified = verified;
cx.notify();
})
.ok();
}
}
}),
);
Self {
profile,
verified: false,
followed: false,
last_active: None,
mutual_contacts: vec![],
_tasks: tasks,
}
.ok();
}));
}
fn address(&self, _cx: &Context<Self>) -> Option<String> {
self.profile.metadata().nip05
fn verify_identifier(&mut self, cx: &mut Context<Self>) {
let http_client = cx.http_client();
let public_key = self.public_key;
// Skip if the user doesn't have a NIP-05 identifier
let Some(address) = self.address(cx) else {
return;
};
let task: Task<Result<bool, Error>> =
cx.background_spawn(async move { address.verify(&http_client, &public_key).await });
self.tasks.push(cx.spawn(async move |this, cx| {
let result = task.await.unwrap_or(false);
this.update(cx, |this, cx| {
this.verified = result;
cx.notify();
})
.ok();
}));
}
fn open_njump(&mut self, _window: &mut Window, cx: &mut App) {
let Ok(bech32) = self.profile.public_key().to_bech32();
fn profile(&self, cx: &Context<Self>) -> Person {
let persons = PersonRegistry::global(cx);
persons.read(cx).get(&self.public_key, cx)
}
fn address(&self, cx: &Context<Self>) -> Option<Nip05Address> {
self.profile(cx)
.metadata()
.nip05
.and_then(|addr| Nip05Address::parse(&addr).ok())
}
fn open_njump(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
let Ok(bech32) = self.profile(cx).public_key().to_bech32();
cx.open_url(&format!("https://njump.me/{bech32}"));
}
fn report(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let public_key = self.profile.public_key();
let public_key = self.public_key;
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let tag = Tag::public_key_report(public_key, Report::Impersonation);
@@ -180,7 +224,7 @@ impl Screening {
Ok(())
});
cx.spawn_in(window, async move |_, cx| {
self.tasks.push(cx.spawn_in(window, async move |_, cx| {
if task.await.is_ok() {
cx.update(|window, cx| {
window.close_modal(cx);
@@ -188,8 +232,7 @@ impl Screening {
})
.ok();
}
})
.detach();
}));
}
fn mutual_contacts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@@ -202,25 +245,27 @@ impl Screening {
this.title(SharedString::from("Mutual contacts")).child(
v_flex().gap_1().pb_4().child(
uniform_list("contacts", total, move |range, _window, cx| {
let persons = PersonRegistry::global(cx);
let mut items = Vec::with_capacity(total);
for ix in range {
if let Some(contact) = contacts.get(ix) {
items.push(
h_flex()
.h_11()
.w_full()
.px_2()
.gap_1p5()
.rounded(cx.theme().radius)
.text_sm()
.hover(|this| {
this.bg(cx.theme().elevated_surface_background)
})
.child(Avatar::new(contact.avatar()).size(rems(1.75)))
.child(contact.display_name()),
);
}
let Some(contact) = contacts.get(ix) else {
continue;
};
let profile = persons.read(cx).get(contact, cx);
items.push(
h_flex()
.h_11()
.w_full()
.px_2()
.gap_1p5()
.rounded(cx.theme().radius)
.text_sm()
.hover(|this| this.bg(cx.theme().elevated_surface_background))
.child(Avatar::new(profile.avatar()).size(rems(1.75)))
.child(profile.name()),
);
}
items
@@ -234,7 +279,9 @@ impl Screening {
impl Render for Screening {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let shorten_pubkey = shorten_pubkey(self.profile.public_key(), 8);
let profile = self.profile(cx);
let shorten_pubkey = shorten_pubkey(self.public_key, 8);
let total_mutuals = self.mutual_contacts.len();
let last_active = self.last_active.map(|_| true);
@@ -246,12 +293,12 @@ impl Render for Screening {
.items_center()
.justify_center()
.text_center()
.child(Avatar::new(self.profile.avatar()).size(rems(4.)))
.child(Avatar::new(profile.avatar()).size(rems(4.)))
.child(
div()
.font_semibold()
.line_height(relative(1.25))
.child(self.profile.name()),
.child(profile.name()),
),
)
.child(