update send dialog
This commit is contained in:
Generated
+4
@@ -6148,7 +6148,11 @@ dependencies = [
|
|||||||
"gpui-pre",
|
"gpui-pre",
|
||||||
"nip60",
|
"nip60",
|
||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
|
"person",
|
||||||
|
"settings",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
|
"state",
|
||||||
|
"ui",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ publish.workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
common = { path = "../common" }
|
common = { path = "../common" }
|
||||||
nip60 = { path = "../nip60" }
|
nip60 = { path = "../nip60" }
|
||||||
|
person = { path = "../person" }
|
||||||
|
settings = { path = "../settings" }
|
||||||
|
state = { path = "../state" }
|
||||||
|
ui = { path = "../ui" }
|
||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
gpui-kit.workspace = true
|
gpui-kit.workspace = true
|
||||||
|
|||||||
+409
-108
@@ -1,3 +1,4 @@
|
|||||||
|
use std::ops::Range;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use ::nip60::{HistoryEntry, WalletEvent, WalletRegistry};
|
use ::nip60::{HistoryEntry, WalletEvent, WalletRegistry};
|
||||||
@@ -5,20 +6,26 @@ use common::TimestampExt;
|
|||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, AppContext, ClickEvent, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
AnyElement, App, AppContext, ClickEvent, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||||
IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Window, div, px,
|
InteractiveElement as _, IntoElement, ParentElement, Render, SharedString,
|
||||||
relative, uniform_list,
|
StatefulInteractiveElement as _, Styled, Subscription, Task, Window, div, px, relative,
|
||||||
|
uniform_list,
|
||||||
};
|
};
|
||||||
use gpui_kit::assets::IconName;
|
use gpui_kit::assets::IconName;
|
||||||
use gpui_kit::base::StyledExt as _;
|
use gpui_kit::base::StyledExt as _;
|
||||||
use gpui_kit::component::button::{Button, ButtonVariants as _};
|
use gpui_kit::component::button::{Button, ButtonVariants as _};
|
||||||
use gpui_kit::component::clipboard::Clipboard;
|
|
||||||
use gpui_kit::component::dock::{Panel, PanelEvent};
|
use gpui_kit::component::dock::{Panel, PanelEvent};
|
||||||
use gpui_kit::component::input::{Input, InputState};
|
use gpui_kit::component::input::{Input, InputEvent, InputState, NumberInput};
|
||||||
use gpui_kit::component::notification::Notification;
|
use gpui_kit::component::notification::Notification;
|
||||||
use gpui_kit::component::separator::Separator;
|
use gpui_kit::component::separator::Separator;
|
||||||
use gpui_kit::component::{ActiveTheme, Icon, Sizable, WindowExt, h_flex, v_flex};
|
use gpui_kit::component::{
|
||||||
|
ActiveTheme, Disableable as _, Icon, Sizable, WindowExt, h_flex, v_flex,
|
||||||
|
};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
use person::Directory;
|
||||||
|
use settings::AppSettings;
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
use state::NostrRegistry;
|
||||||
|
use ui::Avatar;
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<WalletPanel> {
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<WalletPanel> {
|
||||||
cx.new(|cx| WalletPanel::new(window, cx))
|
cx.new(|cx| WalletPanel::new(window, cx))
|
||||||
@@ -29,8 +36,6 @@ type SubmitFn = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) -> bool>;
|
|||||||
pub struct WalletPanel {
|
pub struct WalletPanel {
|
||||||
name: SharedString,
|
name: SharedString,
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
/// The token produced by the last send, shown for copying
|
|
||||||
sent_token: Option<SharedString>,
|
|
||||||
_subscriptions: SmallVec<[Subscription; 1]>,
|
_subscriptions: SmallVec<[Subscription; 1]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,27 +43,21 @@ impl WalletPanel {
|
|||||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
let wallet = WalletRegistry::global(cx);
|
let wallet = WalletRegistry::global(cx);
|
||||||
let mut subscriptions = SmallVec::new();
|
let mut subscriptions = SmallVec::new();
|
||||||
subscriptions.push(
|
subscriptions.push(cx.subscribe_in(
|
||||||
cx.subscribe_in(
|
|
||||||
&wallet,
|
&wallet,
|
||||||
window,
|
window,
|
||||||
|this, _wallet, event, window, cx| match event {
|
|_this, _wallet, event, window, cx| match event {
|
||||||
WalletEvent::Updated => cx.notify(),
|
WalletEvent::Updated => cx.notify(),
|
||||||
WalletEvent::Error(error) => {
|
WalletEvent::Error(error) => {
|
||||||
window.push_notification(Notification::error(error.clone()), cx);
|
window.push_notification(Notification::error(error.clone()), cx);
|
||||||
}
|
}
|
||||||
WalletEvent::TokenCreated(token) => {
|
WalletEvent::TokenCreated(_) => {}
|
||||||
this.sent_token = Some(token.clone().into());
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
),
|
));
|
||||||
);
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
name: "Wallet".into(),
|
name: "Wallet".into(),
|
||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
sent_token: None,
|
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,42 +108,6 @@ impl WalletPanel {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_sent_token(&self, cx: &Context<Self>) -> impl IntoElement {
|
|
||||||
v_flex()
|
|
||||||
.gap_1p5()
|
|
||||||
.w_full()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_xs()
|
|
||||||
.font_semibold()
|
|
||||||
.text_color(cx.theme().muted_foreground)
|
|
||||||
.child("Sent token:"),
|
|
||||||
)
|
|
||||||
.when_some(self.sent_token.as_ref(), |this, token| {
|
|
||||||
this.child(
|
|
||||||
h_flex()
|
|
||||||
.gap_1()
|
|
||||||
.w_full()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.flex_1()
|
|
||||||
.min_w_0()
|
|
||||||
.truncate()
|
|
||||||
.text_color(cx.theme().muted_foreground)
|
|
||||||
.child(token.clone()),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
Clipboard::new("copy-token")
|
|
||||||
.accessibility_label("Copy cashu token")
|
|
||||||
.value_fn({
|
|
||||||
let token = token.clone();
|
|
||||||
move |_, _| SharedString::from(token.to_string())
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_history(
|
fn render_history(
|
||||||
&mut self,
|
&mut self,
|
||||||
history: Vec<HistoryEntry>,
|
history: Vec<HistoryEntry>,
|
||||||
@@ -217,73 +180,413 @@ impl WalletPanel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ask for an amount and send ecash.
|
/// Pick a contact and an amount, then send a NIP-61 nutzap
|
||||||
fn open_send_dialog(window: &mut Window, cx: &mut App) {
|
fn open_send_dialog(window: &mut Window, cx: &mut App) {
|
||||||
let registry = WalletRegistry::global(cx);
|
let view = cx.new(|cx| SendDialog::new(window, cx));
|
||||||
let receiver_input = cx.new(|cx| InputState::new(window, cx).placeholder("npub… (optional)"));
|
|
||||||
let amount_input = cx.new(|cx| InputState::new(window, cx).placeholder("Amount (sats)"));
|
|
||||||
|
|
||||||
let send: SubmitFn = {
|
window.open_dialog(cx, {
|
||||||
let receiver_input = receiver_input.clone();
|
let view = view.clone();
|
||||||
let amount_input = amount_input.clone();
|
move |this, _window, _cx| this.width(px(420.)).title("Send eCash").child(view.clone())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Rc::new(move |_, window, cx| {
|
struct SendDialog {
|
||||||
let Some(amount) = parse_amount(&amount_input, cx) else {
|
/// Search input, filters contacts or accepts an npub / NIP-05 address
|
||||||
window.push_notification(Notification::error("Enter an amount in sats"), cx);
|
input: Entity<InputState>,
|
||||||
return false;
|
amount_input: Entity<InputState>,
|
||||||
};
|
/// Current user's contacts
|
||||||
|
contacts: Option<Vec<PublicKey>>,
|
||||||
|
/// Person directory for contact names and avatars
|
||||||
|
directory: Entity<Directory>,
|
||||||
|
/// The contact chosen to receive the nutzap
|
||||||
|
selected: Option<PublicKey>,
|
||||||
|
/// Whether a NIP-05 address is being resolved
|
||||||
|
resolving: bool,
|
||||||
|
error: Option<SharedString>,
|
||||||
|
tasks: Vec<Task<Result<(), anyhow::Error>>>,
|
||||||
|
_subscriptions: Vec<Subscription>,
|
||||||
|
}
|
||||||
|
|
||||||
let receiver = receiver_input.read(cx).value().trim().to_string();
|
impl SendDialog {
|
||||||
|
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
|
let input = cx.new(|cx| {
|
||||||
|
InputState::new(window, cx).placeholder("Search or enter npub or user@address.me")
|
||||||
|
});
|
||||||
|
let amount_input = cx.new(|cx| {
|
||||||
|
InputState::new(window, cx)
|
||||||
|
.placeholder("Amount (sats)")
|
||||||
|
.step(1.)
|
||||||
|
.min(1.)
|
||||||
|
});
|
||||||
|
|
||||||
let recipient = match receiver.as_str() {
|
let subscriptions = vec![
|
||||||
"" => None,
|
cx.subscribe_in(&input, window, |this, _input, event, window, cx| {
|
||||||
value => match PublicKey::parse(value) {
|
if let InputEvent::PressEnter { .. } = event {
|
||||||
Ok(recipient) => Some(recipient),
|
this.submit(window, cx);
|
||||||
Err(_) => {
|
|
||||||
window.push_notification(
|
|
||||||
Notification::error("Receiver must be an npub or a public key"),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
},
|
}),
|
||||||
|
cx.subscribe_in(&amount_input, window, |this, _input, event, window, cx| {
|
||||||
|
if let InputEvent::PressEnter { .. } = event {
|
||||||
|
this.send(window, cx);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Load the contacts once the view is mounted
|
||||||
|
cx.defer_in(window, |this, window, cx| {
|
||||||
|
this.load_contacts(window, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
input,
|
||||||
|
amount_input,
|
||||||
|
contacts: None,
|
||||||
|
directory: Directory::new(cx),
|
||||||
|
selected: None,
|
||||||
|
resolving: false,
|
||||||
|
error: None,
|
||||||
|
tasks: Vec::new(),
|
||||||
|
_subscriptions: subscriptions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load the signed-in user's contact list.
|
||||||
|
fn load_contacts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.contacts.is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let client = nostr.read(cx).client();
|
||||||
|
|
||||||
|
let Some(current_user) = nostr.read(cx).current_user() else {
|
||||||
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
match recipient {
|
let task: Task<Result<Vec<PublicKey>, anyhow::Error>> = cx.background_spawn(async move {
|
||||||
Some(recipient) => registry.update(cx, |registry, cx| {
|
let filter = Filter::new()
|
||||||
|
.author(current_user)
|
||||||
|
.kind(Kind::ContactList)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
let contacts: Vec<PublicKey> = client
|
||||||
|
.database()
|
||||||
|
.query(filter)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.map(|event| event.tags.public_keys().collect())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
Ok(contacts)
|
||||||
|
});
|
||||||
|
|
||||||
|
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||||
|
let contacts = task.await;
|
||||||
|
|
||||||
|
this.update_in(cx, |this, _window, cx| {
|
||||||
|
match contacts {
|
||||||
|
Ok(contacts) => {
|
||||||
|
this.directory.update(cx, |directory, cx| {
|
||||||
|
directory.track(contacts.iter().copied(), cx)
|
||||||
|
});
|
||||||
|
this.contacts = Some(contacts);
|
||||||
|
}
|
||||||
|
Err(error) => this.set_error(error.to_string(), cx),
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The contacts matching the current input, or every contact when empty.
|
||||||
|
fn filtered_contacts(&self, cx: &Context<Self>) -> Vec<PublicKey> {
|
||||||
|
let Some(contacts) = self.contacts.as_ref() else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
|
||||||
|
let query = self.input.read(cx).value().trim().to_lowercase();
|
||||||
|
|
||||||
|
if query.is_empty() {
|
||||||
|
return contacts.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
contacts
|
||||||
|
.iter()
|
||||||
|
.filter(|public_key| {
|
||||||
|
if public_key.to_hex().to_lowercase().contains(&query) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = self.directory.read(cx).person(public_key, cx).name();
|
||||||
|
name.to_lowercase().contains(&query)
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve the input into a contact selection.
|
||||||
|
fn submit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.resolving {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let query = self.input.read(cx).value().trim().to_string();
|
||||||
|
|
||||||
|
if query.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A raw public key or an npub can be selected directly.
|
||||||
|
if let Ok(public_key) = PublicKey::parse(&query) {
|
||||||
|
self.select(public_key, cx);
|
||||||
|
self.clear_input(window, cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A NIP-05 address is resolved before being selected.
|
||||||
|
if let Ok(address) = Nip05Address::parse(&query) {
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let task = nostr.read(cx).query_address(address, cx);
|
||||||
|
|
||||||
|
self.set_resolving(true, window, cx);
|
||||||
|
|
||||||
|
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||||
|
let result = task.await;
|
||||||
|
|
||||||
|
this.update_in(cx, |this, window, cx| {
|
||||||
|
this.set_resolving(false, window, cx);
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(public_key) => {
|
||||||
|
this.select(public_key, cx);
|
||||||
|
this.clear_input(window, cx);
|
||||||
|
}
|
||||||
|
Err(error) => this.set_error(error.to_string(), cx),
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to a local search by contact name.
|
||||||
|
let matches = self.filtered_contacts(cx);
|
||||||
|
match matches.len() {
|
||||||
|
0 => self.set_error("Enter a valid npub or NIP-05 address", cx),
|
||||||
|
1 => {
|
||||||
|
let public_key = matches[0];
|
||||||
|
self.select(public_key, cx);
|
||||||
|
self.clear_input(window, cx);
|
||||||
|
}
|
||||||
|
_ => self.set_error("Multiple contacts match, pick one from the list", cx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Select a single recipient for the nutzap.
|
||||||
|
fn select(&mut self, public_key: PublicKey, cx: &mut Context<Self>) {
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
|
||||||
|
if nostr.read(cx).current_user() == Some(public_key) {
|
||||||
|
self.set_error("You cannot send to yourself", cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.directory
|
||||||
|
.update(cx, |directory, cx| directory.track([public_key], cx));
|
||||||
|
self.selected = Some(public_key);
|
||||||
|
self.error = None;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Toggle a contact's selection.
|
||||||
|
fn toggle_selection(&mut self, public_key: &PublicKey, cx: &mut Context<Self>) {
|
||||||
|
if self.selected == Some(*public_key) {
|
||||||
|
self.selected = None;
|
||||||
|
cx.notify();
|
||||||
|
} else {
|
||||||
|
self.select(*public_key, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate the selection and amount, then schedule the nutzap.
|
||||||
|
fn send(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> bool {
|
||||||
|
let Some(recipient) = self.selected else {
|
||||||
|
self.set_error("Select a contact to send to", cx);
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(amount) = parse_amount(&self.amount_input, cx) else {
|
||||||
|
self.set_error("Enter an amount in sats", cx);
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
WalletRegistry::global(cx).update(cx, |registry, cx| {
|
||||||
registry.send_nutzap(recipient, amount, None, None, cx);
|
registry.send_nutzap(recipient, amount, None, None, cx);
|
||||||
}),
|
});
|
||||||
None => registry.update(cx, |registry, cx| {
|
|
||||||
registry.send_token(amount, cx);
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_input(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.input.update(cx, |this, cx| {
|
||||||
|
this.set_value("", window, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Toggle the input's loading spinner while a NIP-05 address is resolved.
|
||||||
|
fn set_resolving(&mut self, resolving: bool, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.input.update(cx, |this, cx| {
|
||||||
|
this.set_loading(resolving, window, cx);
|
||||||
|
});
|
||||||
|
self.resolving = resolving;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_error(&mut self, message: impl Into<SharedString>, cx: &mut Context<Self>) {
|
||||||
|
self.error = Some(message.into());
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_contacts(
|
||||||
|
&self,
|
||||||
|
range: Range<usize>,
|
||||||
|
contacts: &[PublicKey],
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Vec<AnyElement> {
|
||||||
|
contacts
|
||||||
|
.get(range)
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.map(|public_key| {
|
||||||
|
let profile = self.directory.read(cx).person(public_key, cx);
|
||||||
|
let public_key = *public_key;
|
||||||
|
let selected = self.selected == Some(public_key);
|
||||||
|
let id = SharedString::from(format!("contact-{}", public_key.to_hex()));
|
||||||
|
|
||||||
|
h_flex()
|
||||||
|
.id(id)
|
||||||
|
.w_full()
|
||||||
|
.h_8()
|
||||||
|
.px_2()
|
||||||
|
.gap_2()
|
||||||
|
.rounded(cx.theme().radius)
|
||||||
|
.text_sm()
|
||||||
|
.cursor_pointer()
|
||||||
|
.hover(|this| this.bg(cx.theme().list_hover))
|
||||||
|
.when(selected, |this| this.bg(cx.theme().list_active))
|
||||||
|
.when(!AppSettings::get_hide_avatar(cx), |this| {
|
||||||
|
this.child(
|
||||||
|
Avatar::new(profile.avatar())
|
||||||
|
.seed(profile.avatar_seed())
|
||||||
|
.small(),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
.child(div().min_w_0().truncate().child(profile.name()))
|
||||||
|
.on_click(cx.listener(move |this, _event, _window, cx| {
|
||||||
|
this.toggle_selection(&public_key, cx);
|
||||||
|
}))
|
||||||
|
.into_any_element()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_selected(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let Some(public_key) = self.selected else {
|
||||||
|
return div().into_any_element();
|
||||||
};
|
};
|
||||||
|
|
||||||
window.open_dialog(cx, move |this, _window, _cx| {
|
let profile = self.directory.read(cx).person(&public_key, cx);
|
||||||
this.width(px(380.))
|
let id = SharedString::from(format!("selected-{}", public_key.to_hex()));
|
||||||
.title("Send eCash")
|
|
||||||
.on_ok({
|
h_flex()
|
||||||
let send = send.clone();
|
.gap_1()
|
||||||
move |event, window, cx| send(event, window, cx)
|
|
||||||
})
|
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
h_flex()
|
||||||
.gap_3()
|
.id(id)
|
||||||
|
.gap_1()
|
||||||
|
.p_0p5()
|
||||||
|
.rounded_full()
|
||||||
|
.bg(cx.theme().secondary)
|
||||||
|
.hover(|this| this.bg(cx.theme().primary))
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
Avatar::new(profile.avatar())
|
||||||
.gap_1p5()
|
.seed(profile.avatar_seed())
|
||||||
|
.xsmall(),
|
||||||
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
|
.max_w_32()
|
||||||
|
.truncate()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.font_semibold()
|
.line_height(relative(1.0))
|
||||||
.child("To (npub, optional):"),
|
.child(profile.name()),
|
||||||
)
|
)
|
||||||
.child(Input::new(&receiver_input).cleanable(true)),
|
.on_click(cx.listener(move |this, _event, _window, cx| {
|
||||||
|
this.selected = None;
|
||||||
|
cx.notify();
|
||||||
|
})),
|
||||||
)
|
)
|
||||||
.child(Input::new(&amount_input).cleanable(true))
|
.into_any_element()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for SendDialog {
|
||||||
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
const NOTICE: &str = "Recipients need a NIP-60 wallet on a mint you both use.";
|
||||||
|
|
||||||
|
let contacts = Rc::new(self.filtered_contacts(cx));
|
||||||
|
let contact_count = contacts.len();
|
||||||
|
|
||||||
|
v_flex()
|
||||||
|
.gap_3()
|
||||||
|
.child(NumberInput::new(&self.amount_input))
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1()
|
||||||
|
.w_full()
|
||||||
|
.child(Input::new(&self.input).flex_1().cleanable(true))
|
||||||
|
.child(
|
||||||
|
Button::new("add")
|
||||||
|
.icon(IconName::Plus)
|
||||||
|
.tooltip("Add person")
|
||||||
|
.ghost()
|
||||||
|
.large()
|
||||||
|
.disabled(self.resolving)
|
||||||
|
.on_click(cx.listener(|this, _event, window, cx| {
|
||||||
|
this.submit(window, cx);
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.when(self.selected.is_some(), |this| {
|
||||||
|
this.child(self.render_selected(cx))
|
||||||
|
})
|
||||||
|
.child(
|
||||||
|
uniform_list(
|
||||||
|
"send-contacts",
|
||||||
|
contact_count,
|
||||||
|
cx.processor(move |this, range, _window, cx| {
|
||||||
|
this.render_contacts(range, contacts.as_slice(), cx)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.h_64(),
|
||||||
|
)
|
||||||
|
.map(|this| {
|
||||||
|
if let Some(error) = self.error.clone() {
|
||||||
|
this.child(div().text_xs().text_color(cx.theme().danger).child(error))
|
||||||
|
} else {
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().muted_foreground)
|
||||||
|
.child(NOTICE),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
@@ -294,17 +597,18 @@ fn open_send_dialog(window: &mut Window, cx: &mut App) {
|
|||||||
.secondary()
|
.secondary()
|
||||||
.on_click(|_, window, cx| window.close_dialog(cx)),
|
.on_click(|_, window, cx| window.close_dialog(cx)),
|
||||||
)
|
)
|
||||||
.child(Button::new("send").label("Send").primary().on_click({
|
.child(
|
||||||
let send = send.clone();
|
Button::new("send")
|
||||||
move |event, window, cx| {
|
.label("Send")
|
||||||
if send(event, window, cx) {
|
.primary()
|
||||||
|
.on_click(cx.listener(move |this, _event, window, cx| {
|
||||||
|
if this.send(window, cx) {
|
||||||
window.close_dialog(cx);
|
window.close_dialog(cx);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a lightning invoice or redeem a pasted cashu token
|
/// Create a lightning invoice or redeem a pasted cashu token
|
||||||
@@ -467,9 +771,6 @@ impl Render for WalletPanel {
|
|||||||
.text_xs()
|
.text_xs()
|
||||||
.child(self.render_balance(cx))
|
.child(self.render_balance(cx))
|
||||||
.child(self.render_actions())
|
.child(self.render_actions())
|
||||||
.when_some(self.sent_token.as_ref(), |this, _token| {
|
|
||||||
this.child(self.render_sent_token(cx))
|
|
||||||
})
|
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
|
|||||||
Reference in New Issue
Block a user