use crate::{get_client, states::chat::room::Member}; use gpui::{ div, img, impl_internal_actions, px, uniform_list, Context, FocusHandle, InteractiveElement, IntoElement, Model, ParentElement, Render, StatefulInteractiveElement, Styled, View, ViewContext, VisualContext, WindowContext, }; use nostr_sdk::prelude::*; use serde::Deserialize; use std::{collections::HashSet, time::Duration}; use ui::{ button::{Button, ButtonRounded}, indicator::Indicator, input::{InputEvent, TextInput}, prelude::FluentBuilder, theme::{scale::ColorScaleStep, ActiveTheme}, Icon, IconName, Sizable, Size, StyledExt, }; #[derive(Clone, PartialEq, Eq, Deserialize)] struct SelectContact(PublicKey); impl_internal_actions!(contacts, [SelectContact]); pub struct Compose { title_input: View, message_input: View, user_input: View, contacts: Model>>, selected: Model>, focus_handle: FocusHandle, is_loading: bool, } impl Compose { pub fn new(cx: &mut ViewContext<'_, Self>) -> Self { let contacts = cx.new_model(|_| None); let selected = cx.new_model(|_| HashSet::new()); let user_input = cx.new_view(|cx| { TextInput::new(cx) .text_size(ui::Size::Small) .small() .placeholder("npub1...") }); let title_input = cx.new_view(|cx| { TextInput::new(cx) .appearance(false) .text_size(Size::XSmall) .placeholder("Family...") }); let message_input = cx.new_view(|cx| { TextInput::new(cx) .appearance(false) .text_size(Size::XSmall) .placeholder("Hello...") }); cx.subscribe(&user_input, move |this, _, input_event, cx| { if let InputEvent::PressEnter = input_event { this.add(cx); } }) .detach(); cx.spawn(|this, mut async_cx| { let client = get_client(); async move { let query: anyhow::Result, anyhow::Error> = async_cx .background_executor() .spawn(async move { 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| Member::new(profile.public_key(), profile.metadata())) .collect(); Ok(members) }) .await; if let Ok(contacts) = query { if let Some(view) = this.upgrade() { _ = async_cx.update_view(&view, |this, cx| { this.contacts.update(cx, |this, cx| { *this = Some(contacts); cx.notify(); }); cx.notify(); }); } } } }) .detach(); Self { title_input, message_input, user_input, contacts, selected, is_loading: false, focus_handle: cx.focus_handle(), } } pub fn selected<'a>(&self, cx: &'a WindowContext) -> Vec<&'a PublicKey> { self.selected.read(cx).iter().collect() } fn add(&mut self, cx: &mut ViewContext) { let content = self.user_input.read(cx).text().to_string(); let input = self.user_input.downgrade(); // Show loading spinner self.is_loading = true; cx.notify(); if let Ok(public_key) = PublicKey::parse(&content) { cx.spawn(|this, mut async_cx| async move { let query: anyhow::Result = async_cx .background_executor() .spawn(async move { let client = get_client(); let metadata = client .fetch_metadata(public_key, Duration::from_secs(3)) .await?; Ok(metadata) }) .await; if let Ok(metadata) = query { if let Some(view) = this.upgrade() { _ = async_cx.update_view(&view, |this, cx| { this.contacts.update(cx, |this, cx| { if let Some(members) = this { members.insert(0, Member::new(public_key, metadata)); } cx.notify(); }); this.selected.update(cx, |this, cx| { this.insert(public_key); cx.notify(); }); this.is_loading = false; cx.notify(); }); } if let Some(input) = input.upgrade() { _ = async_cx.update_view(&input, |input, cx| { input.set_text("", cx); }); } } }) .detach(); } else { // Handle error } } fn on_action_select(&mut self, action: &SelectContact, cx: &mut ViewContext) { self.selected.update(cx, |this, cx| { if this.contains(&action.0) { this.remove(&action.0); } else { this.insert(action.0); }; cx.notify(); }); } } impl Render for Compose { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let msg = "Start a conversation with someone using their npub or NIP-05 (like foo@bar.com)."; div() .track_focus(&self.focus_handle) .on_action(cx.listener(Self::on_action_select)) .flex() .flex_col() .gap_1() .child( div() .px_2() .text_xs() .text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN)) .child(msg), ) .child( div() .flex() .flex_col() .child( div() .h_10() .px_2() .border_b_1() .border_color(cx.theme().base.step(cx, ColorScaleStep::FIVE)) .flex() .items_center() .gap_1() .child(div().text_xs().font_semibold().child("Title:")) .child(self.title_input.clone()), ) .child( div() .h_10() .px_2() .border_b_1() .border_color(cx.theme().base.step(cx, ColorScaleStep::FIVE)) .flex() .items_center() .gap_1() .child(div().text_xs().font_semibold().child("Message:")) .child(self.message_input.clone()), ), ) .child( div() .flex() .flex_col() .gap_2() .child(div().px_2().text_xs().font_semibold().child("To:")) .child( div() .flex() .items_center() .gap_2() .px_2() .child( Button::new("add") .icon(IconName::Plus) .small() .rounded(ButtonRounded::Size(px(9999.))) .loading(self.is_loading) .on_click(cx.listener(|this, _, cx| this.add(cx))), ) .child(self.user_input.clone()), ) .map(|this| { if let Some(contacts) = self.contacts.read(cx).clone() { this.child( uniform_list( cx.view().clone(), "contacts", contacts.len(), move |this, range, cx| { let selected = this.selected.read(cx); let mut items = Vec::new(); for ix in range { let item = contacts.get(ix).unwrap().clone(); let is_select = selected.contains(&item.public_key()); items.push( div() .id(ix) .w_full() .h_9() .px_2() .flex() .items_center() .justify_between() .child( div() .flex() .items_center() .gap_2() .text_xs() .child( div().flex_shrink_0().child( img(item.avatar()).size_6(), ), ) .child(item.name()), ) .when(is_select, |this| { this.child( Icon::new(IconName::CircleCheck) .size_3() .text_color(cx.theme().base.step( cx, ColorScaleStep::TWELVE, )), ) }) .hover(|this| { this.bg(cx .theme() .base .step(cx, ColorScaleStep::THREE)) }) .on_click(move |_, cx| { cx.dispatch_action(Box::new( SelectContact(item.public_key()), )); }), ); } items }, ) .h(px(300.)), ) } else { this.flex() .items_center() .justify_center() .h_16() .child(Indicator::new().small()) } }), ) } }