chore: improve data requests (#81)

* refactor

* refactor

* add documents

* clean up

* refactor

* clean up

* refactor identity

* .

* .

* rename
This commit is contained in:
reya
2025-07-08 15:23:35 +07:00
committed by GitHub
parent 122dbaf693
commit 8bfad30a99
32 changed files with 1478 additions and 1594 deletions

View File

@@ -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" }

View File

@@ -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,
&registry,
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()

View File

@@ -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
}

View File

@@ -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)
}))

View File

@@ -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.)),
)
}

View File

@@ -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| {

View File

@@ -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);
}
});

View File

@@ -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()),
),
),
)

View File

@@ -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()

View File

@@ -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();
}

View File

@@ -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?;

View File

@@ -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()

View File

@@ -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) {