From a3389ec8930d54e5692b0898366930dceae565c3 Mon Sep 17 00:00:00 2001 From: reya Date: Mon, 3 Feb 2025 16:01:13 +0700 Subject: [PATCH] feat: add contacts panel --- crates/app/src/views/contacts/mod.rs | 112 ++++++++++++++++++++++-- crates/app/src/views/sidebar/compose.rs | 37 ++++---- 2 files changed, 124 insertions(+), 25 deletions(-) diff --git a/crates/app/src/views/contacts/mod.rs b/crates/app/src/views/contacts/mod.rs index 7b7c925..05bb18b 100644 --- a/crates/app/src/views/contacts/mod.rs +++ b/crates/app/src/views/contacts/mod.rs @@ -1,11 +1,20 @@ +use common::profile::NostrProfile; use gpui::{ - div, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, - IntoElement, ParentElement, Render, SharedString, Styled, Window, + div, img, px, uniform_list, AnyElement, App, AppContext, Context, Entity, EventEmitter, + FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Render, SharedString, + Styled, Window, }; +use nostr_sdk::prelude::*; +use state::get_client; +use tokio::sync::oneshot; use ui::{ button::Button, dock_area::panel::{Panel, PanelEvent}, + indicator::Indicator, popup_menu::PopupMenu, + prelude::FluentBuilder, + theme::{scale::ColorScaleStep, ActiveTheme}, + Sizable, }; pub fn init(window: &mut Window, cx: &mut App) -> Entity { @@ -13,6 +22,8 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity { } pub struct Contacts { + contacts: Entity>>, + // Panel name: SharedString, closable: bool, zoomable: bool, @@ -21,7 +32,42 @@ pub struct Contacts { impl Contacts { pub fn new(_window: &mut Window, cx: &mut App) -> Entity { + let contacts = cx.new(|_| None); + let async_contact = contacts.clone(); + + cx.spawn(|mut cx| async move { + let client = get_client(); + let (tx, rx) = oneshot::channel::>(); + + cx.background_executor() + .spawn(async move { + let signer = client.signer().await.unwrap(); + let public_key = signer.get_public_key().await.unwrap(); + + if let Ok(profiles) = client.database().contacts(public_key).await { + let members: Vec = profiles + .into_iter() + .map(|profile| { + NostrProfile::new(profile.public_key(), profile.metadata()) + }) + .collect(); + + _ = tx.send(members); + } + }) + .detach(); + + if let Ok(contacts) = rx.await { + _ = cx.update_entity(&async_contact, |this, cx| { + *this = Some(contacts); + cx.notify(); + }); + } + }) + .detach(); + cx.new(|cx| Self { + contacts, name: "Contacts".into(), closable: true, zoomable: true, @@ -65,12 +111,60 @@ impl Focusable for Contacts { } impl Render for Contacts { - fn render(&mut self, _window: &mut gpui::Window, _cx: &mut Context) -> impl IntoElement { - div() - .size_full() - .flex() - .items_center() - .justify_center() - .child("Contacts") + fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context) -> impl IntoElement { + div().size_full().pt_2().px_2().map(|this| { + if let Some(contacts) = self.contacts.read(cx).clone() { + this.child( + uniform_list( + cx.entity().clone(), + "contacts", + contacts.len(), + move |_, range, _window, cx| { + let mut items = Vec::new(); + + for ix in range { + let item = contacts.get(ix).unwrap().clone(); + + items.push( + div() + .w_full() + .h_9() + .px_2() + .flex() + .items_center() + .justify_between() + .rounded(px(cx.theme().radius)) + .child( + div() + .flex() + .items_center() + .gap_2() + .text_xs() + .child( + div() + .flex_shrink_0() + .child(img(item.avatar()).size_6()), + ) + .child(item.name()), + ) + .hover(|this| { + this.bg(cx.theme().base.step(cx, ColorScaleStep::THREE)) + }), + ); + } + + items + }, + ) + .h_full(), + ) + } else { + this.flex() + .items_center() + .justify_center() + .h_16() + .child(Indicator::new().small()) + } + }) } } diff --git a/crates/app/src/views/sidebar/compose.rs b/crates/app/src/views/sidebar/compose.rs index 9e96ce3..dd929e4 100644 --- a/crates/app/src/views/sidebar/compose.rs +++ b/crates/app/src/views/sidebar/compose.rs @@ -13,6 +13,7 @@ use nostr_sdk::prelude::*; use serde::Deserialize; use state::get_client; use std::{collections::HashSet, time::Duration}; +use tokio::sync::oneshot; use ui::{ button::{Button, ButtonRounded}, indicator::Indicator, @@ -78,31 +79,35 @@ impl Compose { ) .detach(); - cx.spawn(|this, mut async_cx| async move { - let query: anyhow::Result, anyhow::Error> = async_cx - .background_executor() + cx.spawn(|this, mut cx| async move { + let client = get_client(); + let (tx, rx) = oneshot::channel::>(); + + cx.background_executor() .spawn(async move { - let client = get_client(); - let signer = client.signer().await?; - let public_key = signer.get_public_key().await?; - let profiles = client.database().contacts(public_key).await?; - let members: Vec = profiles - .into_iter() - .map(|profile| NostrProfile::new(profile.public_key(), profile.metadata())) - .collect(); + let signer = client.signer().await.unwrap(); + let public_key = signer.get_public_key().await.unwrap(); - Ok(members) + if let Ok(profiles) = client.database().contacts(public_key).await { + let members: Vec = profiles + .into_iter() + .map(|profile| { + NostrProfile::new(profile.public_key(), profile.metadata()) + }) + .collect(); + + _ = tx.send(members); + } }) - .await; + .detach(); - if let Ok(contacts) = query { + if let Ok(contacts) = rx.await { if let Some(view) = this.upgrade() { - _ = async_cx.update_entity(&view, |this, cx| { + _ = cx.update_entity(&view, |this, cx| { this.contacts.update(cx, |this, cx| { *this = Some(contacts); cx.notify(); }); - cx.notify(); }); }