chore: improve data requests (#81)
* refactor * refactor * add documents * clean up * refactor * clean up * refactor identity * . * . * rename
This commit is contained in:
@@ -14,7 +14,7 @@ identity = { path = "../identity" }
|
||||
theme = { path = "../theme" }
|
||||
common = { path = "../common" }
|
||||
global = { path = "../global" }
|
||||
chats = { path = "../chats" }
|
||||
registry = { path = "../registry" }
|
||||
settings = { path = "../settings" }
|
||||
client_keys = { path = "../client_keys" }
|
||||
auto_update = { path = "../auto_update" }
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Error;
|
||||
use chats::{ChatRegistry, RoomEmitter};
|
||||
use client_keys::ClientKeys;
|
||||
use global::constants::{DEFAULT_MODAL_WIDTH, DEFAULT_SIDEBAR_WIDTH};
|
||||
use global::shared_state;
|
||||
use global::nostr_client;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, px, relative, Action, App, AppContext, Axis, Context, Entity, IntoElement, ParentElement,
|
||||
@@ -13,6 +12,7 @@ use gpui::{
|
||||
use i18n::t;
|
||||
use identity::Identity;
|
||||
use nostr_connect::prelude::*;
|
||||
use registry::{Registry, RoomEmitter};
|
||||
use serde::Deserialize;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use theme::{ActiveTheme, Theme, ThemeMode};
|
||||
@@ -84,7 +84,7 @@ impl ChatSpace {
|
||||
});
|
||||
|
||||
cx.new(|cx| {
|
||||
let chats = ChatRegistry::global(cx);
|
||||
let registry = Registry::global(cx);
|
||||
let client_keys = ClientKeys::global(cx);
|
||||
let identity = Identity::global(cx);
|
||||
let mut subscriptions = smallvec![];
|
||||
@@ -153,11 +153,11 @@ impl ChatSpace {
|
||||
&identity,
|
||||
window,
|
||||
|this: &mut Self, state, window, cx| {
|
||||
if !state.read(cx).has_profile() {
|
||||
if !state.read(cx).has_signer() {
|
||||
this.open_onboarding(window, cx);
|
||||
} else {
|
||||
// Load all chat rooms from database
|
||||
ChatRegistry::global(cx).update(cx, |this, cx| {
|
||||
Registry::global(cx).update(cx, |this, cx| {
|
||||
this.load_rooms(window, cx);
|
||||
});
|
||||
// Open chat panels
|
||||
@@ -175,7 +175,7 @@ impl ChatSpace {
|
||||
|
||||
// Subscribe to open chat room requests
|
||||
subscriptions.push(cx.subscribe_in(
|
||||
&chats,
|
||||
®istry,
|
||||
window,
|
||||
|this: &mut Self, _state, event, window, cx| {
|
||||
if let RoomEmitter::Open(room) = event {
|
||||
@@ -187,10 +187,7 @@ impl ChatSpace {
|
||||
this.add_panel(panel, placement, window, cx);
|
||||
});
|
||||
} else {
|
||||
window.push_notification(
|
||||
SharedString::new(t!("chatspace.failed_to_open_room")),
|
||||
cx,
|
||||
);
|
||||
window.push_notification(t!("chatspace.failed_to_open_room"), cx);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -283,7 +280,7 @@ impl ChatSpace {
|
||||
|
||||
fn verify_messaging_relays(&self, cx: &App) -> Task<Result<bool, Error>> {
|
||||
cx.background_spawn(async move {
|
||||
let client = shared_state().client();
|
||||
let client = nostr_client();
|
||||
let signer = client.signer().await?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
let filter = Filter::new()
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Error};
|
||||
use asset::Assets;
|
||||
use auto_update::AutoUpdater;
|
||||
use chats::ChatRegistry;
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
use global::constants::APP_NAME;
|
||||
use global::constants::{ALL_MESSAGES_SUB_ID, APP_ID};
|
||||
use global::{shared_state, NostrSignal};
|
||||
use global::constants::{
|
||||
ALL_MESSAGES_SUB_ID, APP_ID, APP_PUBKEY, BOOTSTRAP_RELAYS, METADATA_BATCH_LIMIT,
|
||||
METADATA_BATCH_TIMEOUT, NEW_MESSAGE_SUB_ID, SEARCH_RELAYS,
|
||||
};
|
||||
use global::{nostr_client, NostrSignal};
|
||||
use gpui::{
|
||||
actions, px, size, App, AppContext, Application, Bounds, KeyBinding, Menu, MenuItem,
|
||||
WindowBounds, WindowKind, WindowOptions,
|
||||
@@ -15,7 +20,10 @@ use gpui::{
|
||||
use gpui::{point, SharedString, TitlebarOptions};
|
||||
#[cfg(target_os = "linux")]
|
||||
use gpui::{WindowBackgroundAppearance, WindowDecorations};
|
||||
use nostr_sdk::SubscriptionId;
|
||||
use itertools::Itertools;
|
||||
use nostr_sdk::prelude::*;
|
||||
use registry::Registry;
|
||||
use smol::channel::{self, Sender};
|
||||
use theme::Theme;
|
||||
use ui::Root;
|
||||
|
||||
@@ -31,15 +39,145 @@ fn main() {
|
||||
// Initialize logging
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
// Initialize the Nostr Client
|
||||
let client = nostr_client();
|
||||
|
||||
// Initialize the Application
|
||||
let app = Application::new()
|
||||
.with_assets(Assets)
|
||||
.with_http_client(Arc::new(reqwest_client::ReqwestClient::new()));
|
||||
|
||||
// Initialize the Global State and process events in a separate thread.
|
||||
let (signal_tx, signal_rx) = channel::bounded::<NostrSignal>(2048);
|
||||
let (mta_tx, mta_rx) = channel::bounded::<PublicKey>(1024);
|
||||
let (event_tx, event_rx) = channel::unbounded::<Event>();
|
||||
|
||||
let signal_tx_clone = signal_tx.clone();
|
||||
let mta_tx_clone = mta_tx.clone();
|
||||
|
||||
app.background_executor()
|
||||
.spawn(async move {
|
||||
shared_state().start().await;
|
||||
// Subscribe for app updates from the bootstrap relays.
|
||||
if let Err(e) = connect(client).await {
|
||||
log::error!("Failed to connect to bootstrap relays: {e}");
|
||||
}
|
||||
|
||||
// Connect to bootstrap relays.
|
||||
if let Err(e) = subscribe_for_app_updates(client).await {
|
||||
log::error!("Failed to subscribe for app updates: {e}");
|
||||
}
|
||||
|
||||
// Handle Nostr notifications.
|
||||
//
|
||||
// Send the redefined signal back to GPUI via channel.
|
||||
if let Err(e) =
|
||||
handle_nostr_notifications(client, &signal_tx_clone, &mta_tx_clone, &event_tx).await
|
||||
{
|
||||
log::error!("Failed to handle Nostr notifications: {e}");
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
app.background_executor()
|
||||
.spawn(async move {
|
||||
let duration = Duration::from_millis(METADATA_BATCH_TIMEOUT);
|
||||
let mut batch: BTreeSet<PublicKey> = BTreeSet::new();
|
||||
|
||||
/// Internal events for the metadata batching system
|
||||
enum BatchEvent {
|
||||
NewKeys(PublicKey),
|
||||
Timeout,
|
||||
Closed,
|
||||
}
|
||||
|
||||
loop {
|
||||
let duration = smol::Timer::after(duration);
|
||||
|
||||
let recv = || async {
|
||||
if let Ok(public_key) = mta_rx.recv().await {
|
||||
BatchEvent::NewKeys(public_key)
|
||||
} else {
|
||||
BatchEvent::Closed
|
||||
}
|
||||
};
|
||||
|
||||
let timeout = || async {
|
||||
duration.await;
|
||||
BatchEvent::Timeout
|
||||
};
|
||||
|
||||
match smol::future::or(recv(), timeout()).await {
|
||||
BatchEvent::NewKeys(public_key) => {
|
||||
batch.insert(public_key);
|
||||
// Process immediately if batch limit reached
|
||||
if batch.len() >= METADATA_BATCH_LIMIT {
|
||||
sync_data_for_pubkeys(client, std::mem::take(&mut batch)).await;
|
||||
}
|
||||
}
|
||||
BatchEvent::Timeout => {
|
||||
if !batch.is_empty() {
|
||||
sync_data_for_pubkeys(client, std::mem::take(&mut batch)).await;
|
||||
}
|
||||
}
|
||||
BatchEvent::Closed => {
|
||||
if !batch.is_empty() {
|
||||
sync_data_for_pubkeys(client, std::mem::take(&mut batch)).await;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
app.background_executor()
|
||||
.spawn(async move {
|
||||
let mut counter = 0;
|
||||
|
||||
loop {
|
||||
// Signer is unset, probably user is not ready to retrieve gift wrap events
|
||||
if client.signer().await.is_err() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let duration = smol::Timer::after(Duration::from_secs(75));
|
||||
|
||||
let recv = || async {
|
||||
// prevent inline format
|
||||
(event_rx.recv().await).ok()
|
||||
};
|
||||
|
||||
let timeout = || async {
|
||||
duration.await;
|
||||
None
|
||||
};
|
||||
|
||||
match smol::future::or(recv(), timeout()).await {
|
||||
Some(event) => {
|
||||
// Process the gift wrap event unwrapping
|
||||
let is_cached =
|
||||
try_unwrap_event(client, &signal_tx, &mta_tx, &event, false).await;
|
||||
|
||||
// Increment the total messages counter if message is not from cache
|
||||
if !is_cached {
|
||||
counter += 1;
|
||||
}
|
||||
|
||||
// Send partial finish signal to GPUI
|
||||
if counter >= 20 {
|
||||
signal_tx.send(NostrSignal::PartialFinish).await.ok();
|
||||
// Reset counter
|
||||
counter = 0;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
signal_tx.send(NostrSignal::Finish).await.ok();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Event channel is no longer needed when all gift wrap events have been processed
|
||||
event_rx.close();
|
||||
})
|
||||
.detach();
|
||||
|
||||
@@ -98,6 +236,8 @@ fn main() {
|
||||
cx.activate(true);
|
||||
// Initialize components
|
||||
ui::init(cx);
|
||||
// Initialize app registry
|
||||
registry::init(cx);
|
||||
// Initialize settings
|
||||
settings::init(cx);
|
||||
// Initialize client keys
|
||||
@@ -106,44 +246,51 @@ fn main() {
|
||||
identity::init(window, cx);
|
||||
// Initialize auto update
|
||||
auto_update::init(cx);
|
||||
// Initialize chat state
|
||||
chats::init(cx);
|
||||
|
||||
// Spawn a task to handle events from nostr channel
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
let all_messages_sub_id = SubscriptionId::new(ALL_MESSAGES_SUB_ID);
|
||||
|
||||
while let Ok(signal) = shared_state().signal().recv().await {
|
||||
while let Ok(signal) = signal_rx.recv().await {
|
||||
cx.update(|window, cx| {
|
||||
let chats = ChatRegistry::global(cx);
|
||||
let registry = Registry::global(cx);
|
||||
let auto_updater = AutoUpdater::global(cx);
|
||||
|
||||
match signal {
|
||||
NostrSignal::Event(event) => {
|
||||
chats.update(cx, |this, cx| {
|
||||
this.event_to_message(event, window, cx);
|
||||
});
|
||||
}
|
||||
// Load chat rooms and stop the loading status
|
||||
NostrSignal::Finish => {
|
||||
chats.update(cx, |this, cx| {
|
||||
registry.update(cx, |this, cx| {
|
||||
this.load_rooms(window, cx);
|
||||
this.set_loading(false, cx);
|
||||
});
|
||||
}
|
||||
// Load chat rooms without setting as finished
|
||||
NostrSignal::PartialFinish => {
|
||||
chats.update(cx, |this, cx| {
|
||||
registry.update(cx, |this, cx| {
|
||||
this.load_rooms(window, cx);
|
||||
});
|
||||
}
|
||||
// Load chat rooms without setting as finished
|
||||
NostrSignal::Eose(subscription_id) => {
|
||||
// Only load chat rooms if the subscription ID matches the all_messages_sub_id
|
||||
if subscription_id == all_messages_sub_id {
|
||||
chats.update(cx, |this, cx| {
|
||||
registry.update(cx, |this, cx| {
|
||||
this.load_rooms(window, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
// Add the new metadata to the registry or update the existing one
|
||||
NostrSignal::Metadata(event) => {
|
||||
registry.update(cx, |this, cx| {
|
||||
this.insert_or_update_person(event, cx);
|
||||
});
|
||||
}
|
||||
// Convert the gift wrapped message to a message
|
||||
NostrSignal::GiftWrap(event) => {
|
||||
registry.update(cx, |this, cx| {
|
||||
this.event_to_message(event, window, cx);
|
||||
});
|
||||
}
|
||||
NostrSignal::Notice(_msg) => {
|
||||
// window.push_notification(msg, cx);
|
||||
}
|
||||
@@ -170,3 +317,262 @@ fn quit(_: &Quit, cx: &mut App) {
|
||||
log::info!("Gracefully quitting the application . . .");
|
||||
cx.quit();
|
||||
}
|
||||
|
||||
async fn connect(client: &Client) -> Result<(), Error> {
|
||||
for relay in BOOTSTRAP_RELAYS.into_iter() {
|
||||
client.add_relay(relay).await?;
|
||||
}
|
||||
|
||||
log::info!("Connected to bootstrap relays");
|
||||
|
||||
for relay in SEARCH_RELAYS.into_iter() {
|
||||
client.add_relay(relay).await?;
|
||||
}
|
||||
|
||||
log::info!("Connected to search relays");
|
||||
|
||||
// Establish connection to relays
|
||||
client.connect().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_nostr_notifications(
|
||||
client: &Client,
|
||||
signal_tx: &Sender<NostrSignal>,
|
||||
mta_tx: &Sender<PublicKey>,
|
||||
event_tx: &Sender<Event>,
|
||||
) -> Result<(), Error> {
|
||||
let new_messages_sub_id = SubscriptionId::new(NEW_MESSAGE_SUB_ID);
|
||||
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||
|
||||
let mut notifications = client.notifications();
|
||||
let mut processed_events: BTreeSet<EventId> = BTreeSet::new();
|
||||
let mut processed_dm_relays: BTreeSet<PublicKey> = BTreeSet::new();
|
||||
|
||||
while let Ok(notification) = notifications.recv().await {
|
||||
let RelayPoolNotification::Message { message, .. } = notification else {
|
||||
continue;
|
||||
};
|
||||
|
||||
match message {
|
||||
RelayMessage::Event {
|
||||
event,
|
||||
subscription_id,
|
||||
} => {
|
||||
if processed_events.contains(&event.id) {
|
||||
continue;
|
||||
}
|
||||
// Skip events that have already been processed
|
||||
processed_events.insert(event.id);
|
||||
|
||||
match event.kind {
|
||||
Kind::GiftWrap => {
|
||||
if *subscription_id == new_messages_sub_id {
|
||||
let event = event.as_ref();
|
||||
_ = try_unwrap_event(client, signal_tx, mta_tx, event, false).await;
|
||||
} else {
|
||||
event_tx.send(event.into_owned()).await.ok();
|
||||
}
|
||||
}
|
||||
Kind::Metadata => {
|
||||
signal_tx
|
||||
.send(NostrSignal::Metadata(event.into_owned()))
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
Kind::ContactList => {
|
||||
if let Ok(true) = check_author(client, &event).await {
|
||||
for public_key in event.tags.public_keys().copied() {
|
||||
mta_tx.send(public_key).await.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
Kind::RelayList => {
|
||||
if processed_dm_relays.contains(&event.pubkey) {
|
||||
continue;
|
||||
}
|
||||
// Skip public keys that have already been processed
|
||||
processed_dm_relays.insert(event.pubkey);
|
||||
|
||||
let filter = Filter::new()
|
||||
.author(event.pubkey)
|
||||
.kind(Kind::InboxRelays)
|
||||
.limit(1);
|
||||
|
||||
let relay_urls = nip65::extract_owned_relay_list(event.into_owned())
|
||||
.map(|(url, _)| url)
|
||||
.collect_vec();
|
||||
|
||||
if !relay_urls.is_empty() {
|
||||
client
|
||||
.subscribe_to(relay_urls, filter, Some(opts))
|
||||
.await
|
||||
.ok();
|
||||
|
||||
log::info!("Subscribe for messaging relays")
|
||||
}
|
||||
}
|
||||
Kind::ReleaseArtifactSet => {
|
||||
let ids = event.tags.event_ids().copied();
|
||||
let filter = Filter::new().ids(ids).kind(Kind::FileMetadata);
|
||||
|
||||
client
|
||||
.subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts))
|
||||
.await
|
||||
.ok();
|
||||
|
||||
signal_tx
|
||||
.send(NostrSignal::AppUpdate(event.into_owned()))
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
RelayMessage::EndOfStoredEvents(subscription_id) => {
|
||||
signal_tx
|
||||
.send(NostrSignal::Eose(subscription_id.into_owned()))
|
||||
.await?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn subscribe_for_app_updates(client: &Client) -> Result<(), Error> {
|
||||
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||
|
||||
let coordinate = Coordinate {
|
||||
kind: Kind::Custom(32267),
|
||||
public_key: PublicKey::from_hex(APP_PUBKEY).expect("App Pubkey is invalid"),
|
||||
identifier: APP_ID.into(),
|
||||
};
|
||||
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::ReleaseArtifactSet)
|
||||
.coordinate(&coordinate)
|
||||
.limit(1);
|
||||
|
||||
client
|
||||
.subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn check_author(client: &Client, event: &Event) -> Result<bool, Error> {
|
||||
let signer = client.signer().await?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
|
||||
Ok(public_key == event.pubkey)
|
||||
}
|
||||
|
||||
async fn sync_data_for_pubkeys(client: &Client, public_keys: BTreeSet<PublicKey>) {
|
||||
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||
let kinds = vec![Kind::Metadata, Kind::ContactList, Kind::RelayList];
|
||||
|
||||
let filter = Filter::new()
|
||||
.limit(public_keys.len() * kinds.len())
|
||||
.authors(public_keys)
|
||||
.kinds(kinds);
|
||||
|
||||
if let Err(e) = client
|
||||
.subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts))
|
||||
.await
|
||||
{
|
||||
log::error!("Failed to sync metadata: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Stores an unwrapped event in local database with reference to original
|
||||
async fn set_unwrapped(client: &Client, root: EventId, event: &Event) -> Result<(), Error> {
|
||||
// Must be use the random generated keys to sign this event
|
||||
let event = EventBuilder::new(Kind::ApplicationSpecificData, event.as_json())
|
||||
.tags(vec![Tag::identifier(root), Tag::event(root)])
|
||||
.sign(&Keys::generate())
|
||||
.await?;
|
||||
|
||||
// Only save this event into the local database
|
||||
client.database().save_event(&event).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Retrieves a previously unwrapped event from local database
|
||||
async fn get_unwrapped(client: &Client, target: EventId) -> Result<Event, Error> {
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::ApplicationSpecificData)
|
||||
.identifier(target)
|
||||
.event(target)
|
||||
.limit(1);
|
||||
|
||||
if let Some(event) = client.database().query(filter).await?.first_owned() {
|
||||
Ok(Event::from_json(event.content)?)
|
||||
} else {
|
||||
Err(anyhow!("Event is not cached yet"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Unwraps a gift-wrapped event and processes its contents.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `event` - The gift-wrapped event to unwrap
|
||||
/// * `incoming` - Whether this is a newly received event (true) or old
|
||||
///
|
||||
/// # Returns
|
||||
/// Returns `true` if the event was successfully loaded from cache or saved after unwrapping.
|
||||
async fn try_unwrap_event(
|
||||
client: &Client,
|
||||
signal_tx: &Sender<NostrSignal>,
|
||||
mta_tx: &Sender<PublicKey>,
|
||||
event: &Event,
|
||||
incoming: bool,
|
||||
) -> bool {
|
||||
let mut is_cached = false;
|
||||
|
||||
let event = match get_unwrapped(client, event.id).await {
|
||||
Ok(event) => {
|
||||
is_cached = true;
|
||||
event
|
||||
}
|
||||
Err(_) => {
|
||||
match client.unwrap_gift_wrap(event).await {
|
||||
Ok(unwrap) => {
|
||||
let Ok(unwrapped) = unwrap.rumor.sign_with_keys(&Keys::generate()) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
// Save this event to the database for future use.
|
||||
if let Err(e) = set_unwrapped(client, event.id, &unwrapped).await {
|
||||
log::error!("Failed to save event: {e}")
|
||||
}
|
||||
|
||||
unwrapped
|
||||
}
|
||||
Err(_) => return false,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Save the event to the database, use for query directly.
|
||||
if let Err(e) = client.database().save_event(&event).await {
|
||||
log::error!("Failed to save event: {e}")
|
||||
}
|
||||
|
||||
// Send all pubkeys to the batch to sync metadata
|
||||
mta_tx.send(event.pubkey).await.ok();
|
||||
|
||||
for public_key in event.tags.public_keys().copied() {
|
||||
mta_tx.send(public_key).await.ok();
|
||||
}
|
||||
|
||||
// Send a notify to GPUI if this is a new message
|
||||
if incoming {
|
||||
signal_tx.send(NostrSignal::GiftWrap(event)).await.ok();
|
||||
}
|
||||
|
||||
is_cached
|
||||
}
|
||||
|
||||
@@ -3,16 +3,14 @@ use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use chats::message::Message;
|
||||
use chats::room::{Room, RoomKind, SendError};
|
||||
use common::display::DisplayProfile;
|
||||
use common::nip96::nip96_upload;
|
||||
use common::profile::RenderProfile;
|
||||
use global::shared_state;
|
||||
use global::nostr_client;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, img, list, px, red, relative, rems, svg, white, Action, AnyElement, App, AppContext,
|
||||
ClipboardItem, Context, Div, Element, Empty, Entity, EventEmitter, Flatten, FocusHandle,
|
||||
Focusable, InteractiveElement, IntoElement, ListAlignment, ListState, ObjectFit, ParentElement,
|
||||
div, img, list, px, red, rems, white, Action, AnyElement, App, AppContext, ClipboardItem,
|
||||
Context, Div, Element, Empty, Entity, EventEmitter, Flatten, FocusHandle, Focusable,
|
||||
InteractiveElement, IntoElement, ListAlignment, ListState, ObjectFit, ParentElement,
|
||||
PathPromptOptions, Render, RetainAllImageCache, SharedString, StatefulInteractiveElement,
|
||||
Styled, StyledImage, Subscription, Window,
|
||||
};
|
||||
@@ -20,6 +18,9 @@ use i18n::t;
|
||||
use identity::Identity;
|
||||
use itertools::Itertools;
|
||||
use nostr_sdk::prelude::*;
|
||||
use registry::message::Message;
|
||||
use registry::room::{Room, RoomKind, SendError};
|
||||
use registry::Registry;
|
||||
use serde::Deserialize;
|
||||
use settings::AppSettings;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
@@ -71,15 +72,7 @@ impl Chat {
|
||||
pub fn new(room: Entity<Room>, window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||
let attaches = cx.new(|_| None);
|
||||
let replies_to = cx.new(|_| None);
|
||||
|
||||
let messages = cx.new(|_| {
|
||||
let message = Message::builder()
|
||||
.content(t!("chat.private_conversation_notice").into())
|
||||
.build_rc()
|
||||
.unwrap();
|
||||
|
||||
vec![message]
|
||||
});
|
||||
let messages = cx.new(|_| vec![]);
|
||||
|
||||
let input = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
@@ -220,15 +213,11 @@ impl Chat {
|
||||
|
||||
// TODO: find a better way to prevent duplicate messages during optimistic updates
|
||||
fn prevent_duplicate_message(&self, new_msg: &Message, cx: &Context<Self>) -> bool {
|
||||
let Some(account) = Identity::get_global(cx).profile() else {
|
||||
let Some(identity) = Identity::read_global(cx).public_key() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let Some(author) = new_msg.author.as_ref() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if account.public_key() != author.public_key() {
|
||||
if new_msg.author != identity {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -237,12 +226,7 @@ impl Chat {
|
||||
self.messages
|
||||
.read(cx)
|
||||
.iter()
|
||||
.filter(|m| {
|
||||
m.borrow()
|
||||
.author
|
||||
.as_ref()
|
||||
.is_some_and(|p| p.public_key() == account.public_key())
|
||||
})
|
||||
.filter(|m| m.borrow().author == identity)
|
||||
.any(|existing| {
|
||||
let existing = existing.borrow();
|
||||
// Check if messages are within the time window
|
||||
@@ -297,10 +281,10 @@ impl Chat {
|
||||
});
|
||||
|
||||
this.messages.update(cx, |this, cx| {
|
||||
if let Some(msg) = id.and_then(|id| {
|
||||
this.iter().find(|msg| msg.borrow().id == Some(id)).cloned()
|
||||
}) {
|
||||
msg.borrow_mut().errors = Some(reports);
|
||||
if let Some(msg) =
|
||||
this.iter().find(|msg| msg.borrow().id == id).cloned()
|
||||
{
|
||||
msg.borrow_mut().errors = Some(reports.into());
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
@@ -330,7 +314,7 @@ impl Chat {
|
||||
.messages
|
||||
.read(cx)
|
||||
.iter()
|
||||
.position(|m| m.borrow().id == Some(id))
|
||||
.position(|m| m.borrow().id == id)
|
||||
{
|
||||
self.list_state.scroll_to_reveal_item(ix);
|
||||
}
|
||||
@@ -350,7 +334,7 @@ impl Chat {
|
||||
fn remove_reply(&mut self, id: EventId, cx: &mut Context<Self>) {
|
||||
self.replies_to.update(cx, |this, cx| {
|
||||
if let Some(replies) = this {
|
||||
if let Some(ix) = replies.iter().position(|m| m.id == Some(id)) {
|
||||
if let Some(ix) = replies.iter().position(|m| m.id == id) {
|
||||
replies.remove(ix);
|
||||
cx.notify();
|
||||
}
|
||||
@@ -391,9 +375,7 @@ impl Chat {
|
||||
|
||||
// Spawn task via async utility instead of GPUI context
|
||||
nostr_sdk::async_utility::task::spawn(async move {
|
||||
let url = nip96_upload(shared_state().client(), &nip96, file_data)
|
||||
.await
|
||||
.ok();
|
||||
let url = nip96_upload(nostr_client(), &nip96, file_data).await.ok();
|
||||
_ = tx.send(url);
|
||||
});
|
||||
|
||||
@@ -482,6 +464,9 @@ impl Chat {
|
||||
}
|
||||
|
||||
fn render_reply(&mut self, message: &Message, cx: &Context<Self>) -> impl IntoElement {
|
||||
let registry = Registry::read_global(cx);
|
||||
let profile = registry.get_person(&message.author, cx);
|
||||
|
||||
div()
|
||||
.w_full()
|
||||
.pl_2()
|
||||
@@ -503,7 +488,7 @@ impl Chat {
|
||||
.child(
|
||||
div()
|
||||
.text_color(cx.theme().text_accent)
|
||||
.child(message.author.as_ref().unwrap().render_name()),
|
||||
.child(profile.display_name()),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
@@ -512,7 +497,7 @@ impl Chat {
|
||||
.xsmall()
|
||||
.ghost()
|
||||
.on_click({
|
||||
let id = message.id.unwrap();
|
||||
let id = message.id;
|
||||
cx.listener(move |this, _, _, cx| {
|
||||
this.remove_reply(id, cx);
|
||||
})
|
||||
@@ -541,43 +526,16 @@ impl Chat {
|
||||
|
||||
let proxy = AppSettings::get_global(cx).settings.proxy_user_avatars;
|
||||
let hide_avatar = AppSettings::get_global(cx).settings.hide_user_avatars;
|
||||
let registry = Registry::read_global(cx);
|
||||
|
||||
let message = message.borrow();
|
||||
|
||||
// Message without ID, Author probably the placeholder
|
||||
let (Some(id), Some(author)) = (message.id, message.author.as_ref()) else {
|
||||
return div()
|
||||
.id(ix)
|
||||
.group("")
|
||||
.w_full()
|
||||
.relative()
|
||||
.flex()
|
||||
.gap_3()
|
||||
.px_3()
|
||||
.py_2()
|
||||
.w_full()
|
||||
.h_32()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.text_center()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().text_placeholder)
|
||||
.line_height(relative(1.3))
|
||||
.child(
|
||||
svg()
|
||||
.path("brand/coop.svg")
|
||||
.size_10()
|
||||
.text_color(cx.theme().elevated_surface_background),
|
||||
)
|
||||
.child(message.content.clone());
|
||||
};
|
||||
let author = registry.get_person(&message.author, cx);
|
||||
let mentions = registry.get_group_person(&message.mentions, cx);
|
||||
|
||||
let texts = self
|
||||
.text_data
|
||||
.entry(id)
|
||||
.or_insert_with(|| RichText::new(message.content.to_string(), &message.mentions));
|
||||
.entry(message.id)
|
||||
.or_insert_with(|| RichText::new(message.content.to_string(), &mentions));
|
||||
|
||||
div()
|
||||
.id(ix)
|
||||
@@ -591,7 +549,7 @@ impl Chat {
|
||||
.flex()
|
||||
.gap_3()
|
||||
.when(!hide_avatar, |this| {
|
||||
this.child(Avatar::new(author.render_avatar(proxy)).size(rems(2.)))
|
||||
this.child(Avatar::new(author.avatar_url(proxy)).size(rems(2.)))
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
@@ -610,7 +568,7 @@ impl Chat {
|
||||
div()
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().text)
|
||||
.child(author.render_name()),
|
||||
.child(author.display_name()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
@@ -627,7 +585,7 @@ impl Chat {
|
||||
.messages
|
||||
.read(cx)
|
||||
.iter()
|
||||
.find(|msg| msg.borrow().id == Some(*id))
|
||||
.find(|msg| msg.borrow().id == *id)
|
||||
.cloned()
|
||||
{
|
||||
let message = message.borrow();
|
||||
@@ -643,13 +601,7 @@ impl Chat {
|
||||
.child(
|
||||
div()
|
||||
.text_color(cx.theme().text_accent)
|
||||
.child(
|
||||
message
|
||||
.author
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.render_name(),
|
||||
),
|
||||
.child(author.display_name()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
@@ -664,7 +616,7 @@ impl Chat {
|
||||
.elevated_surface_background)
|
||||
})
|
||||
.on_click({
|
||||
let id = message.id.unwrap();
|
||||
let id = message.id;
|
||||
cx.listener(move |this, _, _, cx| {
|
||||
this.scroll_to(id, cx)
|
||||
})
|
||||
@@ -881,7 +833,7 @@ fn message_border(cx: &App) -> Div {
|
||||
.bg(cx.theme().border_transparent)
|
||||
}
|
||||
|
||||
fn message_errors(errors: Vec<SendError>, cx: &App) -> Div {
|
||||
fn message_errors(errors: SmallVec<[SendError; 1]>, cx: &App) -> Div {
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
@@ -898,7 +850,7 @@ fn message_errors(errors: Vec<SendError>, cx: &App) -> Div {
|
||||
.gap_1()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(SharedString::new(t!("chat.send_to_label")))
|
||||
.child(error.profile.render_name()),
|
||||
.child(error.profile.display_name()),
|
||||
)
|
||||
.child(error.message)
|
||||
}))
|
||||
|
||||
@@ -2,11 +2,10 @@ use std::ops::Range;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Error};
|
||||
use chats::room::{Room, RoomKind};
|
||||
use chats::ChatRegistry;
|
||||
use common::display::DisplayProfile;
|
||||
use common::nip05::nip05_profile;
|
||||
use common::profile::RenderProfile;
|
||||
use global::shared_state;
|
||||
use global::constants::BOOTSTRAP_RELAYS;
|
||||
use global::nostr_client;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, img, px, red, relative, uniform_list, App, AppContext, Context, Entity,
|
||||
@@ -16,37 +15,37 @@ use gpui::{
|
||||
use i18n::t;
|
||||
use itertools::Itertools;
|
||||
use nostr_sdk::prelude::*;
|
||||
use registry::room::{Room, RoomKind};
|
||||
use registry::Registry;
|
||||
use settings::AppSettings;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use smol::Timer;
|
||||
use theme::ActiveTheme;
|
||||
use ui::{
|
||||
button::{Button, ButtonVariants},
|
||||
input::{InputEvent, InputState, TextInput},
|
||||
notification::Notification,
|
||||
ContextModal, Disableable, Icon, IconName, Sizable, StyledExt,
|
||||
};
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::input::{InputEvent, InputState, TextInput};
|
||||
use ui::notification::Notification;
|
||||
use ui::{ContextModal, Disableable, Icon, IconName, Sizable, StyledExt};
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Compose> {
|
||||
cx.new(|cx| Compose::new(window, cx))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug)]
|
||||
struct Contact {
|
||||
profile: Profile,
|
||||
public_key: PublicKey,
|
||||
select: bool,
|
||||
}
|
||||
|
||||
impl AsRef<Profile> for Contact {
|
||||
fn as_ref(&self) -> &Profile {
|
||||
&self.profile
|
||||
impl AsRef<PublicKey> for Contact {
|
||||
fn as_ref(&self) -> &PublicKey {
|
||||
&self.public_key
|
||||
}
|
||||
}
|
||||
|
||||
impl Contact {
|
||||
pub fn new(profile: Profile) -> Self {
|
||||
pub fn new(public_key: PublicKey) -> Self {
|
||||
Self {
|
||||
profile,
|
||||
public_key,
|
||||
select: false,
|
||||
}
|
||||
}
|
||||
@@ -88,20 +87,21 @@ impl Compose {
|
||||
&user_input,
|
||||
window,
|
||||
move |this, _input, event, window, cx| {
|
||||
match event {
|
||||
InputEvent::PressEnter { .. } => this.add_and_select_contact(window, cx),
|
||||
InputEvent::Change(_) => {}
|
||||
_ => {}
|
||||
if let InputEvent::PressEnter { .. } = event {
|
||||
this.add_and_select_contact(window, cx)
|
||||
};
|
||||
},
|
||||
));
|
||||
|
||||
let get_contacts: Task<Result<Vec<Contact>, Error>> = cx.background_spawn(async move {
|
||||
let client = shared_state().client();
|
||||
let client = nostr_client();
|
||||
let signer = client.signer().await?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
let profiles = client.database().contacts(public_key).await?;
|
||||
let contacts = profiles.into_iter().map(Contact::new).collect_vec();
|
||||
let contacts = profiles
|
||||
.into_iter()
|
||||
.map(|profile| Contact::new(profile.public_key()))
|
||||
.collect_vec();
|
||||
|
||||
Ok(contacts)
|
||||
});
|
||||
@@ -110,7 +110,7 @@ impl Compose {
|
||||
match get_contacts.await {
|
||||
Ok(contacts) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.contacts(contacts, cx);
|
||||
this.extend_contacts(contacts, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
@@ -135,6 +135,28 @@ impl Compose {
|
||||
}
|
||||
}
|
||||
|
||||
async fn request_metadata(client: &Client, public_key: PublicKey) -> Result<(), Error> {
|
||||
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||
let kinds = vec![Kind::Metadata, Kind::ContactList, Kind::RelayList];
|
||||
let filter = Filter::new().author(public_key).kinds(kinds).limit(10);
|
||||
|
||||
client
|
||||
.subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_pubkey(content: &str) -> Result<PublicKey, Error> {
|
||||
if content.starts_with("nprofile1") {
|
||||
Ok(Nip19Profile::from_bech32(content)?.public_key)
|
||||
} else if content.starts_with("npub1") {
|
||||
Ok(PublicKey::parse(content)?)
|
||||
} else {
|
||||
Err(anyhow!(t!("common.pubkey_invalid")))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compose(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let public_keys: Vec<PublicKey> = self.selected(cx);
|
||||
|
||||
@@ -158,7 +180,7 @@ impl Compose {
|
||||
}
|
||||
|
||||
let event: Task<Result<Room, anyhow::Error>> = cx.background_spawn(async move {
|
||||
let signer = shared_state().client().signer().await?;
|
||||
let signer = nostr_client().signer().await?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
|
||||
let room = EventBuilder::private_msg_rumor(public_keys[0], "")
|
||||
@@ -180,7 +202,7 @@ impl Compose {
|
||||
})
|
||||
.ok();
|
||||
|
||||
ChatRegistry::global(cx).update(cx, |this, cx| {
|
||||
Registry::global(cx).update(cx, |this, cx| {
|
||||
this.push_room(cx.new(|_| room), cx);
|
||||
});
|
||||
|
||||
@@ -199,7 +221,10 @@ impl Compose {
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn contacts(&mut self, contacts: impl IntoIterator<Item = Contact>, cx: &mut Context<Self>) {
|
||||
fn extend_contacts<I>(&mut self, contacts: I, cx: &mut Context<Self>)
|
||||
where
|
||||
I: IntoIterator<Item = Contact>,
|
||||
{
|
||||
self.contacts
|
||||
.extend(contacts.into_iter().map(|contact| cx.new(|_| contact)));
|
||||
cx.notify();
|
||||
@@ -209,15 +234,12 @@ impl Compose {
|
||||
if !self
|
||||
.contacts
|
||||
.iter()
|
||||
.any(|e| e.read(cx).profile.public_key() == contact.profile.public_key())
|
||||
.any(|e| e.read(cx).public_key == contact.public_key)
|
||||
{
|
||||
self.contacts.insert(0, cx.new(|_| contact));
|
||||
cx.notify();
|
||||
} else {
|
||||
self.set_error(
|
||||
Some(t!("compose.contact_existed", name = contact.profile.name()).into()),
|
||||
cx,
|
||||
);
|
||||
self.set_error(Some(t!("compose.contact_existed").into()), cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,7 +248,7 @@ impl Compose {
|
||||
.iter()
|
||||
.filter_map(|contact| {
|
||||
if contact.read(cx).select {
|
||||
Some(contact.read(cx).profile.public_key())
|
||||
Some(contact.read(cx).public_key)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -245,7 +267,7 @@ impl Compose {
|
||||
this.set_loading(true, cx);
|
||||
});
|
||||
|
||||
let task: Task<Result<Contact, anyhow::Error>> = if content.contains("@") {
|
||||
let task: Task<Result<Contact, Error>> = if content.contains("@") {
|
||||
cx.background_spawn(async move {
|
||||
let (tx, rx) = oneshot::channel::<Option<Nip05Profile>>();
|
||||
|
||||
@@ -255,82 +277,54 @@ impl Compose {
|
||||
});
|
||||
|
||||
if let Ok(Some(profile)) = rx.await {
|
||||
let client = nostr_client();
|
||||
let public_key = profile.public_key;
|
||||
let metadata = shared_state()
|
||||
.client()
|
||||
.fetch_metadata(public_key, Duration::from_secs(2))
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
let profile = Profile::new(public_key, metadata);
|
||||
let contact = Contact::new(profile).select();
|
||||
let contact = Contact::new(public_key).select();
|
||||
|
||||
Self::request_metadata(client, public_key).await?;
|
||||
|
||||
Ok(contact)
|
||||
} else {
|
||||
Err(anyhow!(t!("common.not_found")))
|
||||
}
|
||||
})
|
||||
} else if content.starts_with("nprofile1") {
|
||||
let Some(public_key) = Nip19Profile::from_bech32(&content)
|
||||
.map(|nip19| nip19.public_key)
|
||||
.ok()
|
||||
else {
|
||||
self.set_error(Some(t!("common.pubkey_invalid").into()), cx);
|
||||
return;
|
||||
};
|
||||
|
||||
} else if let Ok(public_key) = Self::parse_pubkey(&content) {
|
||||
cx.background_spawn(async move {
|
||||
let metadata = shared_state()
|
||||
.client()
|
||||
.fetch_metadata(public_key, Duration::from_secs(2))
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
let client = nostr_client();
|
||||
let contact = Contact::new(public_key).select();
|
||||
|
||||
let profile = Profile::new(public_key, metadata);
|
||||
let contact = Contact::new(profile).select();
|
||||
Self::request_metadata(client, public_key).await?;
|
||||
|
||||
Ok(contact)
|
||||
})
|
||||
} else {
|
||||
let Ok(public_key) = PublicKey::parse(&content) else {
|
||||
self.set_error(Some(t!("common.pubkey_invalid").into()), cx);
|
||||
return;
|
||||
};
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let metadata = shared_state()
|
||||
.client()
|
||||
.fetch_metadata(public_key, Duration::from_secs(2))
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
|
||||
let profile = Profile::new(public_key, metadata);
|
||||
let contact = Contact::new(profile).select();
|
||||
|
||||
Ok(contact)
|
||||
})
|
||||
self.set_error(Some(t!("common.pubkey_invalid").into()), cx);
|
||||
return;
|
||||
};
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| match task.await {
|
||||
Ok(contact) => {
|
||||
cx.update(|window, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.push_contact(contact, cx);
|
||||
this.set_adding(false, cx);
|
||||
this.user_input.update(cx, |this, cx| {
|
||||
this.set_value("", window, cx);
|
||||
this.set_loading(false, cx);
|
||||
});
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
match task.await {
|
||||
Ok(contact) => {
|
||||
cx.update(|window, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.push_contact(contact, cx);
|
||||
this.set_adding(false, cx);
|
||||
this.user_input.update(cx, |this, cx| {
|
||||
this.set_value("", window, cx);
|
||||
this.set_loading(false, cx);
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Err(e) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_error(Some(e.to_string().into()), cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_error(Some(e.to_string().into()), cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
};
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
@@ -374,6 +368,7 @@ impl Compose {
|
||||
|
||||
fn list_items(&self, range: Range<usize>, cx: &Context<Self>) -> Vec<impl IntoElement> {
|
||||
let proxy = AppSettings::get_global(cx).settings.proxy_user_avatars;
|
||||
let registry = Registry::read_global(cx);
|
||||
let mut items = Vec::with_capacity(self.contacts.len());
|
||||
|
||||
for ix in range {
|
||||
@@ -381,14 +376,16 @@ impl Compose {
|
||||
continue;
|
||||
};
|
||||
|
||||
let profile = entity.read(cx).as_ref();
|
||||
let public_key = entity.read(cx).as_ref();
|
||||
let profile = registry.get_person(public_key, cx);
|
||||
let selected = entity.read(cx).select;
|
||||
|
||||
items.push(
|
||||
div()
|
||||
.id(ix)
|
||||
.w_full()
|
||||
.h_10()
|
||||
.h_11()
|
||||
.py_1()
|
||||
.px_3()
|
||||
.flex()
|
||||
.items_center()
|
||||
@@ -399,14 +396,14 @@ impl Compose {
|
||||
.items_center()
|
||||
.gap_3()
|
||||
.text_sm()
|
||||
.child(img(profile.render_avatar(proxy)).size_7().flex_shrink_0())
|
||||
.child(profile.render_name()),
|
||||
.child(img(profile.avatar_url(proxy)).size_7().flex_shrink_0())
|
||||
.child(profile.display_name()),
|
||||
)
|
||||
.when(selected, |this| {
|
||||
this.child(
|
||||
Icon::new(IconName::CheckCircleFill)
|
||||
.small()
|
||||
.text_color(cx.theme().ring),
|
||||
.text_color(cx.theme().text_accent),
|
||||
)
|
||||
})
|
||||
.hover(|this| this.bg(cx.theme().elevated_surface_background))
|
||||
@@ -542,7 +539,6 @@ impl Render for Compose {
|
||||
this.list_items(range, cx)
|
||||
}),
|
||||
)
|
||||
.pb_4()
|
||||
.min_h(px(280.)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use i18n::t;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -12,6 +11,7 @@ use gpui::{
|
||||
EventEmitter, FocusHandle, Focusable, Image, InteractiveElement, IntoElement, ParentElement,
|
||||
Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Window,
|
||||
};
|
||||
use i18n::t;
|
||||
use identity::Identity;
|
||||
use nostr_connect::prelude::*;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
@@ -581,15 +581,13 @@ impl Render for Login {
|
||||
})),
|
||||
)
|
||||
.when_some(self.countdown.read(cx).as_ref(), |this, i| {
|
||||
let msg = t!("login.approve_message", i = i);
|
||||
this.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_center()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(SharedString::new(t!(
|
||||
"login.approve_message",
|
||||
i = i
|
||||
))),
|
||||
.child(SharedString::new(msg)),
|
||||
)
|
||||
})
|
||||
.when_some(self.error.read(cx).clone(), |this, error| {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use common::nip96::nip96_upload;
|
||||
use global::shared_state;
|
||||
use global::nostr_client;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, img, relative, AnyElement, App, AppContext, Context, Entity, EventEmitter, Flatten,
|
||||
@@ -102,7 +102,7 @@ impl NewAccount {
|
||||
.ok();
|
||||
true
|
||||
})
|
||||
.on_ok(move |_, _window, cx| {
|
||||
.on_ok(move |_, window, cx| {
|
||||
let metadata = metadata.clone();
|
||||
let value = weak_input
|
||||
.read_with(cx, |state, _cx| state.value().to_owned())
|
||||
@@ -110,7 +110,7 @@ impl NewAccount {
|
||||
|
||||
if let Some(password) = value {
|
||||
Identity::global(cx).update(cx, |this, cx| {
|
||||
this.new_identity(Keys::generate(), password.to_string(), metadata, cx);
|
||||
this.new_identity(password.to_string(), metadata, window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -161,9 +161,7 @@ impl NewAccount {
|
||||
let (tx, rx) = oneshot::channel::<Url>();
|
||||
|
||||
nostr_sdk::async_utility::task::spawn(async move {
|
||||
if let Ok(url) =
|
||||
nip96_upload(shared_state().client(), &nip96, file_data).await
|
||||
{
|
||||
if let Ok(url) = nip96_upload(nostr_client(), &nip96, file_data).await {
|
||||
_ = tx.send(url);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use anyhow::anyhow;
|
||||
use common::profile::RenderProfile;
|
||||
use common::display::DisplayProfile;
|
||||
use global::constants::ACCOUNT_D;
|
||||
use global::shared_state;
|
||||
use global::nostr_client;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, relative, rems, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter,
|
||||
@@ -46,14 +46,12 @@ impl Onboarding {
|
||||
let local_account = cx.new(|_| None);
|
||||
|
||||
let task = cx.background_spawn(async move {
|
||||
let database = shared_state().client().database();
|
||||
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::ApplicationSpecificData)
|
||||
.identifier(ACCOUNT_D)
|
||||
.limit(1);
|
||||
|
||||
if let Some(event) = database.query(filter).await?.first_owned() {
|
||||
if let Some(event) = nostr_client().database().query(filter).await?.first_owned() {
|
||||
let public_key = event
|
||||
.tags
|
||||
.public_keys()
|
||||
@@ -62,10 +60,14 @@ impl Onboarding {
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap();
|
||||
let metadata = database.metadata(public_key).await?.unwrap_or_default();
|
||||
let profile = Profile::new(public_key, metadata);
|
||||
|
||||
Ok(profile)
|
||||
let metadata = nostr_client()
|
||||
.database()
|
||||
.metadata(public_key)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(Profile::new(public_key, metadata))
|
||||
} else {
|
||||
Err(anyhow!("Not found"))
|
||||
}
|
||||
@@ -213,15 +215,13 @@ impl Render for Onboarding {
|
||||
.gap_1()
|
||||
.font_semibold()
|
||||
.child(
|
||||
Avatar::new(
|
||||
profile.render_avatar(proxy),
|
||||
)
|
||||
.size(rems(1.5)),
|
||||
Avatar::new(profile.avatar_url(proxy))
|
||||
.size(rems(1.5)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.pb_px()
|
||||
.child(profile.render_name()),
|
||||
.child(profile.display_name()),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use common::profile::RenderProfile;
|
||||
use common::display::DisplayProfile;
|
||||
use global::constants::{DEFAULT_MODAL_WIDTH, NIP96_SERVER};
|
||||
use gpui::http_client::Url;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
@@ -8,6 +8,7 @@ use gpui::{
|
||||
};
|
||||
use i18n::t;
|
||||
use identity::Identity;
|
||||
use registry::Registry;
|
||||
use settings::AppSettings;
|
||||
use theme::ActiveTheme;
|
||||
use ui::avatar::Avatar;
|
||||
@@ -74,9 +75,15 @@ impl Preferences {
|
||||
|
||||
impl Render for Preferences {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let input_state = self.media_input.downgrade();
|
||||
let registry = Registry::read_global(cx);
|
||||
let settings = AppSettings::get_global(cx).settings.as_ref();
|
||||
|
||||
let profile = Identity::read_global(cx)
|
||||
.public_key()
|
||||
.map(|pk| registry.get_person(&pk, cx));
|
||||
|
||||
let input_state = self.media_input.downgrade();
|
||||
|
||||
div()
|
||||
.track_focus(&self.focus_handle)
|
||||
.size_full()
|
||||
@@ -97,7 +104,7 @@ impl Render for Preferences {
|
||||
.font_semibold()
|
||||
.child(SharedString::new(t!("preferences.account_header"))),
|
||||
)
|
||||
.when_some(Identity::get_global(cx).profile(), |this, profile| {
|
||||
.when_some(profile, |this, profile| {
|
||||
this.child(
|
||||
div()
|
||||
.w_full()
|
||||
@@ -112,7 +119,7 @@ impl Render for Preferences {
|
||||
.gap_2()
|
||||
.child(
|
||||
Avatar::new(
|
||||
profile.render_avatar(settings.proxy_user_avatars),
|
||||
profile.avatar_url(settings.proxy_user_avatars),
|
||||
)
|
||||
.size(rems(2.4)),
|
||||
)
|
||||
@@ -124,7 +131,7 @@ impl Render for Preferences {
|
||||
div()
|
||||
.line_height(relative(1.3))
|
||||
.font_semibold()
|
||||
.child(profile.render_name()),
|
||||
.child(profile.display_name()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use common::nip96::nip96_upload;
|
||||
use global::shared_state;
|
||||
use global::nostr_client;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, img, App, AppContext, Context, Entity, Flatten, IntoElement, ParentElement,
|
||||
@@ -57,7 +57,7 @@ impl Profile {
|
||||
};
|
||||
|
||||
let task: Task<Result<Option<Metadata>, Error>> = cx.background_spawn(async move {
|
||||
let client = shared_state().client();
|
||||
let client = nostr_client();
|
||||
let signer = client.signer().await?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
let metadata = client
|
||||
@@ -106,7 +106,7 @@ impl Profile {
|
||||
}
|
||||
|
||||
fn upload(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let nip96_server = AppSettings::get_global(cx).settings.media_server.clone();
|
||||
let nip96 = AppSettings::get_global(cx).settings.media_server.clone();
|
||||
let avatar_input = self.avatar_input.downgrade();
|
||||
let paths = cx.prompt_for_paths(PathPromptOptions {
|
||||
files: true,
|
||||
@@ -126,8 +126,7 @@ impl Profile {
|
||||
let (tx, rx) = oneshot::channel::<Url>();
|
||||
|
||||
nostr_sdk::async_utility::task::spawn(async move {
|
||||
let client = shared_state().client();
|
||||
if let Ok(url) = nip96_upload(client, &nip96_server, file_data).await {
|
||||
if let Ok(url) = nip96_upload(nostr_client(), &nip96, file_data).await {
|
||||
_ = tx.send(url);
|
||||
}
|
||||
});
|
||||
@@ -193,27 +192,29 @@ impl Profile {
|
||||
}
|
||||
|
||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||
let _ = shared_state().client().set_metadata(&new_metadata).await?;
|
||||
nostr_client().set_metadata(&new_metadata).await?;
|
||||
Ok(())
|
||||
});
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| match task.await {
|
||||
Ok(_) => {
|
||||
cx.update(|window, cx| {
|
||||
window.push_notification(t!("profile.updated_successfully"), cx);
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_submitting(false, cx);
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
match task.await {
|
||||
Ok(_) => {
|
||||
cx.update(|window, cx| {
|
||||
window.push_notification(t!("profile.updated_successfully"), cx);
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_submitting(false, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Err(e) => {
|
||||
cx.update(|window, cx| {
|
||||
window.push_notification(e.to_string(), cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
cx.update(|window, cx| {
|
||||
window.push_notification(e.to_string(), cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
};
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use anyhow::Error;
|
||||
use global::constants::NEW_MESSAGE_SUB_ID;
|
||||
use global::shared_state;
|
||||
use global::nostr_client;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, px, uniform_list, App, AppContext, Context, Entity, FocusHandle, InteractiveElement,
|
||||
@@ -35,7 +35,7 @@ impl Relays {
|
||||
let input = cx.new(|cx| InputState::new(window, cx).placeholder("wss://example.com"));
|
||||
let relays = cx.new(|cx| {
|
||||
let task: Task<Result<Vec<RelayUrl>, Error>> = cx.background_spawn(async move {
|
||||
let client = shared_state().client();
|
||||
let client = nostr_client();
|
||||
let signer = client.signer().await?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
let filter = Filter::new()
|
||||
@@ -106,7 +106,7 @@ impl Relays {
|
||||
|
||||
let relays = self.relays.read(cx).clone();
|
||||
let task: Task<Result<EventId, Error>> = cx.background_spawn(async move {
|
||||
let client = shared_state().client();
|
||||
let client = nostr_client();
|
||||
let signer = client.signer().await?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
|
||||
|
||||
@@ -3,14 +3,12 @@ use std::ops::Range;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Error;
|
||||
use chats::room::{Room, RoomKind};
|
||||
use chats::{ChatRegistry, RoomEmitter};
|
||||
use common::debounced_delay::DebouncedDelay;
|
||||
use common::display::DisplayProfile;
|
||||
use common::nip05::nip05_verify;
|
||||
use common::profile::RenderProfile;
|
||||
use element::DisplayRoom;
|
||||
use global::constants::{DEFAULT_MODAL_WIDTH, SEARCH_RELAYS};
|
||||
use global::shared_state;
|
||||
use global::nostr_client;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, px, relative, rems, uniform_list, AnyElement, App, AppContext, ClipboardItem, Context,
|
||||
@@ -18,9 +16,12 @@ use gpui::{
|
||||
Render, RetainAllImageCache, SharedString, StatefulInteractiveElement, Styled, Subscription,
|
||||
Task, Window,
|
||||
};
|
||||
use i18n::t;
|
||||
use identity::Identity;
|
||||
use itertools::Itertools;
|
||||
use nostr_sdk::prelude::*;
|
||||
use registry::room::{Room, RoomKind};
|
||||
use registry::{Registry, RoomEmitter};
|
||||
use settings::AppSettings;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use theme::ActiveTheme;
|
||||
@@ -35,7 +36,6 @@ use ui::skeleton::Skeleton;
|
||||
use ui::{ContextModal, IconName, Selectable, Sizable, StyledExt};
|
||||
|
||||
use crate::views::compose;
|
||||
use i18n::t;
|
||||
|
||||
mod element;
|
||||
|
||||
@@ -80,7 +80,7 @@ impl Sidebar {
|
||||
InputState::new(window, cx).placeholder(t!("sidebar.find_or_start_conversation"))
|
||||
});
|
||||
|
||||
let chats = ChatRegistry::global(cx);
|
||||
let chats = Registry::global(cx);
|
||||
let mut subscriptions = smallvec![];
|
||||
|
||||
subscriptions.push(cx.subscribe_in(
|
||||
@@ -154,7 +154,7 @@ impl Sidebar {
|
||||
let query_cloned = query.clone();
|
||||
|
||||
let task: Task<Result<BTreeSet<Room>, Error>> = cx.background_spawn(async move {
|
||||
let client = shared_state().client();
|
||||
let client = nostr_client();
|
||||
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::Metadata)
|
||||
@@ -266,7 +266,7 @@ impl Sidebar {
|
||||
};
|
||||
|
||||
let task: Task<Result<(Profile, Room), Error>> = cx.background_spawn(async move {
|
||||
let client = shared_state().client();
|
||||
let client = nostr_client();
|
||||
let signer = client.signer().await.unwrap();
|
||||
let user_pubkey = signer.get_public_key().await.unwrap();
|
||||
|
||||
@@ -290,7 +290,7 @@ impl Sidebar {
|
||||
match task.await {
|
||||
Ok((profile, room)) => {
|
||||
this.update(cx, |this, cx| {
|
||||
let chats = ChatRegistry::global(cx);
|
||||
let chats = Registry::global(cx);
|
||||
let result = chats
|
||||
.read(cx)
|
||||
.search_by_public_key(profile.public_key(), cx);
|
||||
@@ -343,7 +343,7 @@ impl Sidebar {
|
||||
return;
|
||||
};
|
||||
|
||||
let chats = ChatRegistry::global(cx);
|
||||
let chats = Registry::global(cx);
|
||||
let result = chats.read(cx).search(&query, cx);
|
||||
|
||||
if result.is_empty() {
|
||||
@@ -426,7 +426,7 @@ impl Sidebar {
|
||||
}
|
||||
|
||||
fn open_room(&mut self, id: u64, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let room = if let Some(room) = ChatRegistry::get_global(cx).room(&id, cx) {
|
||||
let room = if let Some(room) = Registry::read_global(cx).room(&id, cx) {
|
||||
room
|
||||
} else {
|
||||
let Some(result) = self.global_result.read(cx).as_ref() else {
|
||||
@@ -445,7 +445,7 @@ impl Sidebar {
|
||||
room
|
||||
};
|
||||
|
||||
ChatRegistry::global(cx).update(cx, |this, cx| {
|
||||
Registry::global(cx).update(cx, |this, cx| {
|
||||
this.push_room(room, cx);
|
||||
});
|
||||
}
|
||||
@@ -508,15 +508,15 @@ impl Sidebar {
|
||||
.gap_2()
|
||||
.text_sm()
|
||||
.font_semibold()
|
||||
.child(Avatar::new(profile.render_avatar(proxy)).size(rems(1.75)))
|
||||
.child(profile.render_name())
|
||||
.child(Avatar::new(profile.avatar_url(proxy)).size(rems(1.75)))
|
||||
.child(profile.display_name())
|
||||
.on_click(cx.listener({
|
||||
let Ok(public_key) = profile.public_key().to_bech32();
|
||||
let item = ClipboardItem::new_string(public_key);
|
||||
|
||||
move |_, _, window, cx| {
|
||||
cx.write_to_clipboard(item.clone());
|
||||
window.push_notification("User's NPUB is copied", cx);
|
||||
window.push_notification(t!("common.copied"), cx);
|
||||
}
|
||||
})),
|
||||
)
|
||||
@@ -616,7 +616,11 @@ impl Focusable for Sidebar {
|
||||
|
||||
impl Render for Sidebar {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let chats = ChatRegistry::get_global(cx);
|
||||
let registry = Registry::read_global(cx);
|
||||
|
||||
let profile = Identity::read_global(cx)
|
||||
.public_key()
|
||||
.map(|pk| registry.get_person(&pk, cx));
|
||||
|
||||
// Get rooms from either search results or the chat registry
|
||||
let rooms = if let Some(results) = self.local_result.read(cx) {
|
||||
@@ -624,9 +628,9 @@ impl Render for Sidebar {
|
||||
} else {
|
||||
#[allow(clippy::collapsible_else_if)]
|
||||
if self.active_filter.read(cx) == &RoomKind::Ongoing {
|
||||
chats.ongoing_rooms(cx)
|
||||
registry.ongoing_rooms(cx)
|
||||
} else {
|
||||
chats.request_rooms(self.trusted_only, cx)
|
||||
registry.request_rooms(self.trusted_only, cx)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -638,7 +642,7 @@ impl Render for Sidebar {
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
// Account
|
||||
.when_some(Identity::get_global(cx).profile(), |this, profile| {
|
||||
.when_some(profile, |this, profile| {
|
||||
this.child(self.account(&profile, cx))
|
||||
})
|
||||
// Search Input
|
||||
@@ -770,7 +774,7 @@ impl Render for Sidebar {
|
||||
)
|
||||
}),
|
||||
)
|
||||
.when(chats.loading, |this| {
|
||||
.when(registry.loading, |this| {
|
||||
this.child(
|
||||
div()
|
||||
.flex_1()
|
||||
@@ -791,7 +795,7 @@ impl Render for Sidebar {
|
||||
.h_full(),
|
||||
),
|
||||
)
|
||||
.when(chats.loading, |this| {
|
||||
.when(registry.loading, |this| {
|
||||
this.child(
|
||||
div().absolute().bottom_4().px_4().child(
|
||||
div()
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use chats::ChatRegistry;
|
||||
use gpui::{
|
||||
div, App, AppContext, Context, Entity, FocusHandle, InteractiveElement, IntoElement,
|
||||
ParentElement, Render, SharedString, Styled, Window,
|
||||
};
|
||||
use i18n::t;
|
||||
use registry::Registry;
|
||||
use theme::ActiveTheme;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::input::{InputState, TextInput};
|
||||
@@ -47,7 +47,7 @@ impl Subject {
|
||||
}
|
||||
|
||||
pub fn update(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let registry = ChatRegistry::global(cx).read(cx);
|
||||
let registry = Registry::global(cx).read(cx);
|
||||
let subject = self.input.read(cx).value().clone();
|
||||
|
||||
if let Some(room) = registry.room(&self.id, cx) {
|
||||
|
||||
Reference in New Issue
Block a user