Redesign for the v1 stable release #3
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -6069,10 +6069,12 @@ dependencies = [
|
|||||||
"common",
|
"common",
|
||||||
"flume",
|
"flume",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"gpui_tokio",
|
||||||
"log",
|
"log",
|
||||||
"nostr-connect",
|
"nostr-connect",
|
||||||
"nostr-lmdb",
|
"nostr-lmdb",
|
||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
|
"reqwest",
|
||||||
"rustls",
|
"rustls",
|
||||||
"smol",
|
"smol",
|
||||||
"webbrowser",
|
"webbrowser",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use std::sync::Arc;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{anyhow, Context as AnyhowContext, Error};
|
use anyhow::{anyhow, Context as AnyhowContext, Error};
|
||||||
use common::EventUtils;
|
use common::{EventUtils, BOOTSTRAP_RELAYS};
|
||||||
use device::DeviceRegistry;
|
use device::DeviceRegistry;
|
||||||
use flume::Sender;
|
use flume::Sender;
|
||||||
use fuzzy_matcher::skim::SkimMatcherV2;
|
use fuzzy_matcher::skim::SkimMatcherV2;
|
||||||
@@ -16,7 +16,7 @@ use gpui::{
|
|||||||
};
|
};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use state::{tracker, NostrRegistry, RelayState, DEVICE_GIFTWRAP, USER_GIFTWRAP};
|
use state::{tracker, NostrAddress, NostrRegistry, RelayState, DEVICE_GIFTWRAP, USER_GIFTWRAP};
|
||||||
|
|
||||||
mod message;
|
mod message;
|
||||||
mod room;
|
mod room;
|
||||||
@@ -336,6 +336,7 @@ impl ChatRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Emit an open room event.
|
/// Emit an open room event.
|
||||||
|
///
|
||||||
/// If the room is new, add it to the registry.
|
/// If the room is new, add it to the registry.
|
||||||
pub fn emit_room(&mut self, room: WeakEntity<Room>, cx: &mut Context<Self>) {
|
pub fn emit_room(&mut self, room: WeakEntity<Room>, cx: &mut Context<Self>) {
|
||||||
if let Some(room) = room.upgrade() {
|
if let Some(room) = room.upgrade() {
|
||||||
@@ -364,10 +365,65 @@ impl ChatRegistry {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Search rooms by their name.
|
/// Find for rooms that match the query.
|
||||||
pub fn search(&self, query: &str, cx: &App) -> Vec<Entity<Room>> {
|
pub fn find(&self, query: &str, cx: &App) -> Task<Result<Vec<Entity<Room>>, Error>> {
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let client = nostr.read(cx).client();
|
||||||
|
let http_client = cx.http_client();
|
||||||
|
let query = query.to_string();
|
||||||
|
|
||||||
|
if let Ok(addr) = Nip05Address::parse(&query) {
|
||||||
|
let task: Task<Result<PublicKey, Error>> = cx.background_spawn(async move {
|
||||||
|
let profile = addr.profile(&http_client).await?;
|
||||||
|
let public_key = profile.public_key;
|
||||||
|
|
||||||
|
let opts = SubscribeAutoCloseOptions::default()
|
||||||
|
.exit_policy(ReqExitPolicy::ExitOnEOSE)
|
||||||
|
.timeout(Some(Duration::from_secs(3)));
|
||||||
|
|
||||||
|
// Construct the filter for the metadata event
|
||||||
|
let filter = Filter::new()
|
||||||
|
.kind(Kind::Metadata)
|
||||||
|
.author(public_key)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
// Subscribe to bootstrap relays
|
||||||
|
client
|
||||||
|
.subscribe_to(BOOTSTRAP_RELAYS, vec![filter], Some(opts))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(public_key)
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.spawn(async move |cx| {
|
||||||
|
let public_key = task.await?;
|
||||||
|
let results = cx.read_global::<GlobalChatRegistry, _>(|this, cx| {
|
||||||
|
this.0
|
||||||
|
.read_with(cx, |this, cx| this.find_rooms(&public_key.to_hex(), cx))
|
||||||
|
});
|
||||||
|
Ok(results)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
cx.spawn(async move |cx| {
|
||||||
|
let results = cx.read_global::<GlobalChatRegistry, _>(|this, cx| {
|
||||||
|
this.0.read_with(cx, |this, cx| this.find_rooms(&query, cx))
|
||||||
|
});
|
||||||
|
Ok(results)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Internal find function for finding rooms based on a query.
|
||||||
|
fn find_rooms(&self, query: &str, cx: &App) -> Vec<Entity<Room>> {
|
||||||
let matcher = SkimMatcherV2::default();
|
let matcher = SkimMatcherV2::default();
|
||||||
|
|
||||||
|
if let Ok(public_key) = PublicKey::parse(query) {
|
||||||
|
self.rooms
|
||||||
|
.iter()
|
||||||
|
.filter(|room| room.read(cx).members.contains(&public_key))
|
||||||
|
.cloned()
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
self.rooms
|
self.rooms
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|room| {
|
.filter(|room| {
|
||||||
@@ -378,14 +434,27 @@ impl ChatRegistry {
|
|||||||
.cloned()
|
.cloned()
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Search rooms by public keys.
|
/// Construct a chat room based on NIP-05 address.
|
||||||
pub fn search_by_public_key(&self, public_key: PublicKey, cx: &App) -> Vec<Entity<Room>> {
|
pub fn address_to_room(&self, addr: Nip05Address, cx: &App) -> Task<Result<Room, Error>> {
|
||||||
self.rooms
|
let nostr = NostrRegistry::global(cx);
|
||||||
.iter()
|
let client = nostr.read(cx).client();
|
||||||
.filter(|room| room.read(cx).members.contains(&public_key))
|
let http_client = cx.http_client();
|
||||||
.cloned()
|
|
||||||
.collect()
|
cx.background_spawn(async move {
|
||||||
|
let signer = client.signer().await?;
|
||||||
|
let public_key = signer.get_public_key().await?;
|
||||||
|
|
||||||
|
// Get the profile belonging to the address
|
||||||
|
let profile = addr.profile(&http_client).await?;
|
||||||
|
|
||||||
|
// Construct the room
|
||||||
|
let receivers = vec![profile.public_key];
|
||||||
|
let room = Room::new(None, public_key, receivers);
|
||||||
|
|
||||||
|
Ok(room)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reset the registry.
|
/// Reset the registry.
|
||||||
@@ -531,7 +600,7 @@ impl ChatRegistry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a Nostr event into a Coop Message and push it to the belonging room
|
/// Parse a nostr event into a message and push it to the belonging room
|
||||||
///
|
///
|
||||||
/// If the room doesn't exist, it will be created.
|
/// If the room doesn't exist, it will be created.
|
||||||
/// Updates room ordering based on the most recent messages.
|
/// Updates room ordering based on the most recent messages.
|
||||||
@@ -578,7 +647,7 @@ impl ChatRegistry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unwraps a gift-wrapped event and processes its contents.
|
/// Unwraps a gift-wrapped event and processes its contents.
|
||||||
async fn extract_rumor(
|
async fn extract_rumor(
|
||||||
client: &Client,
|
client: &Client,
|
||||||
device_signer: &Option<Arc<dyn NostrSigner>>,
|
device_signer: &Option<Arc<dyn NostrSigner>>,
|
||||||
@@ -602,7 +671,7 @@ impl ChatRegistry {
|
|||||||
Ok(rumor_unsigned)
|
Ok(rumor_unsigned)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper method to try unwrapping with different signers
|
/// Helper method to try unwrapping with different signers
|
||||||
async fn try_unwrap(
|
async fn try_unwrap(
|
||||||
client: &Client,
|
client: &Client,
|
||||||
device_signer: &Option<Arc<dyn NostrSigner>>,
|
device_signer: &Option<Arc<dyn NostrSigner>>,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ pub use constants::*;
|
|||||||
pub use debounced_delay::*;
|
pub use debounced_delay::*;
|
||||||
pub use display::*;
|
pub use display::*;
|
||||||
pub use event::*;
|
pub use event::*;
|
||||||
pub use nip05::*;
|
|
||||||
pub use nip96::*;
|
pub use nip96::*;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
pub use paths::*;
|
pub use paths::*;
|
||||||
@@ -13,7 +12,6 @@ mod constants;
|
|||||||
mod debounced_delay;
|
mod debounced_delay;
|
||||||
mod display;
|
mod display;
|
||||||
mod event;
|
mod event;
|
||||||
mod nip05;
|
|
||||||
mod nip96;
|
mod nip96;
|
||||||
mod paths;
|
mod paths;
|
||||||
|
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
use anyhow::anyhow;
|
|
||||||
use nostr::prelude::*;
|
|
||||||
use reqwest::Client as ReqClient;
|
|
||||||
|
|
||||||
pub async fn nip05_verify(public_key: PublicKey, address: &str) -> Result<bool, anyhow::Error> {
|
|
||||||
let req_client = ReqClient::new();
|
|
||||||
let address = Nip05Address::parse(address)?;
|
|
||||||
|
|
||||||
// Get NIP-05 response
|
|
||||||
let res = req_client.get(address.url().to_string()).send().await?;
|
|
||||||
let json: Value = res.json().await?;
|
|
||||||
|
|
||||||
let verify = nip05::verify_from_json(&public_key, &address, &json);
|
|
||||||
|
|
||||||
Ok(verify)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn nip05_profile(address: &str) -> Result<Nip05Profile, anyhow::Error> {
|
|
||||||
let req_client = ReqClient::new();
|
|
||||||
let address = Nip05Address::parse(address)?;
|
|
||||||
|
|
||||||
// Get NIP-05 response
|
|
||||||
let res = req_client.get(address.url().to_string()).send().await?;
|
|
||||||
let json: Value = res.json().await?;
|
|
||||||
|
|
||||||
if let Ok(profile) = Nip05Profile::from_json(&address, &json) {
|
|
||||||
Ok(profile)
|
|
||||||
} else {
|
|
||||||
Err(anyhow!("Failed to get NIP-05 profile"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
309
crates/coop/src/command_bar.rs
Normal file
309
crates/coop/src/command_bar.rs
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
use std::ops::Range;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use anyhow::Error;
|
||||||
|
use chat::{ChatRegistry, Room};
|
||||||
|
use common::DebouncedDelay;
|
||||||
|
use gpui::prelude::FluentBuilder;
|
||||||
|
use gpui::{
|
||||||
|
anchored, deferred, div, point, px, uniform_list, AppContext, Bounds, Context, Entity,
|
||||||
|
Focusable, InteractiveElement, IntoElement, MouseButton, ParentElement, Point, Render, Styled,
|
||||||
|
Subscription, Task, Window,
|
||||||
|
};
|
||||||
|
use nostr_sdk::prelude::*;
|
||||||
|
use smallvec::{smallvec, SmallVec};
|
||||||
|
use state::{NostrRegistry, FIND_DELAY};
|
||||||
|
use theme::{ActiveTheme, TITLEBAR_HEIGHT};
|
||||||
|
use ui::button::{Button, ButtonVariants};
|
||||||
|
use ui::input::{InputEvent, InputState, TextInput};
|
||||||
|
use ui::{v_flex, window_paddings, IconName, Sizable, WindowExtension};
|
||||||
|
|
||||||
|
/// Command bar for searching conversations.
|
||||||
|
pub struct CommandBar {
|
||||||
|
/// Find input state
|
||||||
|
find_input: Entity<InputState>,
|
||||||
|
|
||||||
|
/// Debounced delay for find input
|
||||||
|
find_debouncer: DebouncedDelay<Self>,
|
||||||
|
|
||||||
|
/// Whether a search is in progress
|
||||||
|
finding: bool,
|
||||||
|
|
||||||
|
/// Find results
|
||||||
|
find_results: Entity<Option<Vec<Entity<Room>>>>,
|
||||||
|
|
||||||
|
/// Async find operation
|
||||||
|
find_task: Option<Task<Result<(), Error>>>,
|
||||||
|
|
||||||
|
/// Event subscriptions
|
||||||
|
_subscriptions: SmallVec<[Subscription; 1]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommandBar {
|
||||||
|
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
|
let find_results = cx.new(|_| None);
|
||||||
|
let find_input = cx.new(|cx| {
|
||||||
|
InputState::new(window, cx)
|
||||||
|
.placeholder("Find or start a conversation")
|
||||||
|
.clean_on_escape()
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut subscriptions = smallvec![];
|
||||||
|
|
||||||
|
subscriptions.push(
|
||||||
|
// Subscribe to find input events
|
||||||
|
cx.subscribe_in(&find_input, window, |this, state, event, window, cx| {
|
||||||
|
let delay = Duration::from_millis(FIND_DELAY);
|
||||||
|
|
||||||
|
match event {
|
||||||
|
InputEvent::PressEnter { .. } => {
|
||||||
|
this.search(window, cx);
|
||||||
|
}
|
||||||
|
InputEvent::Change => {
|
||||||
|
if state.read(cx).value().is_empty() {
|
||||||
|
// Clear results when input is empty
|
||||||
|
this.reset(window, cx);
|
||||||
|
} else {
|
||||||
|
// Run debounced search
|
||||||
|
this.find_debouncer
|
||||||
|
.fire_new(delay, window, cx, |this, window, cx| {
|
||||||
|
this.debounced_search(window, cx)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
find_debouncer: DebouncedDelay::new(),
|
||||||
|
finding: false,
|
||||||
|
find_input,
|
||||||
|
find_results,
|
||||||
|
find_task: None,
|
||||||
|
_subscriptions: subscriptions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn debounced_search(&self, window: &mut Window, cx: &mut Context<Self>) -> Task<()> {
|
||||||
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
this.update_in(cx, |this, window, cx| {
|
||||||
|
this.search(window, cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn search(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let chat = ChatRegistry::global(cx);
|
||||||
|
let query = self.find_input.read(cx).value();
|
||||||
|
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let identity = nostr.read(cx).identity().read(cx).public_key();
|
||||||
|
|
||||||
|
// Return if the query is empty
|
||||||
|
if query.is_empty() {
|
||||||
|
log::warn!("Empty query");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return if a search is already in progress
|
||||||
|
if self.finding {
|
||||||
|
if self.find_task.is_none() {
|
||||||
|
window.push_notification("There is another search in progress", cx);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// Cancel the ongoing search request
|
||||||
|
self.find_task = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block the input until the search completes
|
||||||
|
self.set_finding(true, window, cx);
|
||||||
|
|
||||||
|
// Perform a local search
|
||||||
|
let find_rooms = chat.read(cx).find(&query, cx);
|
||||||
|
// Perform a global search
|
||||||
|
let find_users = nostr.read(cx).search(&query, cx);
|
||||||
|
|
||||||
|
// Run task in the main thread
|
||||||
|
self.find_task = Some(cx.spawn_in(window, async move |this, cx| {
|
||||||
|
let local_rooms = find_rooms.await.ok().filter(|rooms| !rooms.is_empty());
|
||||||
|
let rooms = match local_rooms {
|
||||||
|
Some(rooms) => rooms,
|
||||||
|
None => find_users
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(|event| cx.new(|_| Room::new(None, identity, vec![event.pubkey])))
|
||||||
|
.collect(),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.update_in(cx, |this, window, cx| {
|
||||||
|
this.set_results(rooms, cx);
|
||||||
|
this.set_finding(false, window, cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_results(&mut self, rooms: Vec<Entity<Room>>, cx: &mut Context<Self>) {
|
||||||
|
self.find_results.update(cx, |this, cx| {
|
||||||
|
*this = Some(rooms);
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_finding(&mut self, status: bool, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
// Disable the input to prevent duplicate requests
|
||||||
|
self.find_input.update(cx, |this, cx| {
|
||||||
|
this.set_disabled(status, cx);
|
||||||
|
this.set_loading(status, cx);
|
||||||
|
});
|
||||||
|
// Set the search status
|
||||||
|
self.finding = status;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
// Clear all search results
|
||||||
|
self.find_results.update(cx, |this, cx| {
|
||||||
|
*this = None;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset the search status
|
||||||
|
self.set_finding(false, window, cx);
|
||||||
|
|
||||||
|
// Cancel the current search task
|
||||||
|
self.find_task = None;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_list_items(&self, range: Range<usize>, cx: &Context<Self>) -> Vec<impl IntoElement> {
|
||||||
|
let Some(rooms) = self.find_results.read(cx) else {
|
||||||
|
return vec![div().into_any_element()];
|
||||||
|
};
|
||||||
|
|
||||||
|
rooms
|
||||||
|
.get(range.clone())
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(ix, item)| {
|
||||||
|
let room = item.read(cx);
|
||||||
|
|
||||||
|
div()
|
||||||
|
.id(range.start + ix)
|
||||||
|
.child(room.display_name(cx))
|
||||||
|
.into_any_element()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for CommandBar {
|
||||||
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let window_paddings = window_paddings(window, cx);
|
||||||
|
let view_size = window.viewport_size()
|
||||||
|
- gpui::size(
|
||||||
|
window_paddings.left + window_paddings.right,
|
||||||
|
window_paddings.top + window_paddings.bottom,
|
||||||
|
);
|
||||||
|
|
||||||
|
let bounds = Bounds {
|
||||||
|
origin: Point::default(),
|
||||||
|
size: view_size,
|
||||||
|
};
|
||||||
|
|
||||||
|
let x = bounds.center().x - px(320.) / 2.;
|
||||||
|
let y = TITLEBAR_HEIGHT;
|
||||||
|
|
||||||
|
let input_focus_handle = self.find_input.read(cx).focus_handle(cx);
|
||||||
|
let input_focused = input_focus_handle.is_focused(window);
|
||||||
|
|
||||||
|
let results = self.find_results.read(cx).as_ref();
|
||||||
|
|
||||||
|
div()
|
||||||
|
.w_full()
|
||||||
|
.child(
|
||||||
|
TextInput::new(&self.find_input)
|
||||||
|
.cleanable()
|
||||||
|
.appearance(true)
|
||||||
|
.bordered(false)
|
||||||
|
.xsmall()
|
||||||
|
.text_xs()
|
||||||
|
.when(!self.find_input.read(cx).loading, |this| {
|
||||||
|
this.suffix(
|
||||||
|
Button::new("find")
|
||||||
|
.icon(IconName::Search)
|
||||||
|
.tooltip("Press Enter to search")
|
||||||
|
.transparent()
|
||||||
|
.small(),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.when(input_focused, |this| {
|
||||||
|
this.child(deferred(
|
||||||
|
anchored()
|
||||||
|
.position(point(window_paddings.left, window_paddings.top))
|
||||||
|
.snap_to_window()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.occlude()
|
||||||
|
.w(view_size.width)
|
||||||
|
.h(view_size.height)
|
||||||
|
.on_mouse_down(MouseButton::Left, move |_ev, window, cx| {
|
||||||
|
window.focus_prev(cx);
|
||||||
|
})
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.absolute()
|
||||||
|
.occlude()
|
||||||
|
.relative()
|
||||||
|
.left(x)
|
||||||
|
.top(y)
|
||||||
|
.w(px(320.))
|
||||||
|
.min_h_24()
|
||||||
|
.p_1()
|
||||||
|
.gap_1()
|
||||||
|
.justify_between()
|
||||||
|
.border_1()
|
||||||
|
.border_color(cx.theme().border.alpha(0.4))
|
||||||
|
.bg(cx.theme().surface_background)
|
||||||
|
.shadow_md()
|
||||||
|
.rounded(cx.theme().radius_lg)
|
||||||
|
.when_some(results, |this, results| {
|
||||||
|
this.child(
|
||||||
|
uniform_list(
|
||||||
|
"rooms",
|
||||||
|
results.len(),
|
||||||
|
cx.processor(|this, range, _window, cx| {
|
||||||
|
this.render_list_items(range, cx)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.h_32(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.pt_1()
|
||||||
|
.border_t_1()
|
||||||
|
.border_color(cx.theme().border_variant)
|
||||||
|
.child(
|
||||||
|
Button::new("directory")
|
||||||
|
.icon(IconName::Door)
|
||||||
|
.label("Nostr Directory")
|
||||||
|
.ghost()
|
||||||
|
.xsmall()
|
||||||
|
.no_center(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use anyhow::{anyhow, Error};
|
use anyhow::{anyhow, Error};
|
||||||
use chat::{ChatRegistry, Room};
|
use chat::{ChatRegistry, Room};
|
||||||
use common::{nip05_profile, TextUtils, BOOTSTRAP_RELAYS};
|
use common::{TextUtils, BOOTSTRAP_RELAYS};
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, relative, rems, uniform_list, App, AppContext, Context, Entity, InteractiveElement,
|
div, px, relative, rems, uniform_list, App, AppContext, Context, Entity, InteractiveElement,
|
||||||
@@ -14,7 +14,7 @@ use gpui_tokio::Tokio;
|
|||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use person::PersonRegistry;
|
use person::PersonRegistry;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use state::NostrRegistry;
|
use state::{NostrAddress, NostrRegistry};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::avatar::Avatar;
|
use ui::avatar::Avatar;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
@@ -250,6 +250,7 @@ impl Compose {
|
|||||||
|
|
||||||
fn add_and_select_contact(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn add_and_select_contact(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let content = self.user_input.read(cx).value().to_string();
|
let content = self.user_input.read(cx).value().to_string();
|
||||||
|
let http_client = cx.http_client();
|
||||||
|
|
||||||
// Show loading indicator in the input
|
// Show loading indicator in the input
|
||||||
self.user_input.update(cx, |this, cx| {
|
self.user_input.update(cx, |this, cx| {
|
||||||
@@ -259,9 +260,9 @@ impl Compose {
|
|||||||
if let Ok(public_key) = content.to_public_key() {
|
if let Ok(public_key) = content.to_public_key() {
|
||||||
let contact = Contact::new(public_key).selected();
|
let contact = Contact::new(public_key).selected();
|
||||||
self.push_contact(contact, window, cx);
|
self.push_contact(contact, window, cx);
|
||||||
} else if content.contains("@") {
|
} else if let Ok(addr) = Nip05Address::parse(&content) {
|
||||||
let task = Tokio::spawn(cx, async move {
|
let task = Tokio::spawn(cx, async move {
|
||||||
if let Ok(profile) = nip05_profile(&content).await {
|
if let Ok(profile) = addr.profile(&http_client).await {
|
||||||
let public_key = profile.public_key;
|
let public_key = profile.public_key;
|
||||||
let contact = Contact::new(public_key).selected();
|
let contact = Contact::new(public_key).selected();
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use common::{nip05_verify, shorten_pubkey};
|
use common::shorten_pubkey;
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, relative, rems, App, AppContext, ClipboardItem, Context, Entity, IntoElement,
|
div, relative, rems, App, AppContext, ClipboardItem, Context, Entity, IntoElement,
|
||||||
ParentElement, Render, SharedString, Styled, Task, Window,
|
ParentElement, Render, SharedString, Styled, Task, Window,
|
||||||
};
|
};
|
||||||
use gpui_tokio::Tokio;
|
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use person::PersonRegistry;
|
use person::PersonRegistry;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use state::NostrRegistry;
|
use state::{NostrAddress, NostrRegistry};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::avatar::Avatar;
|
use ui::avatar::Avatar;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
@@ -40,6 +39,7 @@ pub struct ProfileDialog {
|
|||||||
|
|
||||||
impl ProfileDialog {
|
impl ProfileDialog {
|
||||||
pub fn new(public_key: PublicKey, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
pub fn new(public_key: PublicKey, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
|
let http_client = cx.http_client();
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
|
|
||||||
@@ -48,6 +48,7 @@ impl ProfileDialog {
|
|||||||
|
|
||||||
let mut tasks = smallvec![];
|
let mut tasks = smallvec![];
|
||||||
|
|
||||||
|
// Check if the user is following
|
||||||
let check_follow: Task<Result<bool, Error>> = cx.background_spawn(async move {
|
let check_follow: Task<Result<bool, Error>> = cx.background_spawn(async move {
|
||||||
let signer = client.signer().await?;
|
let signer = client.signer().await?;
|
||||||
let public_key = signer.get_public_key().await?;
|
let public_key = signer.get_public_key().await?;
|
||||||
@@ -56,13 +57,12 @@ impl ProfileDialog {
|
|||||||
Ok(contact_list.contains(&public_key))
|
Ok(contact_list.contains(&public_key))
|
||||||
});
|
});
|
||||||
|
|
||||||
let verify_nip05 = if let Some(address) = profile.metadata().nip05 {
|
// Verify the NIP05 address if available
|
||||||
Some(Tokio::spawn(cx, async move {
|
let verify_nip05 = profile.metadata().nip05.and_then(|address| {
|
||||||
nip05_verify(public_key, &address).await.unwrap_or(false)
|
Nip05Address::parse(&address).ok().map(|addr| {
|
||||||
}))
|
cx.background_spawn(async move { addr.verify(&http_client, &public_key).await })
|
||||||
} else {
|
})
|
||||||
None
|
});
|
||||||
};
|
|
||||||
|
|
||||||
tasks.push(
|
tasks.push(
|
||||||
// Load user profile data
|
// Load user profile data
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use common::{nip05_verify, shorten_pubkey, RenderedProfile, RenderedTimestamp, BOOTSTRAP_RELAYS};
|
use common::{shorten_pubkey, RenderedProfile, RenderedTimestamp, BOOTSTRAP_RELAYS};
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, relative, rems, uniform_list, App, AppContext, Context, Div, Entity,
|
div, px, relative, rems, uniform_list, App, AppContext, Context, Div, Entity,
|
||||||
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Task, Window,
|
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Task, Window,
|
||||||
};
|
};
|
||||||
use gpui_tokio::Tokio;
|
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use person::{Person, PersonRegistry};
|
use person::{Person, PersonRegistry};
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use state::NostrRegistry;
|
use state::{NostrAddress, NostrRegistry};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::avatar::Avatar;
|
use ui::avatar::Avatar;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
@@ -33,6 +32,7 @@ pub struct Screening {
|
|||||||
|
|
||||||
impl Screening {
|
impl Screening {
|
||||||
pub fn new(public_key: PublicKey, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
pub fn new(public_key: PublicKey, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
|
let http_client = cx.http_client();
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
|
|
||||||
@@ -41,6 +41,7 @@ impl Screening {
|
|||||||
|
|
||||||
let mut tasks = smallvec![];
|
let mut tasks = smallvec![];
|
||||||
|
|
||||||
|
// Check WOT
|
||||||
let contact_check: Task<Result<(bool, Vec<Profile>), Error>> = cx.background_spawn({
|
let contact_check: Task<Result<(bool, Vec<Profile>), Error>> = cx.background_spawn({
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
async move {
|
async move {
|
||||||
@@ -68,6 +69,7 @@ impl Screening {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Check the last activity
|
||||||
let activity_check = cx.background_spawn(async move {
|
let activity_check = cx.background_spawn(async move {
|
||||||
let filter = Filter::new().author(public_key).limit(1);
|
let filter = Filter::new().author(public_key).limit(1);
|
||||||
let mut activity: Option<Timestamp> = None;
|
let mut activity: Option<Timestamp> = None;
|
||||||
@@ -86,13 +88,12 @@ impl Screening {
|
|||||||
activity
|
activity
|
||||||
});
|
});
|
||||||
|
|
||||||
let addr_check = if let Some(address) = profile.metadata().nip05 {
|
// Verify the NIP05 address if available
|
||||||
Some(Tokio::spawn(cx, async move {
|
let addr_check = profile.metadata().nip05.and_then(|address| {
|
||||||
nip05_verify(public_key, &address).await.unwrap_or(false)
|
Nip05Address::parse(&address).ok().map(|addr| {
|
||||||
}))
|
cx.background_spawn(async move { addr.verify(&http_client, &public_key).await })
|
||||||
} else {
|
})
|
||||||
None
|
});
|
||||||
};
|
|
||||||
|
|
||||||
tasks.push(
|
tasks.push(
|
||||||
// Run the contact check in the background
|
// Run the contact check in the background
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use ui::Root;
|
|||||||
use crate::actions::Quit;
|
use crate::actions::Quit;
|
||||||
|
|
||||||
mod actions;
|
mod actions;
|
||||||
|
mod command_bar;
|
||||||
mod dialogs;
|
mod dialogs;
|
||||||
mod panels;
|
mod panels;
|
||||||
mod sidebar;
|
mod sidebar;
|
||||||
@@ -73,9 +74,6 @@ fn main() {
|
|||||||
cx.activate(true);
|
cx.activate(true);
|
||||||
|
|
||||||
cx.new(|cx| {
|
cx.new(|cx| {
|
||||||
// Initialize the tokio runtime
|
|
||||||
gpui_tokio::init(cx);
|
|
||||||
|
|
||||||
// Initialize components
|
// Initialize components
|
||||||
ui::init(cx);
|
ui::init(cx);
|
||||||
|
|
||||||
|
|||||||
@@ -67,281 +67,6 @@ impl Sidebar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
async fn nip50(client: &Client, query: &str) -> Result<Vec<Event>, Error> {
|
|
||||||
let signer = client.signer().await?;
|
|
||||||
let public_key = signer.get_public_key().await?;
|
|
||||||
|
|
||||||
let filter = Filter::new()
|
|
||||||
.kind(Kind::Metadata)
|
|
||||||
.search(query.to_lowercase())
|
|
||||||
.limit(FIND_LIMIT);
|
|
||||||
|
|
||||||
let mut stream = client
|
|
||||||
.stream_events_from(SEARCH_RELAYS, filter, Duration::from_secs(3))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let mut results: Vec<Event> = Vec::with_capacity(FIND_LIMIT);
|
|
||||||
|
|
||||||
while let Some((_url, event)) = stream.next().await {
|
|
||||||
if let Ok(event) = event {
|
|
||||||
// Skip if author is match current user
|
|
||||||
if event.pubkey == public_key {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip if the event has already been added
|
|
||||||
if results.iter().any(|this| this.pubkey == event.pubkey) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
results.push(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if results.is_empty() {
|
|
||||||
return Err(anyhow!("No results for query {query}"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all public keys
|
|
||||||
let public_keys: Vec<PublicKey> = results.iter().map(|event| event.pubkey).collect();
|
|
||||||
|
|
||||||
// Fetch metadata and contact lists if public keys is not empty
|
|
||||||
if !public_keys.is_empty() {
|
|
||||||
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
|
||||||
let filter = Filter::new()
|
|
||||||
.kinds(vec![Kind::Metadata, Kind::ContactList])
|
|
||||||
.limit(public_keys.len() * 2)
|
|
||||||
.authors(public_keys);
|
|
||||||
|
|
||||||
client
|
|
||||||
.subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts))
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(results)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn debounced_search(&self, window: &mut Window, cx: &mut Context<Self>) -> Task<()> {
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
|
||||||
this.search(window, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn search_by_nip50(&mut self, query: &str, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
|
||||||
let client = nostr.read(cx).client();
|
|
||||||
let public_key = nostr.read(cx).identity().read(cx).public_key();
|
|
||||||
|
|
||||||
let query = query.to_owned();
|
|
||||||
|
|
||||||
self.search_task = Some(cx.spawn_in(window, async move |this, cx| {
|
|
||||||
let result = Self::nip50(&client, &query).await;
|
|
||||||
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
|
||||||
match result {
|
|
||||||
Ok(results) => {
|
|
||||||
let rooms = results
|
|
||||||
.into_iter()
|
|
||||||
.map(|event| {
|
|
||||||
cx.new(|_| Room::new(None, public_key, vec![event.pubkey]))
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
this.set_results(rooms, cx);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
window.push_notification(e.to_string(), cx);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.set_finding(false, window, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn search_by_nip05(&mut self, query: &str, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
|
||||||
let client = nostr.read(cx).client();
|
|
||||||
let address = query.to_owned();
|
|
||||||
|
|
||||||
let task = Tokio::spawn(cx, async move {
|
|
||||||
match common::nip05_profile(&address).await {
|
|
||||||
Ok(profile) => {
|
|
||||||
let signer = client.signer().await?;
|
|
||||||
let public_key = signer.get_public_key().await?;
|
|
||||||
let receivers = vec![profile.public_key];
|
|
||||||
let room = Room::new(None, public_key, receivers);
|
|
||||||
|
|
||||||
Ok(room)
|
|
||||||
}
|
|
||||||
Err(e) => Err(anyhow!(e)),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
self.search_task = Some(cx.spawn_in(window, async move |this, cx| {
|
|
||||||
let result = task.await;
|
|
||||||
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
|
||||||
match result {
|
|
||||||
Ok(Ok(room)) => {
|
|
||||||
this.set_results(vec![cx.new(|_| room)], cx);
|
|
||||||
}
|
|
||||||
Ok(Err(e)) => {
|
|
||||||
window.push_notification(e.to_string(), cx);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
window.push_notification(e.to_string(), cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.set_finding(false, window, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn search_by_pubkey(&mut self, query: &str, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
|
||||||
let client = nostr.read(cx).client();
|
|
||||||
|
|
||||||
let Ok(public_key) = query.to_public_key() else {
|
|
||||||
window.push_notification("Public Key is invalid", cx);
|
|
||||||
self.set_finding(false, window, cx);
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let task: Task<Result<Room, Error>> = cx.background_spawn(async move {
|
|
||||||
let signer = client.signer().await?;
|
|
||||||
let author = signer.get_public_key().await?;
|
|
||||||
|
|
||||||
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
|
||||||
let receivers = vec![public_key];
|
|
||||||
let room = Room::new(None, author, receivers);
|
|
||||||
|
|
||||||
let filter = Filter::new()
|
|
||||||
.kinds(vec![Kind::Metadata, Kind::ContactList])
|
|
||||||
.author(public_key)
|
|
||||||
.limit(2);
|
|
||||||
|
|
||||||
client
|
|
||||||
.subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(room)
|
|
||||||
});
|
|
||||||
|
|
||||||
self.search_task = Some(cx.spawn_in(window, async move |this, cx| {
|
|
||||||
let result = task.await;
|
|
||||||
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
|
||||||
match result {
|
|
||||||
Ok(room) => {
|
|
||||||
let chat = ChatRegistry::global(cx);
|
|
||||||
let local_results = chat.read(cx).search_by_public_key(public_key, cx);
|
|
||||||
|
|
||||||
if !local_results.is_empty() {
|
|
||||||
this.set_results(local_results, cx);
|
|
||||||
} else {
|
|
||||||
this.set_results(vec![cx.new(|_| room)], cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
window.push_notification(e.to_string(), cx);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.set_finding(false, window, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn search(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
// Return if the query is empty
|
|
||||||
if self.find_input.read(cx).value().is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return if search is in progress
|
|
||||||
if self.finding {
|
|
||||||
if self.search_task.is_none() {
|
|
||||||
window.push_notification("There is another search in progress", cx);
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
// Cancel ongoing search request
|
|
||||||
self.search_task = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let input = self.find_input.read(cx).value();
|
|
||||||
let query = input.to_string();
|
|
||||||
|
|
||||||
// Block the input until the search process completes
|
|
||||||
self.set_finding(true, window, cx);
|
|
||||||
|
|
||||||
// Process to search by pubkey if query starts with npub or nprofile
|
|
||||||
if query.starts_with("npub1") || query.starts_with("nprofile1") {
|
|
||||||
self.search_by_pubkey(&query, window, cx);
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Process to search by NIP05 if query is a valid NIP-05 identifier (name@domain.tld)
|
|
||||||
if query.split('@').count() == 2 {
|
|
||||||
let parts: Vec<&str> = query.split('@').collect();
|
|
||||||
if !parts[0].is_empty() && !parts[1].is_empty() && parts[1].contains('.') {
|
|
||||||
self.search_by_nip05(&query, window, cx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all local results with current query
|
|
||||||
let chat = ChatRegistry::global(cx);
|
|
||||||
let local_results = chat.read(cx).search(&query, cx);
|
|
||||||
|
|
||||||
// Try to update with local results first
|
|
||||||
if !local_results.is_empty() {
|
|
||||||
self.set_results(local_results, cx);
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
// If no local results, try global search via NIP-50
|
|
||||||
self.search_by_nip50(&query, window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_results(&mut self, rooms: Vec<Entity<Room>>, cx: &mut Context<Self>) {
|
|
||||||
self.search_results.update(cx, |this, cx| {
|
|
||||||
*this = Some(rooms);
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_finding(&mut self, status: bool, _window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
// Disable the input to prevent duplicate requests
|
|
||||||
self.find_input.update(cx, |this, cx| {
|
|
||||||
this.set_disabled(status, cx);
|
|
||||||
this.set_loading(status, cx);
|
|
||||||
});
|
|
||||||
// Set the finding status
|
|
||||||
self.finding = status;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn clear(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
// Reset the input state
|
|
||||||
if self.finding {
|
|
||||||
self.set_finding(false, window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear all local results
|
|
||||||
self.search_results.update(cx, |this, cx| {
|
|
||||||
*this = None;
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
/// Get the active filter.
|
/// Get the active filter.
|
||||||
fn current_filter(&self, kind: &RoomKind, cx: &Context<Self>) -> bool {
|
fn current_filter(&self, kind: &RoomKind, cx: &Context<Self>) -> bool {
|
||||||
self.filter.read(cx) == kind
|
self.filter.read(cx) == kind
|
||||||
@@ -356,23 +81,7 @@ impl Sidebar {
|
|||||||
self.new_requests = false;
|
self.new_requests = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Open a room.
|
fn render_list_items(&self, range: Range<usize>, cx: &Context<Self>) -> Vec<impl IntoElement> {
|
||||||
fn open(&mut self, id: u64, _window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let chat = ChatRegistry::global(cx);
|
|
||||||
|
|
||||||
if let Some(room) = chat.read(cx).room(&id, cx) {
|
|
||||||
chat.update(cx, |this, cx| {
|
|
||||||
this.emit_room(room, cx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_list_items(
|
|
||||||
&self,
|
|
||||||
range: Range<usize>,
|
|
||||||
_window: &Window,
|
|
||||||
cx: &Context<Self>,
|
|
||||||
) -> Vec<impl IntoElement> {
|
|
||||||
let chat = ChatRegistry::global(cx);
|
let chat = ChatRegistry::global(cx);
|
||||||
let rooms = chat.read(cx).rooms(self.filter.read(cx), cx);
|
let rooms = chat.read(cx).rooms(self.filter.read(cx), cx);
|
||||||
|
|
||||||
@@ -383,13 +92,12 @@ impl Sidebar {
|
|||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(ix, item)| {
|
.map(|(ix, item)| {
|
||||||
let room = item.read(cx);
|
let room = item.read(cx);
|
||||||
|
let weak_room = item.downgrade();
|
||||||
let public_key = room.display_member(cx).public_key();
|
let public_key = room.display_member(cx).public_key();
|
||||||
let id = room.id;
|
let handler = cx.listener(move |_this, _ev, _window, cx| {
|
||||||
|
ChatRegistry::global(cx).update(cx, |s, cx| {
|
||||||
let handler = cx.listener({
|
s.emit_room(weak_room.clone(), cx);
|
||||||
move |this, _ev, window, cx| {
|
});
|
||||||
this.open(id, window, cx);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
RoomListItem::new(range.start + ix)
|
RoomListItem::new(range.start + ix)
|
||||||
@@ -544,8 +252,8 @@ impl Render for Sidebar {
|
|||||||
uniform_list(
|
uniform_list(
|
||||||
"rooms",
|
"rooms",
|
||||||
total_rooms,
|
total_rooms,
|
||||||
cx.processor(|this, range, window, cx| {
|
cx.processor(|this, range, _window, cx| {
|
||||||
this.render_list_items(range, window, cx)
|
this.render_list_items(range, cx)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.h_full(),
|
.h_full(),
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use chat::{ChatEvent, ChatRegistry, Room};
|
use chat::{ChatEvent, ChatRegistry};
|
||||||
use common::DebouncedDelay;
|
|
||||||
use dock::dock::DockPlacement;
|
use dock::dock::DockPlacement;
|
||||||
use dock::panel::PanelView;
|
use dock::panel::PanelView;
|
||||||
use dock::{ClosePanel, DockArea, DockItem};
|
use dock::{ClosePanel, DockArea, DockItem};
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, rems, App, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement,
|
div, rems, App, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement,
|
||||||
ParentElement, Render, SharedString, Styled, Subscription, Task, Window,
|
ParentElement, Render, SharedString, Styled, Subscription, Window,
|
||||||
};
|
};
|
||||||
use person::PersonRegistry;
|
use person::PersonRegistry;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
@@ -17,16 +15,12 @@ use state::NostrRegistry;
|
|||||||
use theme::{ActiveTheme, Theme, SIDEBAR_WIDTH, TITLEBAR_HEIGHT};
|
use theme::{ActiveTheme, Theme, SIDEBAR_WIDTH, TITLEBAR_HEIGHT};
|
||||||
use titlebar::TitleBar;
|
use titlebar::TitleBar;
|
||||||
use ui::avatar::Avatar;
|
use ui::avatar::Avatar;
|
||||||
use ui::button::{Button, ButtonVariants};
|
|
||||||
use ui::input::{InputEvent, InputState, TextInput};
|
|
||||||
use ui::{h_flex, v_flex, Icon, IconName, Root, Sizable, WindowExtension};
|
use ui::{h_flex, v_flex, Icon, IconName, Root, Sizable, WindowExtension};
|
||||||
|
|
||||||
|
use crate::command_bar::CommandBar;
|
||||||
use crate::panels::greeter;
|
use crate::panels::greeter;
|
||||||
use crate::sidebar;
|
use crate::sidebar;
|
||||||
|
|
||||||
const FIND_DELAY: u64 = 600;
|
|
||||||
const FIND_LIMIT: usize = 20;
|
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Workspace> {
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Workspace> {
|
||||||
cx.new(|cx| Workspace::new(window, cx))
|
cx.new(|cx| Workspace::new(window, cx))
|
||||||
}
|
}
|
||||||
@@ -38,46 +32,21 @@ pub struct Workspace {
|
|||||||
/// App's Dock Area
|
/// App's Dock Area
|
||||||
dock: Entity<DockArea>,
|
dock: Entity<DockArea>,
|
||||||
|
|
||||||
/// Search results
|
/// App's Command Bar
|
||||||
search_results: Entity<Option<Vec<Entity<Room>>>>,
|
command_bar: Entity<CommandBar>,
|
||||||
|
|
||||||
/// Async search operation
|
|
||||||
search_task: Option<Task<()>>,
|
|
||||||
|
|
||||||
/// Search input state
|
|
||||||
find_input: Entity<InputState>,
|
|
||||||
|
|
||||||
/// Debounced delay for search input
|
|
||||||
find_debouncer: DebouncedDelay<Self>,
|
|
||||||
|
|
||||||
/// Whether searching is in progress
|
|
||||||
finding: bool,
|
|
||||||
|
|
||||||
/// Event subscriptions
|
/// Event subscriptions
|
||||||
_subscriptions: SmallVec<[Subscription; 4]>,
|
_subscriptions: SmallVec<[Subscription; 3]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Workspace {
|
impl Workspace {
|
||||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
let chat = ChatRegistry::global(cx);
|
let chat = ChatRegistry::global(cx);
|
||||||
|
|
||||||
// App's titlebar
|
|
||||||
let titlebar = cx.new(|_| TitleBar::new());
|
let titlebar = cx.new(|_| TitleBar::new());
|
||||||
|
let command_bar = cx.new(|cx| CommandBar::new(window, cx));
|
||||||
// App's dock area
|
|
||||||
let dock =
|
let dock =
|
||||||
cx.new(|cx| DockArea::new(window, cx).panel_style(dock::panel::PanelStyle::TabBar));
|
cx.new(|cx| DockArea::new(window, cx).panel_style(dock::panel::PanelStyle::TabBar));
|
||||||
|
|
||||||
// Define the find input state
|
|
||||||
let find_input = cx.new(|cx| {
|
|
||||||
InputState::new(window, cx)
|
|
||||||
.placeholder("Find or start a conversation")
|
|
||||||
.clean_on_escape()
|
|
||||||
});
|
|
||||||
|
|
||||||
// Define the search results states
|
|
||||||
let search_results = cx.new(|_| None);
|
|
||||||
|
|
||||||
let mut subscriptions = smallvec![];
|
let mut subscriptions = smallvec![];
|
||||||
|
|
||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
@@ -131,32 +100,6 @@ impl Workspace {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
subscriptions.push(
|
|
||||||
// Subscribe for find input events
|
|
||||||
cx.subscribe_in(&find_input, window, |this, state, event, window, cx| {
|
|
||||||
let delay = Duration::from_millis(FIND_DELAY);
|
|
||||||
|
|
||||||
match event {
|
|
||||||
InputEvent::PressEnter { .. } => {
|
|
||||||
// this.search(window, cx);
|
|
||||||
}
|
|
||||||
InputEvent::Change => {
|
|
||||||
if state.read(cx).value().is_empty() {
|
|
||||||
// Clear the result when input is empty
|
|
||||||
// this.clear(window, cx);
|
|
||||||
} else {
|
|
||||||
// Run debounced search
|
|
||||||
//this.find_debouncer
|
|
||||||
// .fire_new(delay, window, cx, |this, window, cx| {
|
|
||||||
// this.debounced_search(window, cx)
|
|
||||||
// });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Set the default layout for app's dock
|
// Set the default layout for app's dock
|
||||||
cx.defer_in(window, |this, window, cx| {
|
cx.defer_in(window, |this, window, cx| {
|
||||||
this.set_layout(window, cx);
|
this.set_layout(window, cx);
|
||||||
@@ -165,15 +108,12 @@ impl Workspace {
|
|||||||
Self {
|
Self {
|
||||||
titlebar,
|
titlebar,
|
||||||
dock,
|
dock,
|
||||||
find_debouncer: DebouncedDelay::new(),
|
command_bar,
|
||||||
finding: false,
|
|
||||||
find_input,
|
|
||||||
search_results,
|
|
||||||
search_task: None,
|
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add panel to the dock
|
||||||
pub fn add_panel<P>(panel: P, placement: DockPlacement, window: &mut Window, cx: &mut App)
|
pub fn add_panel<P>(panel: P, placement: DockPlacement, window: &mut Window, cx: &mut App)
|
||||||
where
|
where
|
||||||
P: PanelView,
|
P: PanelView,
|
||||||
@@ -189,6 +129,7 @@ impl Workspace {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get all panel ids
|
||||||
fn panel_ids(&self, cx: &App) -> Option<Vec<u64>> {
|
fn panel_ids(&self, cx: &App) -> Option<Vec<u64>> {
|
||||||
let ids: Vec<u64> = self
|
let ids: Vec<u64> = self
|
||||||
.dock
|
.dock
|
||||||
@@ -202,6 +143,7 @@ impl Workspace {
|
|||||||
Some(ids)
|
Some(ids)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the dock layout
|
||||||
fn set_layout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn set_layout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let weak_dock = self.dock.downgrade();
|
let weak_dock = self.dock.downgrade();
|
||||||
|
|
||||||
@@ -257,24 +199,8 @@ impl Workspace {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn titlebar_center(&mut self, _window: &mut Window, cx: &Context<Self>) -> impl IntoElement {
|
fn titlebar_center(&mut self, _window: &mut Window, _cx: &Context<Self>) -> impl IntoElement {
|
||||||
h_flex().flex_1().child(
|
h_flex().flex_1().w_full().child(self.command_bar.clone())
|
||||||
TextInput::new(&self.find_input)
|
|
||||||
.xsmall()
|
|
||||||
.cleanable()
|
|
||||||
.appearance(true)
|
|
||||||
.bordered(false)
|
|
||||||
.text_xs()
|
|
||||||
.when(!self.find_input.read(cx).loading, |this| {
|
|
||||||
this.prefix(
|
|
||||||
Button::new("find")
|
|
||||||
.icon(IconName::Search)
|
|
||||||
.tooltip("Press Enter to search")
|
|
||||||
.transparent()
|
|
||||||
.small(),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn titlebar_right(&mut self, _window: &mut Window, _cx: &Context<Self>) -> impl IntoElement {
|
fn titlebar_right(&mut self, _window: &mut Window, _cx: &Context<Self>) -> impl IntoElement {
|
||||||
@@ -303,6 +229,7 @@ impl Render for Workspace {
|
|||||||
.size_full()
|
.size_full()
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
|
.relative()
|
||||||
.size_full()
|
.size_full()
|
||||||
// Title Bar
|
// Title Bar
|
||||||
.child(self.titlebar.clone())
|
.child(self.titlebar.clone())
|
||||||
|
|||||||
@@ -190,7 +190,6 @@ impl PersonRegistry {
|
|||||||
.wait_timeout(Duration::from_secs(2))
|
.wait_timeout(Duration::from_secs(2))
|
||||||
{
|
{
|
||||||
Ok(Some(public_key)) => {
|
Ok(Some(public_key)) => {
|
||||||
log::info!("Received public key: {}", public_key);
|
|
||||||
batch.insert(public_key);
|
batch.insert(public_key);
|
||||||
// Process the batch if it's full
|
// Process the batch if it's full
|
||||||
if batch.len() >= 20 {
|
if batch.len() >= 20 {
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ nostr-lmdb.workspace = true
|
|||||||
nostr-connect.workspace = true
|
nostr-connect.workspace = true
|
||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
|
gpui_tokio.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
|
reqwest.workspace = true
|
||||||
flume.workspace = true
|
flume.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
|||||||
@@ -13,20 +13,22 @@ mod device;
|
|||||||
mod event;
|
mod event;
|
||||||
mod gossip;
|
mod gossip;
|
||||||
mod identity;
|
mod identity;
|
||||||
|
mod nip05;
|
||||||
|
|
||||||
pub use device::*;
|
pub use device::*;
|
||||||
pub use event::*;
|
pub use event::*;
|
||||||
pub use gossip::*;
|
pub use gossip::*;
|
||||||
pub use identity::*;
|
pub use identity::*;
|
||||||
|
pub use nip05::*;
|
||||||
|
|
||||||
use crate::identity::Identity;
|
use crate::identity::Identity;
|
||||||
|
|
||||||
pub fn init(cx: &mut App) {
|
|
||||||
NostrRegistry::set_global(cx.new(NostrRegistry::new), cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Default timeout for subscription
|
/// Default timeout for subscription
|
||||||
pub const TIMEOUT: u64 = 3;
|
pub const TIMEOUT: u64 = 3;
|
||||||
|
/// Default delay for searching
|
||||||
|
pub const FIND_DELAY: u64 = 600;
|
||||||
|
/// Default limit for searching
|
||||||
|
pub const FIND_LIMIT: usize = 20;
|
||||||
/// Default timeout for Nostr Connect
|
/// Default timeout for Nostr Connect
|
||||||
pub const NOSTR_CONNECT_TIMEOUT: u64 = 200;
|
pub const NOSTR_CONNECT_TIMEOUT: u64 = 200;
|
||||||
/// Default Nostr Connect relay
|
/// Default Nostr Connect relay
|
||||||
@@ -36,6 +38,13 @@ pub const DEVICE_GIFTWRAP: &str = "device-gift-wraps";
|
|||||||
/// Default subscription id for user gift wrap events
|
/// Default subscription id for user gift wrap events
|
||||||
pub const USER_GIFTWRAP: &str = "user-gift-wraps";
|
pub const USER_GIFTWRAP: &str = "user-gift-wraps";
|
||||||
|
|
||||||
|
pub fn init(cx: &mut App) {
|
||||||
|
// Initialize the tokio runtime
|
||||||
|
gpui_tokio::init(cx);
|
||||||
|
|
||||||
|
NostrRegistry::set_global(cx.new(NostrRegistry::new), cx);
|
||||||
|
}
|
||||||
|
|
||||||
struct GlobalNostrRegistry(Entity<NostrRegistry>);
|
struct GlobalNostrRegistry(Entity<NostrRegistry>);
|
||||||
|
|
||||||
impl Global for GlobalNostrRegistry {}
|
impl Global for GlobalNostrRegistry {}
|
||||||
@@ -796,6 +805,40 @@ impl NostrRegistry {
|
|||||||
|
|
||||||
(signer, uri)
|
(signer, uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Perform a NIP-50 global search for user profiles based on a given query
|
||||||
|
pub fn search(&self, query: &str, cx: &App) -> Task<Result<Vec<Event>, Error>> {
|
||||||
|
let client = self.client();
|
||||||
|
let query = query.to_string();
|
||||||
|
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
let mut results: Vec<Event> = Vec::with_capacity(FIND_LIMIT);
|
||||||
|
|
||||||
|
// Construct the filter for the search query
|
||||||
|
let filter = Filter::new()
|
||||||
|
.search(query.to_lowercase())
|
||||||
|
.kind(Kind::Metadata)
|
||||||
|
.limit(FIND_LIMIT);
|
||||||
|
|
||||||
|
// Stream events from the search relays
|
||||||
|
let mut stream = client
|
||||||
|
.stream_events_from(SEARCH_RELAYS, vec![filter], Duration::from_secs(3))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Collect the results
|
||||||
|
while let Some((_url, res)) = stream.next().await {
|
||||||
|
if let Ok(event) = res {
|
||||||
|
results.push(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if results.is_empty() {
|
||||||
|
return Err(anyhow!("No results for query {query}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(results)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|||||||
60
crates/state/src/nip05.rs
Normal file
60
crates/state/src/nip05.rs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::Error;
|
||||||
|
use gpui::http_client::{AsyncBody, HttpClient};
|
||||||
|
use nostr_sdk::prelude::*;
|
||||||
|
use smol::io::AsyncReadExt;
|
||||||
|
|
||||||
|
#[allow(async_fn_in_trait)]
|
||||||
|
pub trait NostrAddress {
|
||||||
|
/// Get the NIP-05 profile
|
||||||
|
async fn profile(&self, client: &Arc<dyn HttpClient>) -> Result<Nip05Profile, Error>;
|
||||||
|
|
||||||
|
/// Verify the NIP-05 address
|
||||||
|
async fn verify(
|
||||||
|
&self,
|
||||||
|
client: &Arc<dyn HttpClient>,
|
||||||
|
public_key: &PublicKey,
|
||||||
|
) -> Result<bool, Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NostrAddress for Nip05Address {
|
||||||
|
async fn profile(&self, client: &Arc<dyn HttpClient>) -> Result<Nip05Profile, Error> {
|
||||||
|
let mut body = Vec::new();
|
||||||
|
let mut res = client
|
||||||
|
.get(self.url().as_str(), AsyncBody::default(), false)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Read the response body into a vector
|
||||||
|
res.body_mut().read_to_end(&mut body).await?;
|
||||||
|
|
||||||
|
// Parse the JSON response
|
||||||
|
let json: Value = serde_json::from_slice(&body)?;
|
||||||
|
|
||||||
|
let profile = Nip05Profile::from_json(self, &json)?;
|
||||||
|
|
||||||
|
Ok(profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn verify(
|
||||||
|
&self,
|
||||||
|
client: &Arc<dyn HttpClient>,
|
||||||
|
public_key: &PublicKey,
|
||||||
|
) -> Result<bool, Error> {
|
||||||
|
let mut body = Vec::new();
|
||||||
|
let mut res = client
|
||||||
|
.get(self.url().as_str(), AsyncBody::default(), false)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Read the response body into a vector
|
||||||
|
res.body_mut().read_to_end(&mut body).await?;
|
||||||
|
|
||||||
|
// Parse the JSON response
|
||||||
|
let json: Value = serde_json::from_slice(&body)?;
|
||||||
|
|
||||||
|
// Verify the NIP-05 address
|
||||||
|
let verified = nip05::verify_from_json(public_key, self, &json);
|
||||||
|
|
||||||
|
Ok(verified)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ use gpui::MouseButton;
|
|||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
use gpui::Pixels;
|
use gpui::Pixels;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
px, AnyElement, Context, Decorations, Hsla, InteractiveElement as _, IntoElement,
|
div, px, AnyElement, Context, Decorations, Hsla, InteractiveElement as _, IntoElement,
|
||||||
ParentElement, Render, StatefulInteractiveElement as _, Styled, Window, WindowControlArea,
|
ParentElement, Render, StatefulInteractiveElement as _, Styled, Window, WindowControlArea,
|
||||||
};
|
};
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
@@ -129,15 +129,26 @@ impl Render for TitleBar {
|
|||||||
})
|
})
|
||||||
.children(children),
|
.children(children),
|
||||||
)
|
)
|
||||||
.when(!window.is_fullscreen(), |this| match cx.theme().platform {
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.absolute()
|
||||||
|
.top_0()
|
||||||
|
.right_0()
|
||||||
|
.pr_2()
|
||||||
|
.h(height)
|
||||||
|
.child(
|
||||||
|
div().when(!window.is_fullscreen(), |this| match cx.theme().platform {
|
||||||
PlatformKind::Linux => {
|
PlatformKind::Linux => {
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
if matches!(decorations, Decorations::Client { .. }) {
|
if matches!(decorations, Decorations::Client { .. }) {
|
||||||
this.child(LinuxWindowControls::new(None))
|
this.child(LinuxWindowControls::new(None))
|
||||||
.when(supported_controls.window_menu, |this| {
|
.when(supported_controls.window_menu, |this| {
|
||||||
this.on_mouse_down(MouseButton::Right, move |ev, window, _| {
|
this.on_mouse_down(
|
||||||
|
MouseButton::Right,
|
||||||
|
move |ev, window, _| {
|
||||||
window.show_window_menu(ev.position)
|
window.show_window_menu(ev.position)
|
||||||
})
|
},
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.on_mouse_move(cx.listener(move |this, _ev, window, _| {
|
.on_mouse_move(cx.listener(move |this, _ev, window, _| {
|
||||||
if this.should_move {
|
if this.should_move {
|
||||||
@@ -145,9 +156,11 @@ impl Render for TitleBar {
|
|||||||
window.start_window_move();
|
window.start_window_move();
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
.on_mouse_down_out(cx.listener(move |this, _ev, _window, _cx| {
|
.on_mouse_down_out(cx.listener(
|
||||||
|
move |this, _ev, _window, _cx| {
|
||||||
this.should_move = false;
|
this.should_move = false;
|
||||||
}))
|
},
|
||||||
|
))
|
||||||
.on_mouse_up(
|
.on_mouse_up(
|
||||||
MouseButton::Left,
|
MouseButton::Left,
|
||||||
cx.listener(move |this, _ev, _window, _cx| {
|
cx.listener(move |this, _ev, _window, _cx| {
|
||||||
@@ -168,6 +181,8 @@ impl Render for TitleBar {
|
|||||||
}
|
}
|
||||||
PlatformKind::Windows => this.child(WindowsWindowControls::new(height)),
|
PlatformKind::Windows => this.child(WindowsWindowControls::new(height)),
|
||||||
PlatformKind::Mac => this,
|
PlatformKind::Mac => this,
|
||||||
})
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ pub struct Button {
|
|||||||
children: Vec<AnyElement>,
|
children: Vec<AnyElement>,
|
||||||
|
|
||||||
variant: ButtonVariant,
|
variant: ButtonVariant,
|
||||||
|
center: bool,
|
||||||
rounded: bool,
|
rounded: bool,
|
||||||
size: Size,
|
size: Size,
|
||||||
|
|
||||||
@@ -170,6 +171,7 @@ impl Button {
|
|||||||
on_hover: None,
|
on_hover: None,
|
||||||
loading: false,
|
loading: false,
|
||||||
reverse: false,
|
reverse: false,
|
||||||
|
center: true,
|
||||||
bold: false,
|
bold: false,
|
||||||
cta: false,
|
cta: false,
|
||||||
children: Vec::new(),
|
children: Vec::new(),
|
||||||
@@ -221,6 +223,12 @@ impl Button {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Disable centering the button's content.
|
||||||
|
pub fn no_center(mut self) -> Self {
|
||||||
|
self.center = false;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the cta style of the button.
|
/// Set the cta style of the button.
|
||||||
pub fn cta(mut self) -> Self {
|
pub fn cta(mut self) -> Self {
|
||||||
self.cta = true;
|
self.cta = true;
|
||||||
@@ -353,7 +361,7 @@ impl RenderOnce for Button {
|
|||||||
.flex_shrink_0()
|
.flex_shrink_0()
|
||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
.justify_center()
|
.when(self.center, |this| this.justify_center())
|
||||||
.cursor_default()
|
.cursor_default()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.refine_style(&self.style)
|
.refine_style(&self.style)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ pub use focusable::FocusableCycle;
|
|||||||
pub use icon::*;
|
pub use icon::*;
|
||||||
pub use kbd::*;
|
pub use kbd::*;
|
||||||
pub use menu::{context_menu, popup_menu};
|
pub use menu::{context_menu, popup_menu};
|
||||||
pub use root::Root;
|
pub use root::{window_paddings, Root};
|
||||||
pub use styled::*;
|
pub use styled::*;
|
||||||
pub use window_ext::*;
|
pub use window_ext::*;
|
||||||
|
|
||||||
|
|||||||
@@ -381,7 +381,7 @@ impl Render for Root {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get the window paddings.
|
/// Get the window paddings.
|
||||||
pub(crate) fn window_paddings(window: &Window, _cx: &App) -> Edges<Pixels> {
|
pub fn window_paddings(window: &Window, _cx: &App) -> Edges<Pixels> {
|
||||||
match window.window_decorations() {
|
match window.window_decorations() {
|
||||||
Decorations::Server => Edges::all(px(0.0)),
|
Decorations::Server => Edges::all(px(0.0)),
|
||||||
Decorations::Client { tiling } => {
|
Decorations::Client { tiling } => {
|
||||||
|
|||||||
Reference in New Issue
Block a user