wip: onboarding

This commit is contained in:
2025-12-18 09:00:29 +07:00
parent 4dffc4de20
commit d6504a8170
17 changed files with 262 additions and 214 deletions

26
Cargo.lock generated
View File

@@ -3512,7 +3512,6 @@ dependencies = [
"anyhow", "anyhow",
"assets", "assets",
"common", "common",
"dirs 5.0.1",
"flume 0.12.0", "flume 0.12.0",
"futures", "futures",
"gpui", "gpui",
@@ -3522,7 +3521,6 @@ dependencies = [
"log", "log",
"nostr-connect", "nostr-connect",
"nostr-sdk", "nostr-sdk",
"oneshot",
"person", "person",
"reqwest_client", "reqwest_client",
"serde", "serde",
@@ -4015,6 +4013,20 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "note"
version = "0.0.1"
dependencies = [
"anyhow",
"common",
"flume 0.12.0",
"gpui",
"log",
"nostr-sdk",
"smallvec",
"state",
]
[[package]] [[package]]
name = "notify" name = "notify"
version = "7.0.0" version = "7.0.0"
@@ -4315,12 +4327,6 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "oneshot"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ce411919553d3f9fa53a0880544cda985a112117a0444d5ff1e870a893d6ea"
[[package]] [[package]]
name = "oo7" name = "oo7"
version = "0.5.0" version = "0.5.0"
@@ -4552,13 +4558,11 @@ version = "0.0.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"common", "common",
"flume 0.12.0",
"gpui", "gpui",
"log", "log",
"nostr-sdk", "nostr-sdk",
"serde",
"serde_json",
"smallvec", "smallvec",
"smol",
"state", "state",
] ]

View File

@@ -28,8 +28,8 @@ dirs = "5.0"
futures = "0.3" futures = "0.3"
itertools = "0.13.0" itertools = "0.13.0"
log = "0.4" log = "0.4"
oneshot = "0.1.10"
reqwest = { version = "0.12", features = ["multipart", "stream", "json"] } reqwest = { version = "0.12", features = ["multipart", "stream", "json"] }
flume = { version = "0.12", default-features = false, features = ["async", "select"] }
rust-embed = "8.5.0" rust-embed = "8.5.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"

1
assets/icons/check.svg Normal file
View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="#000000"><path d="M5 13L9 17L19 7" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>

After

Width:  |  Height:  |  Size: 294 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="#000000"><path d="M9 6L15 12L9 18" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>

After

Width:  |  Height:  |  Size: 294 B

View File

