330 lines
13 KiB
Rust
330 lines
13 KiB
Rust
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<TextInput>,
|
|
message_input: View<TextInput>,
|
|
user_input: View<TextInput>,
|
|
contacts: Model<Option<Vec<Member>>>,
|
|
selected: Model<HashSet<PublicKey>>,
|
|
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<Vec<Member>, 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<Member> = 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<Self>) {
|
|
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<Metadata, anyhow::Error> = 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>) {
|
|
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<Self>) -> 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())
|
|
}
|
|
}),
|
|
)
|
|
}
|
|
}
|