@@ -1,6 +1,3 @@
use std::collections::HashSet;
use std::env;
use anyhow::Error; use anyhow::Error;
use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task}; use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task};
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
@@ -19,9 +16,6 @@ pub struct Account {
/// Public Key of the account /// Public Key of the account
public_key: Option<PublicKey>, public_key: Option<PublicKey>,
/// Contact List of the account
pub contacts: Entity<HashSet<PublicKey>>,
/// Event subscriptions /// Event subscriptions
_subscriptions: SmallVec<[Subscription; 1]>, _subscriptions: SmallVec<[Subscription; 1]>,
@@ -52,39 +46,7 @@ impl Account {
/// Create a new account instance /// Create a new account instance
fn new(cx: &mut Context<Self>) -> Self { fn new(cx: &mut Context<Self>) -> Self {
let contacts = cx.new(|_| HashSet::default());
// Collect command line arguments
let args: Vec<String> = env::args().collect();
let account = args.get(1).and_then(|s| Keys::parse(s).ok());
let mut subscriptions = smallvec![]; let mut subscriptions = smallvec![];
let mut tasks = smallvec![];
if let Some(keys) = account {
let public_key = keys.public_key();
tasks.push(
// Set signer in background
cx.spawn(async move |this, cx| {
// Set the signer
cx.background_executor()
.await_on_background(async move {
let client = client();
client.set_signer(keys).await;
log::info!("Signer is set");
})
.await;
// Update state
this.update(cx, |this, cx| {
this.public_key = Some(public_key);
cx.notify();
})
}),
);
}
subscriptions.push( subscriptions.push(
// Listen for public key set // Listen for public key set
@@ -97,9 +59,8 @@ impl Account {
Self { Self {
public_key: None, public_key: None,
contacts,
_subscriptions: subscriptions, _subscriptions: subscriptions,
_tasks: tasks, _tasks: smallvec![],
} }
} }
@@ -126,7 +87,24 @@ impl Account {
// Subscribe to the user's contact list // Subscribe to the user's contact list
client.subscribe(filter, Some(opts)).await?; client.subscribe(filter, Some(opts)).await?;
log::info!("Subscribed to user metadata and contact list"); // Construct a filter to get the user's other metadata
let filter = Filter::new()
.kinds(vec![
Kind::MuteList,
Kind::Bookmarks,
Kind::BookmarkSet,
Kind::SearchRelays,
Kind::BlockedRelays,
Kind::RelaySet,
Kind::Custom(10012),
])
.author(public_key)
.limit(24);
// Subscribe to the user's other metadata
client.subscribe(filter, Some(opts)).await?;
log::info!("Subscribed to user metadata");
Ok(()) Ok(())
}); });
@@ -144,30 +122,4 @@ impl Account {
// This method is only called when user is logged in, so unwrap safely // This method is only called when user is logged in, so unwrap safely
self.public_key.unwrap() self.public_key.unwrap()
} }
/// Load the contacts of the account from the database
pub fn load_contacts(&mut self, cx: &mut Context<Self>) {
let task: Task<Result<HashSet<PublicKey>, Error>> = cx.background_spawn(async move {
let client = client();
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
let contacts = client.database().contacts_public_keys(public_key).await?;
Ok(contacts)
});
self._tasks.push(cx.spawn(async move |this, cx| {
if let Ok(contacts) = task.await {
this.update(cx, |this, cx| {
this.contacts.update(cx, |this, cx| {
this.extend(contacts);
cx.notify();
});
})
.ok();
}
Ok(())
}));
}
} }

View File

@@ -1,4 +1,7 @@
/// Client (or application) name.
pub const CLIENT_NAME: &str = "Lume"; pub const CLIENT_NAME: &str = "Lume";
/// Application ID.
pub const APP_ID: &str = "su.reya.lume"; pub const APP_ID: &str = "su.reya.lume";
/// Bootstrap Relays. /// Bootstrap Relays.
@@ -10,13 +13,6 @@ pub const BOOTSTRAP_RELAYS: [&str; 5] = [
"wss://purplepag.es", "wss://purplepag.es",
]; ];
/// Search Relays.
pub const SEARCH_RELAYS: [&str; 3] = [
"wss://relay.nostr.band",
"wss://search.nos.today",
"wss://relay.noswhere.com",
];
/// Default relay for Nostr Connect /// Default relay for Nostr Connect
pub const NOSTR_CONNECT_RELAY: &str = "wss://relay.nsec.app"; pub const NOSTR_CONNECT_RELAY: &str = "wss://relay.nsec.app";

View File

@@ -27,12 +27,10 @@ anyhow.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
itertools.workspace = true itertools.workspace = true
dirs.workspace = true
log.workspace = true log.workspace = true
smallvec.workspace = true smallvec.workspace = true
smol.workspace = true smol.workspace = true
futures.workspace = true futures.workspace = true
oneshot.workspace = true flume.workspace = true
tracing-subscriber = { version = "0.3.18", features = ["fmt"] } tracing-subscriber = { version = "0.3.18", features = ["fmt"] }
flume = { version = "0.12", default-features = false, features = ["async", "select"] }

View File

@@ -1,7 +1,7 @@
use std::sync::Arc; use std::sync::Arc;
use assets::Assets; use assets::Assets;
use common::{APP_ID, BOOTSTRAP_RELAYS, CLIENT_NAME, SEARCH_RELAYS}; use common::{APP_ID, BOOTSTRAP_RELAYS, CLIENT_NAME};
use gpui::{ use gpui::{
point, px, size, AppContext, Application, Bounds, SharedString, TitlebarOptions, point, px, size, AppContext, Application, Bounds, SharedString, TitlebarOptions,
WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, WindowOptions, WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, WindowOptions,
@@ -40,11 +40,6 @@ fn main() {
client.add_relay(url).await.ok(); client.add_relay(url).await.ok();
} }
// Add search relays to the relay pool
for url in SEARCH_RELAYS {
client.add_relay(url).await.ok();
}
// Connect to all added relays // Connect to all added relays
client.connect().await; client.connect().await;
}) })

View File

@@ -15,6 +15,7 @@ pub fn init(title: impl Into<SharedString>, cx: &mut App) -> Entity<AppMenuBar>
cx.observe_global::<Theme>({ cx.observe_global::<Theme>({
let title = title.clone(); let title = title.clone();
let app_menu_bar = app_menu_bar.clone(); let app_menu_bar = app_menu_bar.clone();
move |cx| { move |cx| {
update_app_menu(title.clone(), app_menu_bar.clone(), cx); update_app_menu(title.clone(), app_menu_bar.clone(), cx);
} }
@@ -26,6 +27,7 @@ pub fn init(title: impl Into<SharedString>, cx: &mut App) -> Entity<AppMenuBar>
fn update_app_menu(title: impl Into<SharedString>, app_menu_bar: Entity<AppMenuBar>, cx: &mut App) { fn update_app_menu(title: impl Into<SharedString>, app_menu_bar: Entity<AppMenuBar>, cx: &mut App) {
let mode = cx.theme().mode; let mode = cx.theme().mode;
cx.set_menus(vec![ cx.set_menus(vec![
Menu { Menu {
name: title.into(), name: title.into(),
@@ -87,6 +89,7 @@ fn update_app_menu(title: impl Into<SharedString>, app_menu_bar: Entity<AppMenuB
fn theme_menu(cx: &App) -> MenuItem { fn theme_menu(cx: &App) -> MenuItem {
let themes = ThemeRegistry::global(cx).sorted_themes(); let themes = ThemeRegistry::global(cx).sorted_themes();
let current_name = cx.theme().theme_name(); let current_name = cx.theme().theme_name();
MenuItem::Submenu(Menu { MenuItem::Submenu(Menu {
name: "Theme".into(), name: "Theme".into(),
items: themes items: themes

View File

@@ -1,2 +1,3 @@
pub mod feed; pub mod feed;
pub mod onboarding;
pub mod startup; pub mod startup;

View File

@@ -0,0 +1,45 @@
use gpui::{
div, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, IntoElement,
ParentElement, Render, SharedString, Window,
};
use gpui_component::dock::{Panel, PanelEvent};
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Onboarding> {
cx.new(|cx| Onboarding::new(window, cx))
}
pub struct Onboarding {
focus_handle: FocusHandle,
}
impl Onboarding {
fn new(_window: &mut Window, cx: &mut Context<Self>) -> Self {
Self {
focus_handle: cx.focus_handle(),
}
}
}
impl Panel for Onboarding {
fn panel_name(&self) -> &'static str {
"Onboarding"
}
fn title(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
SharedString::from("Onboarding")
}
}
impl EventEmitter<PanelEvent> for Onboarding {}
impl Focusable for Onboarding {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Render for Onboarding {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div().child("Onboarding")
}
}

View File

@@ -1,4 +1,3 @@
use account::Account;
use common::BOOTSTRAP_RELAYS; use common::BOOTSTRAP_RELAYS;
use gpui::{ use gpui::{
div, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, div, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
@@ -47,8 +46,7 @@ impl Focusable for Sidebar {
impl Render for Sidebar { impl Render for Sidebar {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let person = PersonRegistry::global(cx); let person = PersonRegistry::global(cx);
let account = Account::global(cx); let contacts = Vec::new();
let contacts = account.read(cx).contacts.read(cx);
v_flex() v_flex()
.id("sidebar-wrapper") .id("sidebar-wrapper")

View File

@@ -1,22 +1,18 @@
use std::collections::HashSet;
use std::sync::Arc; use std::sync::Arc;
use account::Account; use account::Account;
use anyhow::Error; use common::{CLIENT_NAME, DEFAULT_SIDEBAR_WIDTH};
use common::{BOOTSTRAP_RELAYS, CLIENT_NAME, DEFAULT_SIDEBAR_WIDTH};
use gpui::{ use gpui::{
div, px, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement, ParentElement, div, px, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement, ParentElement,
Render, Styled, Subscription, Task, Window, Render, Styled, Subscription, Window,
}; };
use gpui_component::dock::{DockArea, DockItem, DockPlacement, PanelStyle}; use gpui_component::dock::{DockArea, DockItem, DockPlacement, PanelStyle};
use gpui_component::{v_flex, Root, Theme}; use gpui_component::{v_flex, Root, Theme};
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use person::PersonRegistry;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use state::{client, StateEvent};
use crate::panels::feed::Feed; use crate::panels::feed::Feed;
use crate::panels::startup; use crate::panels::{onboarding, startup};
use crate::sidebar; use crate::sidebar;
use crate::title_bar::AppTitleBar; use crate::title_bar::AppTitleBar;
@@ -36,32 +32,23 @@ pub struct Workspace {
/// Event subscriptions /// Event subscriptions
_subscriptions: SmallVec<[Subscription; 1]>, _subscriptions: SmallVec<[Subscription; 1]>,
/// Background tasks
_tasks: SmallVec<[Task<Result<(), Error>>; 2]>,
} }
impl Workspace { impl Workspace {
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self { pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let account = Account::global(cx);
// App's title bar // App's title bar
let title_bar = cx.new(|cx| AppTitleBar::new(CLIENT_NAME, window, cx)); let title_bar = cx.new(|cx| AppTitleBar::new(CLIENT_NAME, window, cx));
// Dock area for the workspace. // Dock area for the workspace.
let dock = let dock = cx.new(|cx| {
cx.new(|cx| DockArea::new("dock", Some(1), window, cx).panel_style(PanelStyle::TabBar)); let startup = Arc::new(onboarding::init(window, cx));
let mut this = DockArea::new("dock", None, window, cx).panel_style(PanelStyle::TabBar);
// Channel for communication between Nostr and GPUI this.set_center(DockItem::panel(startup), window, cx);
let (tx, rx) = flume::bounded::<StateEvent>(2048); this
});
let account = Account::global(cx);
let mut subscriptions = smallvec![]; let mut subscriptions = smallvec![];
let mut tasks = smallvec![];
// Automatically sync theme with system appearance
subscriptions.push(window.observe_window_appearance(|window, cx| {
Theme::sync_system_appearance(Some(window), cx);
}));
// Observe account entity changes // Observe account entity changes
subscriptions.push( subscriptions.push(
@@ -72,98 +59,15 @@ impl Workspace {
}), }),
); );
// Handle nostr notifications // Automatically sync theme with system appearance
tasks.push(cx.background_spawn(async move { subscriptions.push(window.observe_window_appearance(|window, cx| {
let client = client(); Theme::sync_system_appearance(Some(window), cx);
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
let mut notifications = client.notifications();
let mut processed_events: HashSet<EventId> = HashSet::default();
while let Ok(notification) = notifications.recv().await {
let RelayPoolNotification::Message { message, .. } = notification else {
continue;
};
match message {
RelayMessage::Event { event, .. } => {
// Skip if already processed
if !processed_events.insert(event.id) {
continue;
}
match event.kind {
Kind::ContactList => {
// Get all public keys from the event
let public_keys: Vec<PublicKey> =
event.tags.public_keys().copied().collect();
// Construct a filter to get metadata for each public key
let filter = Filter::new()
.kind(Kind::Metadata)
.limit(public_keys.len())
.authors(public_keys);
// Subscribe to metadata events in the bootstrap relays
client
.subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts))
.await?;
// Notify GPUI of received contact list
tx.send_async(StateEvent::ReceivedContactList).await.ok();
}
Kind::Metadata => {
// Parse metadata from event, default if invalid
let metadata =
Metadata::from_json(&event.content).unwrap_or_default();
// Construct nostr profile with metadata and public key
let profile = Box::new(Profile::new(event.pubkey, metadata));
// Notify GPUI of received profile
tx.send_async(StateEvent::ReceivedProfile(profile))
.await
.ok();
}
_ => {}
}
}
RelayMessage::EndOfStoredEvents(_) => {
// TODO
}
_ => {}
}
}
Ok(())
}));
// Handle state events
tasks.push(cx.spawn_in(window, async move |_this, cx| {
while let Ok(event) = rx.recv_async().await {
cx.update(|_window, cx| match event {
StateEvent::ReceivedContactList => {
let account = Account::global(cx);
account.update(cx, |this, cx| {
this.load_contacts(cx);
});
}
StateEvent::ReceivedProfile(profile) => {
let person = PersonRegistry::global(cx);
person.update(cx, |this, cx| {
this.insert_or_update(&profile, cx);
});
}
})
// Entity has been released, ignore any errors
.ok();
}
Ok(())
})); }));
Self { Self {
dock, dock,
title_bar, title_bar,
_subscriptions: subscriptions, _subscriptions: subscriptions,
_tasks: tasks,
} }
} }

17
crates/note/Cargo.toml Normal file
View File

@@ -0,0 +1,17 @@
[package]
name = "note"
version.workspace = true
edition.workspace = true
publish.workspace = true
[dependencies]
common = { path = "../common" }
state = { path = "../state" }
gpui.workspace = true
nostr-sdk.workspace = true
anyhow.workspace = true
smallvec.workspace = true
flume.workspace = true
log.workspace = true

92
crates/note/src/lib.rs Normal file
View File

@@ -0,0 +1,92 @@
use std::collections::{HashMap, HashSet};
use gpui::{App, AppContext, Context, Entity, Global, Task};
use nostr_sdk::prelude::*;
use smallvec::{smallvec, SmallVec};
use state::client;
pub fn init(cx: &mut App) {
NoteRegistry::set_global(cx.new(NoteRegistry::new), cx);
}
struct GlobalNoteRegistry(Entity<NoteRegistry>);
impl Global for GlobalNoteRegistry {}
/// Note Registry
#[derive(Debug)]
pub struct NoteRegistry {
/// Collection of all notes
pub notes: HashMap<EventId, Event>,
/// Tasks for asynchronous operations
_tasks: SmallVec<[Task<()>; 2]>,
}
impl NoteRegistry {
/// Retrieve the global note registry state
pub fn global(cx: &App) -> Entity<Self> {
cx.global::<GlobalNoteRegistry>().0.clone()
}
/// Set the global note registry instance
pub(crate) fn set_global(state: Entity<Self>, cx: &mut App) {
cx.set_global(GlobalNoteRegistry(state));
}
/// Create a new note registry instance
pub(crate) fn new(cx: &mut Context<Self>) -> Self {
let mut tasks = smallvec![];
// Channel for communication between Nostr and GPUI
let (tx, rx) = flume::bounded::<Event>(2048);
tasks.push(
// Handle nostr notifications
cx.background_spawn(async move {
let client = client();
let mut notifications = client.notifications();
let mut processed_events: HashSet<EventId> = HashSet::default();
while let Ok(notification) = notifications.recv().await {
let RelayPoolNotification::Message { message, .. } = notification else {
continue;
};
if let RelayMessage::Event { event, .. } = message {
// Skip if already processed
if !processed_events.insert(event.id) {
continue;
}
if event.kind == Kind::TextNote || event.kind == Kind::Repost {
tx.send_async(event.into_owned()).await.ok();
}
}
}
}),
);
tasks.push(
// Update GPUI state
cx.spawn(async move |this, cx| {
while let Ok(event) = rx.recv_async().await {
this.update(cx, |this, cx| {
this.notes.insert(event.id, event);
cx.notify();
})
.ok();
}
}),
);
Self {
notes: HashMap::new(),
_tasks: tasks,
}
}
pub fn get(&self, id: &EventId) -> Option<&Event> {
self.notes.get(id)
}
}

View File

@@ -13,7 +13,5 @@ nostr-sdk.workspace = true
anyhow.workspace = true anyhow.workspace = true
smallvec.workspace = true smallvec.workspace = true
smol.workspace = true flume.workspace = true
log.workspace = true log.workspace = true
serde.workspace = true
serde_json.workspace = true

View File

@@ -1,4 +1,4 @@
use std::collections::HashMap; use std::collections::{HashMap, HashSet};
use gpui::{App, AppContext, AsyncApp, Context, Entity, Global, Task}; use gpui::{App, AppContext, AsyncApp, Context, Entity, Global, Task};
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
@@ -20,7 +20,7 @@ pub struct PersonRegistry {
pub persons: HashMap<PublicKey, Entity<Profile>>, pub persons: HashMap<PublicKey, Entity<Profile>>,
/// Tasks for asynchronous operations /// Tasks for asynchronous operations
_tasks: SmallVec<[Task<()>; 2]>, _tasks: SmallVec<[Task<()>; 3]>,
} }
impl PersonRegistry { impl PersonRegistry {
@@ -38,6 +38,9 @@ impl PersonRegistry {
pub(crate) fn new(cx: &mut Context<Self>) -> Self { pub(crate) fn new(cx: &mut Context<Self>) -> Self {
let mut tasks = smallvec![]; let mut tasks = smallvec![];
// Channel for communication between Nostr and GPUI
let (tx, rx) = flume::bounded::<Profile>(1024);
tasks.push( tasks.push(
// Load all user profiles from the database // Load all user profiles from the database
cx.spawn(async move |this, cx| { cx.spawn(async move |this, cx| {
@@ -57,6 +60,47 @@ impl PersonRegistry {
}), }),
); );
tasks.push(
// Handle nostr notifications
cx.background_spawn(async move {
let client = client();
let mut notifications = client.notifications();
let mut processed_events: HashSet<EventId> = HashSet::default();
while let Ok(notification) = notifications.recv().await {
let RelayPoolNotification::Message { message, .. } = notification else {
continue;
};
if let RelayMessage::Event { event, .. } = message {
// Skip if already processed
if !processed_events.insert(event.id) {
continue;
}
if event.kind == Kind::Metadata {
let metadata = Metadata::from_json(&event.content).unwrap_or_default();
let profile = Profile::new(event.pubkey, metadata);
tx.send_async(profile).await.ok();
}
}
}
}),
);
tasks.push(
// Update GPUI state
cx.spawn(async move |this, cx| {
while let Ok(profile) = rx.recv_async().await {
this.update(cx, |this, cx| {
this.insert_or_update(&profile, cx);
})
.ok();
}
}),
);
Self { Self {
persons: HashMap::new(), persons: HashMap::new(),
_tasks: tasks, _tasks: tasks,
@@ -110,8 +154,7 @@ impl PersonRegistry {
pub fn get(&self, public_key: &PublicKey, cx: &App) -> Profile { pub fn get(&self, public_key: &PublicKey, cx: &App) -> Profile {
self.persons self.persons
.get(public_key) .get(public_key)
.map(|e| e.read(cx)) .map(|e| e.read(cx).clone())
.cloned()
.unwrap_or(Profile::new(public_key.to_owned(), Metadata::default())) .unwrap_or(Profile::new(public_key.to_owned(), Metadata::default()))
} }
} }