chore: refactor the global state and improve signer (#56)
* refactor * update * . * rustfmt * . * . * . * . * . * add document * . * add logout * handle error * chore: update gpui * adjust timeout
This commit is contained in:
@@ -14,7 +14,6 @@ theme = { path = "../theme" }
|
||||
common = { path = "../common" }
|
||||
global = { path = "../global" }
|
||||
chats = { path = "../chats" }
|
||||
account = { path = "../account" }
|
||||
auto_update = { path = "../auto_update" }
|
||||
|
||||
gpui.workspace = true
|
||||
|
||||
@@ -1,29 +1,27 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use account::Account;
|
||||
use anyhow::Error;
|
||||
use chats::{ChatRegistry, RoomEmitter};
|
||||
use global::{
|
||||
constants::{DEFAULT_MODAL_WIDTH, DEFAULT_SIDEBAR_WIDTH},
|
||||
get_client,
|
||||
};
|
||||
use global::constants::{DEFAULT_MODAL_WIDTH, DEFAULT_SIDEBAR_WIDTH};
|
||||
use global::shared_state;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, impl_internal_actions, prelude::FluentBuilder, px, App, AppContext, Axis, Context, Entity,
|
||||
InteractiveElement, IntoElement, ParentElement, Render, Styled, Subscription, Task, Window,
|
||||
div, impl_internal_actions, px, App, AppContext, Axis, Context, Entity, InteractiveElement,
|
||||
IntoElement, ParentElement, Render, Styled, Subscription, Task, Window,
|
||||
};
|
||||
use nostr_sdk::prelude::*;
|
||||
use nostr_connect::prelude::*;
|
||||
use serde::Deserialize;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use theme::{ActiveTheme, Theme, ThemeMode};
|
||||
use ui::{
|
||||
button::{Button, ButtonVariants},
|
||||
dock_area::{dock::DockPlacement, panel::PanelView, DockArea, DockItem},
|
||||
ContextModal, IconName, Root, Sizable, TitleBar,
|
||||
};
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::dock::DockPlacement;
|
||||
use ui::dock_area::panel::PanelView;
|
||||
use ui::dock_area::{DockArea, DockItem};
|
||||
use ui::{ContextModal, IconName, Root, Sizable, TitleBar};
|
||||
|
||||
use crate::views::chat::{self, Chat};
|
||||
use crate::views::{
|
||||
chat::{self, Chat},
|
||||
compose, login, new_account, onboarding, profile, relays, sidebar, welcome,
|
||||
compose, login, new_account, onboarding, profile, relays, sidebar, startup, welcome,
|
||||
};
|
||||
|
||||
impl_internal_actions!(dock, [ToggleModal]);
|
||||
@@ -63,16 +61,16 @@ pub struct ToggleModal {
|
||||
}
|
||||
|
||||
pub struct ChatSpace {
|
||||
titlebar: bool,
|
||||
dock: Entity<DockArea>,
|
||||
titlebar: bool,
|
||||
#[allow(unused)]
|
||||
subscriptions: SmallVec<[Subscription; 3]>,
|
||||
subscriptions: SmallVec<[Subscription; 2]>,
|
||||
}
|
||||
|
||||
impl ChatSpace {
|
||||
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||
let dock = cx.new(|cx| {
|
||||
let panel = Arc::new(onboarding::init(window, cx));
|
||||
let panel = Arc::new(startup::init(window, cx));
|
||||
let center = DockItem::panel(panel);
|
||||
let mut dock = DockArea::new(window, cx);
|
||||
// Initialize the dock area with the center panel
|
||||
@@ -81,46 +79,39 @@ impl ChatSpace {
|
||||
});
|
||||
|
||||
cx.new(|cx| {
|
||||
let account = Account::global(cx);
|
||||
let chats = ChatRegistry::global(cx);
|
||||
let mut subscriptions = smallvec![];
|
||||
|
||||
subscriptions.push(cx.observe_in(
|
||||
&account,
|
||||
window,
|
||||
|this: &mut ChatSpace, account, window, cx| {
|
||||
if account.read(cx).profile.is_some() {
|
||||
this.open_chats(window, cx);
|
||||
} else {
|
||||
this.open_onboarding(window, cx);
|
||||
}
|
||||
},
|
||||
));
|
||||
|
||||
subscriptions.push(cx.subscribe_in(
|
||||
&chats,
|
||||
window,
|
||||
|this, _state, event, window, cx| {
|
||||
if let RoomEmitter::Open(room) = event {
|
||||
if let Some(room) = room.upgrade() {
|
||||
this.dock.update(cx, |this, cx| {
|
||||
let panel = chat::init(room, window, cx);
|
||||
this.add_panel(panel, DockPlacement::Center, window, cx);
|
||||
});
|
||||
} else {
|
||||
window
|
||||
.push_notification("Failed to open room. Please retry later.", cx);
|
||||
}
|
||||
}
|
||||
},
|
||||
));
|
||||
|
||||
subscriptions.push(cx.observe_new::<Chat>(|this, window, cx| {
|
||||
// Automatically load messages when chat panel opens
|
||||
subscriptions.push(cx.observe_new::<Chat>(|this: &mut Chat, window, cx| {
|
||||
if let Some(window) = window {
|
||||
this.load_messages(window, cx);
|
||||
}
|
||||
}));
|
||||
|
||||
// Subscribe to open chat room requests
|
||||
subscriptions.push(cx.subscribe_in(
|
||||
&chats,
|
||||
window,
|
||||
|this: &mut ChatSpace, _state, event, window, cx| {
|
||||
if let RoomEmitter::Open(room) = event {
|
||||
if let Some(room) = room.upgrade() {
|
||||
this.dock.update(cx, |this, cx| {
|
||||
let panel = chat::init(room, window, cx);
|
||||
let placement = DockPlacement::Center;
|
||||
|
||||
this.add_panel(panel, placement, window, cx);
|
||||
});
|
||||
} else {
|
||||
window.push_notification(
|
||||
"Failed to open room. Please try again later.",
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
));
|
||||
|
||||
Self {
|
||||
dock,
|
||||
subscriptions,
|
||||
@@ -129,12 +120,10 @@ impl ChatSpace {
|
||||
})
|
||||
}
|
||||
|
||||
fn show_titlebar(&mut self, cx: &mut Context<Self>) {
|
||||
self.titlebar = true;
|
||||
cx.notify();
|
||||
}
|
||||
pub fn open_onboarding(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
// Disable the titlebar
|
||||
self.titlebar(false, cx);
|
||||
|
||||
fn open_onboarding(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let panel = Arc::new(onboarding::init(window, cx));
|
||||
let center = DockItem::panel(panel);
|
||||
|
||||
@@ -144,8 +133,9 @@ impl ChatSpace {
|
||||
});
|
||||
}
|
||||
|
||||
fn open_chats(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.show_titlebar(cx);
|
||||
pub fn open_chats(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
// Enable the titlebar
|
||||
self.titlebar(true, cx);
|
||||
|
||||
let weak_dock = self.dock.downgrade();
|
||||
let left = DockItem::panel(Arc::new(sidebar::init(window, cx)));
|
||||
@@ -191,20 +181,28 @@ impl ChatSpace {
|
||||
});
|
||||
}
|
||||
|
||||
fn titlebar(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||
self.titlebar = status;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn verify_messaging_relays(&self, cx: &App) -> Task<Result<bool, Error>> {
|
||||
cx.background_spawn(async move {
|
||||
let client = get_client();
|
||||
let signer = client.signer().await?;
|
||||
let signer = shared_state().client.signer().await?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::InboxRelays)
|
||||
.author(public_key)
|
||||
.limit(1);
|
||||
let is_exist = shared_state()
|
||||
.client
|
||||
.database()
|
||||
.query(filter)
|
||||
.await?
|
||||
.first()
|
||||
.is_some();
|
||||
|
||||
let exist = client.database().query(filter).await?.first().is_some();
|
||||
|
||||
Ok(exist)
|
||||
Ok(is_exist)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -298,11 +296,12 @@ impl Render for ChatSpace {
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_end()
|
||||
.gap_2()
|
||||
.gap_1p5()
|
||||
.px_2()
|
||||
.child(
|
||||
Button::new("appearance")
|
||||
.xsmall()
|
||||
.tooltip("Change the app's appearance")
|
||||
.small()
|
||||
.ghost()
|
||||
.map(|this| {
|
||||
if cx.theme().mode.is_dark() {
|
||||
@@ -326,6 +325,26 @@ impl Render for ChatSpace {
|
||||
);
|
||||
}
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("settings")
|
||||
.tooltip("Open settings")
|
||||
.small()
|
||||
.ghost()
|
||||
.icon(IconName::Settings),
|
||||
)
|
||||
.child(
|
||||
Button::new("logout")
|
||||
.tooltip("Log out")
|
||||
.small()
|
||||
.ghost()
|
||||
.icon(IconName::Logout)
|
||||
.on_click(cx.listener(move |_, _, _window, cx| {
|
||||
cx.background_spawn(async move {
|
||||
shared_state().unset_signer().await;
|
||||
})
|
||||
.detach();
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
use anyhow::{anyhow, Error};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Error;
|
||||
use asset::Assets;
|
||||
use auto_update::AutoUpdater;
|
||||
use chats::ChatRegistry;
|
||||
use futures::{select, FutureExt};
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
use global::constants::APP_NAME;
|
||||
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,
|
||||
},
|
||||
get_client, init_global_state, profiles,
|
||||
};
|
||||
use global::constants::{APP_ID, KEYRING_BUNKER, KEYRING_USER_PATH};
|
||||
use global::{shared_state, NostrSignal};
|
||||
use gpui::{
|
||||
actions, px, size, App, AppContext, Application, Bounds, KeyBinding, Menu, MenuItem,
|
||||
WindowBounds, WindowKind, WindowOptions,
|
||||
@@ -20,13 +17,7 @@ use gpui::{
|
||||
use gpui::{point, SharedString, TitlebarOptions};
|
||||
#[cfg(target_os = "linux")]
|
||||
use gpui::{WindowBackgroundAppearance, WindowDecorations};
|
||||
use nostr_sdk::{
|
||||
async_utility::task::spawn, nips::nip01::Coordinate, pool::prelude::ReqExitPolicy, Client,
|
||||
Event, EventBuilder, EventId, Filter, JsonUtil, Keys, Kind, Metadata, PublicKey, RelayMessage,
|
||||
RelayPoolNotification, SubscribeAutoCloseOptions, SubscriptionId, Tag,
|
||||
};
|
||||
use smol::Timer;
|
||||
use std::{collections::HashSet, mem, sync::Arc, time::Duration};
|
||||
use nostr_connect::prelude::*;
|
||||
use theme::Theme;
|
||||
use ui::Root;
|
||||
|
||||
@@ -36,226 +27,27 @@ pub(crate) mod views;
|
||||
|
||||
actions!(coop, [Quit]);
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Signal {
|
||||
/// Receive event
|
||||
Event(Event),
|
||||
/// Receive eose
|
||||
Eose,
|
||||
/// Receive app updates
|
||||
AppUpdates(Event),
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Initialize logging
|
||||
tracing_subscriber::fmt::init();
|
||||
// Initialize global state
|
||||
init_global_state();
|
||||
|
||||
let (event_tx, event_rx) = smol::channel::bounded::<Signal>(2048);
|
||||
let (batch_tx, batch_rx) = smol::channel::bounded::<Vec<PublicKey>>(500);
|
||||
|
||||
let client = get_client();
|
||||
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||
|
||||
// Spawn a task to establish relay connections
|
||||
// NOTE: Use `async_utility` instead of `smol-rs`
|
||||
spawn(async move {
|
||||
for relay in BOOTSTRAP_RELAYS.into_iter() {
|
||||
if let Err(e) = client.add_relay(relay).await {
|
||||
log::error!("Failed to add relay {}: {}", relay, e);
|
||||
}
|
||||
}
|
||||
|
||||
for relay in SEARCH_RELAYS.into_iter() {
|
||||
if let Err(e) = client.add_relay(relay).await {
|
||||
log::error!("Failed to add relay {}: {}", relay, e);
|
||||
}
|
||||
}
|
||||
|
||||
// Establish connection to bootstrap relays
|
||||
client.connect().await;
|
||||
|
||||
log::info!("Connected to bootstrap relays");
|
||||
log::info!("Subscribing to app updates...");
|
||||
|
||||
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);
|
||||
|
||||
if let Err(e) = client
|
||||
.subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts))
|
||||
.await
|
||||
{
|
||||
log::error!("Failed to subscribe for app updates: {}", e);
|
||||
}
|
||||
// Initialize the Global State and process events in a separate thread.
|
||||
// Must be run under async utility runtime
|
||||
nostr_sdk::async_utility::task::spawn(async move {
|
||||
shared_state().start().await;
|
||||
});
|
||||
|
||||
// Spawn a task to handle metadata batching
|
||||
// NOTE: Use `async_utility` instead of `smol-rs`
|
||||
spawn(async move {
|
||||
let mut batch: HashSet<PublicKey> = HashSet::new();
|
||||
|
||||
loop {
|
||||
let mut timeout =
|
||||
Box::pin(Timer::after(Duration::from_millis(METADATA_BATCH_TIMEOUT)).fuse());
|
||||
|
||||
select! {
|
||||
pubkeys = batch_rx.recv().fuse() => {
|
||||
match pubkeys {
|
||||
Ok(keys) => {
|
||||
batch.extend(keys);
|
||||
if batch.len() >= METADATA_BATCH_LIMIT {
|
||||
sync_metadata(mem::take(&mut batch), client, opts).await;
|
||||
}
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
_ = timeout => {
|
||||
if !batch.is_empty() {
|
||||
sync_metadata(mem::take(&mut batch), client, opts).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Spawn a task to handle relay pool notification
|
||||
// NOTE: Use `async_utility` instead of `smol-rs`
|
||||
spawn(async move {
|
||||
let keys = Keys::generate();
|
||||
let all_id = SubscriptionId::new(ALL_MESSAGES_SUB_ID);
|
||||
let new_id = SubscriptionId::new(NEW_MESSAGE_SUB_ID);
|
||||
let mut notifications = client.notifications();
|
||||
|
||||
let mut processed_events: HashSet<EventId> = HashSet::new();
|
||||
|
||||
while let Ok(notification) = notifications.recv().await {
|
||||
if let RelayPoolNotification::Message { message, .. } = notification {
|
||||
match message {
|
||||
RelayMessage::Event {
|
||||
event,
|
||||
subscription_id,
|
||||
} => {
|
||||
if processed_events.contains(&event.id) {
|
||||
continue;
|
||||
}
|
||||
processed_events.insert(event.id);
|
||||
|
||||
match event.kind {
|
||||
Kind::GiftWrap => {
|
||||
let event = match get_unwrapped(event.id).await {
|
||||
Ok(event) => event,
|
||||
Err(_) => match client.unwrap_gift_wrap(&event).await {
|
||||
Ok(unwrap) => match unwrap.rumor.sign_with_keys(&keys) {
|
||||
Ok(unwrapped) => {
|
||||
set_unwrapped(event.id, &unwrapped, &keys)
|
||||
.await
|
||||
.ok();
|
||||
unwrapped
|
||||
}
|
||||
Err(_) => continue,
|
||||
},
|
||||
Err(_) => continue,
|
||||
},
|
||||
};
|
||||
|
||||
let mut pubkeys = vec![];
|
||||
pubkeys.extend(event.tags.public_keys());
|
||||
pubkeys.push(event.pubkey);
|
||||
|
||||
// Send all pubkeys to the batch to sync metadata
|
||||
batch_tx.send(pubkeys).await.ok();
|
||||
|
||||
// Save the event to the database, use for query directly.
|
||||
client.database().save_event(&event).await.ok();
|
||||
|
||||
// Send this event to the GPUI
|
||||
if new_id == *subscription_id {
|
||||
event_tx.send(Signal::Event(event)).await.ok();
|
||||
}
|
||||
}
|
||||
Kind::Metadata => {
|
||||
let metadata = Metadata::from_json(&event.content).ok();
|
||||
|
||||
profiles()
|
||||
.write()
|
||||
.await
|
||||
.entry(event.pubkey)
|
||||
.and_modify(|entry| {
|
||||
if entry.is_none() {
|
||||
*entry = metadata.clone();
|
||||
}
|
||||
})
|
||||
.or_insert_with(|| metadata);
|
||||
}
|
||||
Kind::ContactList => {
|
||||
if let Ok(signer) = client.signer().await {
|
||||
if let Ok(public_key) = signer.get_public_key().await {
|
||||
if public_key == event.pubkey {
|
||||
let pubkeys = event
|
||||
.tags
|
||||
.public_keys()
|
||||
.copied()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
batch_tx.send(pubkeys).await.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Kind::ReleaseArtifactSet => {
|
||||
let filter = Filter::new()
|
||||
.ids(event.tags.event_ids().copied())
|
||||
.kind(Kind::FileMetadata);
|
||||
|
||||
if let Err(e) = client
|
||||
.subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts))
|
||||
.await
|
||||
{
|
||||
log::error!("Failed to subscribe for file metadata: {}", e);
|
||||
} else {
|
||||
event_tx
|
||||
.send(Signal::AppUpdates(event.into_owned()))
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
RelayMessage::EndOfStoredEvents(subscription_id) => {
|
||||
if all_id == *subscription_id {
|
||||
event_tx.send(Signal::Eose).await.ok();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize application
|
||||
// Initialize the Application
|
||||
let app = Application::new()
|
||||
.with_assets(Assets)
|
||||
.with_http_client(Arc::new(reqwest_client::ReqwestClient::new()));
|
||||
|
||||
app.run(move |cx| {
|
||||
// Bring the app to the foreground
|
||||
cx.activate(true);
|
||||
|
||||
// Register the `quit` function
|
||||
cx.on_action(quit);
|
||||
|
||||
// Register the `quit` function with CMD+Q
|
||||
// Register the `quit` function with CMD+Q (macOS only)
|
||||
#[cfg(target_os = "macos")]
|
||||
cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);
|
||||
|
||||
// Set menu items
|
||||
@@ -297,37 +89,87 @@ fn main() {
|
||||
|
||||
// Root Entity
|
||||
cx.new(|cx| {
|
||||
cx.activate(true);
|
||||
// Initialize components
|
||||
ui::init(cx);
|
||||
|
||||
// Initialize auto update
|
||||
auto_update::init(cx);
|
||||
|
||||
// Initialize chat state
|
||||
chats::init(cx);
|
||||
|
||||
// Initialize account state
|
||||
account::init(cx);
|
||||
// Initialize chatspace (or workspace)
|
||||
let chatspace = chatspace::init(window, cx);
|
||||
let async_chatspace = chatspace.downgrade();
|
||||
let async_chatspace_clone = async_chatspace.clone();
|
||||
|
||||
// Read user's credential
|
||||
let read_credential = cx.read_credentials(KEYRING_USER_PATH);
|
||||
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
if let Ok(Some((user, secret))) = read_credential.await {
|
||||
cx.update(|window, cx| {
|
||||
if let Ok(signer) = extract_credential(&user, secret) {
|
||||
cx.background_spawn(async move {
|
||||
if let Err(e) = shared_state().set_signer(signer).await {
|
||||
log::error!("Signer error: {}", e);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
} else {
|
||||
async_chatspace
|
||||
.update(cx, |this, cx| {
|
||||
this.open_onboarding(window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
} else {
|
||||
cx.update(|window, cx| {
|
||||
async_chatspace
|
||||
.update(cx, |this, cx| {
|
||||
this.open_onboarding(window, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
// Spawn a task to handle events from nostr channel
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
while let Ok(signal) = event_rx.recv().await {
|
||||
while let Ok(signal) = shared_state().global_receiver.recv().await {
|
||||
cx.update(|window, cx| {
|
||||
let chats = ChatRegistry::global(cx);
|
||||
let auto_updater = AutoUpdater::global(cx);
|
||||
|
||||
match signal {
|
||||
Signal::Eose => {
|
||||
NostrSignal::SignerUpdated => {
|
||||
async_chatspace_clone
|
||||
.update(cx, |this, cx| {
|
||||
this.open_chats(window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
NostrSignal::SignerUnset => {
|
||||
async_chatspace_clone
|
||||
.update(cx, |this, cx| {
|
||||
this.open_onboarding(window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
NostrSignal::Eose => {
|
||||
chats.update(cx, |this, cx| {
|
||||
this.load_rooms(window, cx);
|
||||
});
|
||||
}
|
||||
Signal::Event(event) => {
|
||||
NostrSignal::Event(event) => {
|
||||
chats.update(cx, |this, cx| {
|
||||
this.event_to_message(event, window, cx);
|
||||
});
|
||||
}
|
||||
Signal::AppUpdates(event) => {
|
||||
NostrSignal::AppUpdate(event) => {
|
||||
auto_updater.update(cx, |this, cx| {
|
||||
this.update(event, cx);
|
||||
});
|
||||
@@ -339,62 +181,26 @@ fn main() {
|
||||
})
|
||||
.detach();
|
||||
|
||||
Root::new(chatspace::init(window, cx).into(), window, cx)
|
||||
Root::new(chatspace.into(), window, cx)
|
||||
})
|
||||
})
|
||||
.expect("Failed to open window. Please restart the application.");
|
||||
});
|
||||
}
|
||||
|
||||
async fn set_unwrapped(root: EventId, event: &Event, keys: &Keys) -> Result<(), Error> {
|
||||
let client = get_client();
|
||||
let event = EventBuilder::new(Kind::Custom(9001), event.as_json())
|
||||
.tags(vec![Tag::event(root)])
|
||||
.sign(keys) // keys must be random generated
|
||||
.await?;
|
||||
fn extract_credential(user: &str, secret: Vec<u8>) -> Result<impl NostrSigner, Error> {
|
||||
if user == KEYRING_BUNKER {
|
||||
let value = String::from_utf8(secret)?;
|
||||
let uri = NostrConnectURI::parse(value)?;
|
||||
let client_keys = shared_state().client_signer.clone();
|
||||
let signer = NostrConnect::new(uri, client_keys, Duration::from_secs(300), None)?;
|
||||
|
||||
client.database().save_event(&event).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_unwrapped(gift_wrap: EventId) -> Result<Event, Error> {
|
||||
let client = get_client();
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::Custom(9001))
|
||||
.event(gift_wrap)
|
||||
.limit(1);
|
||||
|
||||
if let Some(event) = client.database().query(filter).await?.first_owned() {
|
||||
let parsed = Event::from_json(event.content)?;
|
||||
Ok(parsed)
|
||||
Ok(signer.into_nostr_signer())
|
||||
} else {
|
||||
Err(anyhow!("Event not found"))
|
||||
}
|
||||
}
|
||||
let secret_key = SecretKey::from_slice(&secret)?;
|
||||
let keys = Keys::new(secret_key);
|
||||
|
||||
async fn sync_metadata(
|
||||
buffer: HashSet<PublicKey>,
|
||||
client: &Client,
|
||||
opts: SubscribeAutoCloseOptions,
|
||||
) {
|
||||
let kinds = vec![
|
||||
Kind::Metadata,
|
||||
Kind::ContactList,
|
||||
Kind::InboxRelays,
|
||||
Kind::UserStatus,
|
||||
];
|
||||
|
||||
let filter = Filter::new()
|
||||
.authors(buffer.iter().cloned())
|
||||
.limit(buffer.len() * kinds.len())
|
||||
.kinds(kinds);
|
||||
|
||||
if let Err(e) = client
|
||||
.subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts))
|
||||
.await
|
||||
{
|
||||
log::error!("Failed to sync metadata: {e}");
|
||||
Ok(keys.into_nostr_signer())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Arc};
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use account::Account;
|
||||
use async_utility::task::spawn;
|
||||
use chats::{
|
||||
message::Message,
|
||||
room::{Room, RoomKind, SendError},
|
||||
};
|
||||
use common::{nip96_upload, profile::RenderProfile};
|
||||
use global::get_client;
|
||||
use chats::message::Message;
|
||||
use chats::room::{Room, RoomKind, SendError};
|
||||
use common::nip96_upload;
|
||||
use common::profile::RenderProfile;
|
||||
use global::shared_state;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, img, impl_internal_actions, list, prelude::FluentBuilder, px, red, relative, rems, svg,
|
||||
white, 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,
|
||||
div, img, impl_internal_actions, list, px, red, relative, rems, svg, white, 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,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use nostr_sdk::prelude::*;
|
||||
@@ -21,15 +23,15 @@ use serde::Deserialize;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use smol::fs;
|
||||
use theme::ActiveTheme;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::emoji_picker::EmojiPicker;
|
||||
use ui::input::{InputEvent, InputState, TextInput};
|
||||
use ui::notification::Notification;
|
||||
use ui::popup_menu::PopupMenu;
|
||||
use ui::text::RichText;
|
||||
use ui::{
|
||||
avatar::Avatar,
|
||||
button::{Button, ButtonVariants},
|
||||
dock_area::panel::{Panel, PanelEvent},
|
||||
emoji_picker::EmojiPicker,
|
||||
input::{InputEvent, InputState, TextInput},
|
||||
notification::Notification,
|
||||
popup_menu::PopupMenu,
|
||||
text::RichText,
|
||||
v_flex, ContextModal, Disableable, Icon, IconName, InteractiveElementExt, Sizable, StyledExt,
|
||||
};
|
||||
|
||||
@@ -217,7 +219,7 @@ 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(current_user) = Account::get_global(cx).profile_ref() else {
|
||||
let Some(account) = shared_state().identity() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -225,7 +227,7 @@ impl Chat {
|
||||
return false;
|
||||
};
|
||||
|
||||
if current_user.public_key() != author.public_key() {
|
||||
if account.public_key() != author.public_key() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -238,7 +240,7 @@ impl Chat {
|
||||
m.borrow()
|
||||
.author
|
||||
.as_ref()
|
||||
.is_some_and(|p| p.public_key() == current_user.public_key())
|
||||
.is_some_and(|p| p.public_key() == account.public_key())
|
||||
})
|
||||
.any(|existing| {
|
||||
let existing = existing.borrow();
|
||||
@@ -383,12 +385,11 @@ impl Chat {
|
||||
};
|
||||
|
||||
if let Ok(file_data) = fs::read(path).await {
|
||||
let client = get_client();
|
||||
let (tx, rx) = oneshot::channel::<Option<Url>>();
|
||||
|
||||
// Spawn task via async utility instead of GPUI context
|
||||
spawn(async move {
|
||||
let url = match nip96_upload(client, file_data).await {
|
||||
let url = match nip96_upload(&shared_state().client, file_data).await {
|
||||
Ok(url) => Some(url),
|
||||
Err(e) => {
|
||||
log::error!("Upload error: {e}");
|
||||
|
||||
@@ -1,28 +1,25 @@
|
||||
use std::{
|
||||
collections::{BTreeSet, HashSet},
|
||||
time::Duration,
|
||||
};
|
||||
use std::collections::{BTreeSet, HashSet};
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Error;
|
||||
use chats::{room::Room, ChatRegistry};
|
||||
use chats::room::Room;
|
||||
use chats::ChatRegistry;
|
||||
use common::profile::RenderProfile;
|
||||
use global::get_client;
|
||||
use global::shared_state;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, img, impl_internal_actions, prelude::FluentBuilder, px, red, relative, uniform_list, App,
|
||||
AppContext, Context, Entity, FocusHandle, InteractiveElement, IntoElement, ParentElement,
|
||||
Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, TextAlign,
|
||||
Window,
|
||||
div, img, impl_internal_actions, px, red, relative, uniform_list, App, AppContext, Context,
|
||||
Entity, FocusHandle, InteractiveElement, IntoElement, ParentElement, Render, SharedString,
|
||||
StatefulInteractiveElement, Styled, Subscription, Task, TextAlign, Window,
|
||||
};
|
||||
use nostr_sdk::prelude::*;
|
||||
use serde::Deserialize;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use smol::Timer;
|
||||
use theme::ActiveTheme;
|
||||
use ui::{
|
||||
button::{Button, ButtonVariants},
|
||||
input::{InputEvent, InputState, TextInput},
|
||||
ContextModal, Disableable, Icon, IconName, Sizable, StyledExt,
|
||||
};
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::input::{InputEvent, InputState, TextInput};
|
||||
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))
|
||||
@@ -71,10 +68,13 @@ impl Compose {
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let task: Task<Result<BTreeSet<Profile>, Error>> = cx.background_spawn(async move {
|
||||
let client = get_client();
|
||||
let signer = client.signer().await?;
|
||||
let signer = shared_state().client.signer().await?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
let profiles = client.database().contacts(public_key).await?;
|
||||
let profiles = shared_state()
|
||||
.client
|
||||
.database()
|
||||
.contacts(public_key)
|
||||
.await?;
|
||||
|
||||
Ok(profiles)
|
||||
});
|
||||
@@ -133,8 +133,7 @@ impl Compose {
|
||||
let tags = Tags::from_list(tag_list);
|
||||
|
||||
let event: Task<Result<Event, anyhow::Error>> = cx.background_spawn(async move {
|
||||
let client = get_client();
|
||||
let signer = client.signer().await?;
|
||||
let signer = shared_state().client.signer().await?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
|
||||
// [IMPORTANT]
|
||||
@@ -173,7 +172,6 @@ impl Compose {
|
||||
}
|
||||
|
||||
fn add(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let client = get_client();
|
||||
let content = self.user_input.read(cx).value().to_string();
|
||||
|
||||
// Show loading spinner
|
||||
@@ -184,7 +182,8 @@ impl Compose {
|
||||
let profile = nip05::profile(&content, None).await?;
|
||||
let public_key = profile.public_key;
|
||||
|
||||
let metadata = client
|
||||
let metadata = shared_state()
|
||||
.client
|
||||
.fetch_metadata(public_key, Duration::from_secs(2))
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
@@ -199,7 +198,8 @@ impl Compose {
|
||||
};
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let metadata = client
|
||||
let metadata = shared_state()
|
||||
.client
|
||||
.fetch_metadata(public_key, Duration::from_secs(2))
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
|
||||
@@ -1,24 +1,27 @@
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use account::Account;
|
||||
use common::string_to_qr;
|
||||
use global::get_client_keys;
|
||||
use global::constants::{APP_NAME, KEYRING_BUNKER, KEYRING_USER_PATH};
|
||||
use global::shared_state;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, img, prelude::FluentBuilder, red, relative, AnyElement, App, AppContext, Context, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, Image, IntoElement, ParentElement, Render, SharedString,
|
||||
Styled, Subscription, Window,
|
||||
div, img, red, relative, AnyElement, App, AppContext, ClipboardItem, Context, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, Image, InteractiveElement, IntoElement, ParentElement,
|
||||
Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Window,
|
||||
};
|
||||
use nostr_connect::prelude::*;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{
|
||||
button::{Button, ButtonVariants},
|
||||
dock_area::panel::{Panel, PanelEvent},
|
||||
input::{InputEvent, InputState, TextInput},
|
||||
notification::Notification,
|
||||
popup_menu::PopupMenu,
|
||||
ContextModal, Disableable, Sizable, StyledExt,
|
||||
};
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::input::{InputEvent, InputState, TextInput};
|
||||
use ui::notification::Notification;
|
||||
use ui::popup_menu::PopupMenu;
|
||||
use ui::{ContextModal, Disableable, Sizable, StyledExt};
|
||||
|
||||
const NOSTR_CONNECT_RELAY: &str = "wss://relay.nsec.app";
|
||||
const NOSTR_CONNECT_TIMEOUT: u64 = 300;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct CoopAuthUrlHandler;
|
||||
@@ -37,23 +40,20 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity<Login> {
|
||||
}
|
||||
|
||||
pub struct Login {
|
||||
// Inputs
|
||||
key_input: Entity<InputState>,
|
||||
relay_input: Entity<InputState>,
|
||||
connection_string: Entity<NostrConnectURI>,
|
||||
qr_image: Entity<Option<Arc<Image>>>,
|
||||
// Signer that created by Connection String
|
||||
active_signer: Entity<Option<NostrConnect>>,
|
||||
// Error for the key input
|
||||
error: Entity<Option<SharedString>>,
|
||||
is_logging_in: bool,
|
||||
// Nostr Connect
|
||||
qr: Entity<Option<Arc<Image>>>,
|
||||
connect_relay: Entity<InputState>,
|
||||
connect_client: Entity<Option<NostrConnectURI>>,
|
||||
// Keep track of all signers created by nostr connect
|
||||
signers: SmallVec<[NostrConnect; 3]>,
|
||||
// Panel
|
||||
name: SharedString,
|
||||
closable: bool,
|
||||
zoomable: bool,
|
||||
focus_handle: FocusHandle,
|
||||
#[allow(unused)]
|
||||
subscriptions: SmallVec<[Subscription; 4]>,
|
||||
subscriptions: SmallVec<[Subscription; 5]>,
|
||||
}
|
||||
|
||||
impl Login {
|
||||
@@ -62,106 +62,159 @@ impl Login {
|
||||
}
|
||||
|
||||
fn view(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let connect_client: Entity<Option<NostrConnectURI>> = cx.new(|_| None);
|
||||
let error = cx.new(|_| None);
|
||||
let qr = cx.new(|_| None);
|
||||
|
||||
// nsec or bunker_uri (NIP46: https://github.com/nostr-protocol/nips/blob/master/46.md)
|
||||
let key_input =
|
||||
cx.new(|cx| InputState::new(window, cx).placeholder("nsec... or bunker://..."));
|
||||
let connect_relay =
|
||||
cx.new(|cx| InputState::new(window, cx).default_value("wss://relay.nsec.app"));
|
||||
|
||||
let signers = smallvec![];
|
||||
let relay_input =
|
||||
cx.new(|cx| InputState::new(window, cx).default_value(NOSTR_CONNECT_RELAY));
|
||||
|
||||
// NIP46: https://github.com/nostr-protocol/nips/blob/master/46.md
|
||||
//
|
||||
// Direct connection initiated by the client
|
||||
let connection_string = cx.new(|_cx| {
|
||||
let relay = RelayUrl::parse(NOSTR_CONNECT_RELAY).unwrap();
|
||||
let client_keys = shared_state().client_signer.clone();
|
||||
|
||||
NostrConnectURI::client(client_keys.public_key(), vec![relay], APP_NAME)
|
||||
});
|
||||
|
||||
// Convert the Connection String into QR Image
|
||||
let qr_image = cx.new(|_| None);
|
||||
let async_qr_image = qr_image.downgrade();
|
||||
|
||||
// Keep track of the signer that created by Connection String
|
||||
let active_signer = cx.new(|_| None);
|
||||
let async_active_signer = active_signer.downgrade();
|
||||
|
||||
let error = cx.new(|_| None);
|
||||
let mut subscriptions = smallvec![];
|
||||
|
||||
subscriptions.push(cx.subscribe_in(
|
||||
&key_input,
|
||||
window,
|
||||
move |this, _, event, window, cx| {
|
||||
subscriptions.push(
|
||||
cx.subscribe_in(&key_input, window, |this, _, event, window, cx| {
|
||||
if let InputEvent::PressEnter { .. } = event {
|
||||
this.login(window, cx);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
subscriptions.push(
|
||||
cx.subscribe_in(&relay_input, window, |this, _, event, window, cx| {
|
||||
if let InputEvent::PressEnter { .. } = event {
|
||||
this.change_relay(window, cx);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
subscriptions.push(cx.observe_new::<NostrConnectURI>(
|
||||
move |connection_string, _window, cx| {
|
||||
if let Ok(mut signer) = NostrConnect::new(
|
||||
connection_string.to_owned(),
|
||||
shared_state().client_signer.clone(),
|
||||
Duration::from_secs(NOSTR_CONNECT_TIMEOUT),
|
||||
None,
|
||||
) {
|
||||
// Automatically open remote signer's webpage when received auth url
|
||||
signer.auth_url_handler(CoopAuthUrlHandler);
|
||||
|
||||
async_active_signer
|
||||
.update(cx, |this, cx| {
|
||||
*this = Some(signer);
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
// Update the QR Image with the new connection string
|
||||
async_qr_image
|
||||
.update(cx, |this, cx| {
|
||||
*this = string_to_qr(&connection_string.to_string());
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
},
|
||||
));
|
||||
|
||||
subscriptions.push(cx.subscribe_in(
|
||||
&connect_relay,
|
||||
subscriptions.push(cx.observe_in(
|
||||
&connection_string,
|
||||
window,
|
||||
move |this, _, event, window, cx| {
|
||||
if let InputEvent::PressEnter { .. } = event {
|
||||
this.change_relay(window, cx);
|
||||
|this, entity, _window, cx| {
|
||||
let connection_string = entity.read(cx).clone();
|
||||
let client_keys = shared_state().client_signer.clone();
|
||||
|
||||
// Update the QR Image with the new connection string
|
||||
this.qr_image.update(cx, |this, cx| {
|
||||
*this = string_to_qr(&connection_string.to_string());
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
if let Ok(mut signer) = NostrConnect::new(
|
||||
connection_string,
|
||||
client_keys,
|
||||
Duration::from_secs(NOSTR_CONNECT_TIMEOUT),
|
||||
None,
|
||||
) {
|
||||
// Automatically open remote signer's webpage when received auth url
|
||||
signer.auth_url_handler(CoopAuthUrlHandler);
|
||||
|
||||
this.active_signer.update(cx, |this, cx| {
|
||||
*this = Some(signer);
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
},
|
||||
));
|
||||
|
||||
subscriptions.push(
|
||||
cx.observe_in(&connect_client, window, |this, uri, window, cx| {
|
||||
let keys = get_client_keys().to_owned();
|
||||
cx.observe_in(&active_signer, window, |_this, entity, window, cx| {
|
||||
if let Some(signer) = entity.read(cx).clone() {
|
||||
let (tx, rx) = oneshot::channel::<Option<NostrConnectURI>>();
|
||||
|
||||
if let Some(uri) = uri.read(cx).clone() {
|
||||
if let Ok(qr) = string_to_qr(uri.to_string().as_str()) {
|
||||
this.qr.update(cx, |this, cx| {
|
||||
*this = Some(qr);
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
cx.background_spawn(async move {
|
||||
if let Ok(bunker_uri) = signer.bunker_uri().await {
|
||||
tx.send(Some(bunker_uri)).ok();
|
||||
|
||||
// Shutdown all previous nostr connect clients
|
||||
for client in std::mem::take(&mut this.signers).into_iter() {
|
||||
cx.background_spawn(async move {
|
||||
client.shutdown().await;
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
// Create a new nostr connect client
|
||||
match NostrConnect::new(uri, keys, Duration::from_secs(200), None) {
|
||||
Ok(mut signer) => {
|
||||
// Handle auth url
|
||||
signer.auth_url_handler(CoopAuthUrlHandler);
|
||||
// Store this signer for further clean up
|
||||
this.signers.push(signer.clone());
|
||||
|
||||
Account::global(cx).update(cx, |this, cx| {
|
||||
this.login(signer, window, cx);
|
||||
});
|
||||
if let Err(e) = shared_state().set_signer(signer).await {
|
||||
log::error!("{}", e);
|
||||
}
|
||||
} else {
|
||||
tx.send(None).ok();
|
||||
}
|
||||
Err(e) => {
|
||||
window.push_notification(Notification::error(e.to_string()), cx);
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
if let Ok(Some(uri)) = rx.await {
|
||||
this.update(cx, |this, cx| {
|
||||
this.save_bunker(&uri, cx);
|
||||
})
|
||||
.ok();
|
||||
} else {
|
||||
cx.update(|window, cx| {
|
||||
window.push_notification(
|
||||
Notification::error("Connection failed"),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(300))
|
||||
.await;
|
||||
|
||||
cx.update(|window, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.change_relay(window, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
key_input,
|
||||
connect_relay,
|
||||
connect_client,
|
||||
subscriptions,
|
||||
signers,
|
||||
error,
|
||||
qr,
|
||||
is_logging_in: false,
|
||||
name: "Login".into(),
|
||||
closable: true,
|
||||
zoomable: true,
|
||||
focus_handle: cx.focus_handle(),
|
||||
is_logging_in: false,
|
||||
key_input,
|
||||
relay_input,
|
||||
connection_string,
|
||||
qr_image,
|
||||
error,
|
||||
active_signer,
|
||||
subscriptions,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,63 +222,169 @@ impl Login {
|
||||
if self.is_logging_in {
|
||||
return;
|
||||
};
|
||||
|
||||
self.set_logging_in(true, cx);
|
||||
|
||||
let client_keys = shared_state().client_signer.clone();
|
||||
let content = self.key_input.read(cx).value();
|
||||
let account = Account::global(cx);
|
||||
|
||||
if content.starts_with("nsec1") {
|
||||
match SecretKey::parse(content.as_ref()) {
|
||||
Ok(secret) => {
|
||||
let keys = Keys::new(secret);
|
||||
let Ok(keys) = SecretKey::parse(content.as_ref()).map(Keys::new) else {
|
||||
self.set_error("Secret key is not valid", cx);
|
||||
return;
|
||||
};
|
||||
|
||||
account.update(cx, |this, cx| {
|
||||
this.login(keys, window, cx);
|
||||
});
|
||||
// Active signer is no longer needed
|
||||
self.shutdown_active_signer(cx);
|
||||
|
||||
// Save these keys to the OS storage for further logins
|
||||
self.save_keys(&keys, cx);
|
||||
|
||||
// Set signer with this keys in the background
|
||||
cx.background_spawn(async move {
|
||||
if let Err(e) = shared_state().set_signer(keys).await {
|
||||
log::error!("{}", e);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
} else if content.starts_with("bunker://") {
|
||||
let Ok(uri) = NostrConnectURI::parse(content.as_ref()) else {
|
||||
self.set_error("Bunker URL is not valid", cx);
|
||||
return;
|
||||
};
|
||||
|
||||
// Active signer is no longer needed
|
||||
self.shutdown_active_signer(cx);
|
||||
|
||||
match NostrConnect::new(
|
||||
uri.clone(),
|
||||
client_keys,
|
||||
Duration::from_secs(NOSTR_CONNECT_TIMEOUT / 2),
|
||||
None,
|
||||
) {
|
||||
Ok(signer) => {
|
||||
let (tx, rx) = oneshot::channel::<Option<NostrConnectURI>>();
|
||||
|
||||
// Set signer with this remote signer in the background
|
||||
cx.background_spawn(async move {
|
||||
if let Ok(bunker_uri) = signer.bunker_uri().await {
|
||||
tx.send(Some(bunker_uri)).ok();
|
||||
|
||||
if let Err(e) = shared_state().set_signer(signer).await {
|
||||
log::error!("{}", e);
|
||||
}
|
||||
} else {
|
||||
tx.send(None).ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
// Handle error
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
if let Ok(Some(uri)) = rx.await {
|
||||
this.update(cx, |this, cx| {
|
||||
this.save_bunker(&uri, cx);
|
||||
})
|
||||
.ok();
|
||||
} else {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_error(
|
||||
"Connection to the Remote Signer failed or timed out",
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
Err(e) => {
|
||||
self.set_error(e.to_string(), cx);
|
||||
}
|
||||
}
|
||||
} else if content.starts_with("bunker://") {
|
||||
let Ok(uri) = NostrConnectURI::parse(content.as_ref()) else {
|
||||
self.set_error("Bunker URL is not valid".to_owned(), cx);
|
||||
return;
|
||||
};
|
||||
|
||||
self.connect_client.update(cx, |this, cx| {
|
||||
*this = Some(uri);
|
||||
cx.notify();
|
||||
});
|
||||
} else {
|
||||
self.set_error("You must provide a valid Private Key or Bunker.".into(), cx);
|
||||
self.set_error("You must provide a valid Private Key or Bunker.", cx);
|
||||
};
|
||||
}
|
||||
|
||||
fn change_relay(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Ok(relay_url) =
|
||||
RelayUrl::parse(self.connect_relay.read(cx).value().to_string().as_str())
|
||||
let Ok(relay_url) = RelayUrl::parse(self.relay_input.read(cx).value().to_string().as_str())
|
||||
else {
|
||||
window.push_notification(Notification::error("Relay URL is not valid."), cx);
|
||||
return;
|
||||
};
|
||||
|
||||
let client_pubkey = get_client_keys().public_key();
|
||||
let uri = NostrConnectURI::client(client_pubkey, vec![relay_url], "Coop");
|
||||
let client_keys = shared_state().client_signer.clone();
|
||||
let uri = NostrConnectURI::client(client_keys.public_key(), vec![relay_url], "Coop");
|
||||
|
||||
self.connect_client.update(cx, |this, cx| {
|
||||
*this = Some(uri);
|
||||
self.connection_string.update(cx, |this, cx| {
|
||||
*this = uri;
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
|
||||
fn set_error(&mut self, message: String, cx: &mut Context<Self>) {
|
||||
fn save_keys(&self, keys: &Keys, cx: &mut Context<Self>) {
|
||||
let save_credential = cx.write_credentials(
|
||||
KEYRING_USER_PATH,
|
||||
keys.public_key().to_hex().as_str(),
|
||||
keys.secret_key().as_secret_bytes(),
|
||||
);
|
||||
|
||||
cx.background_spawn(async move {
|
||||
if let Err(e) = save_credential.await {
|
||||
log::error!("Failed to save keys: {}", e)
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn save_bunker(&self, uri: &NostrConnectURI, cx: &mut Context<Self>) {
|
||||
let mut value = uri.to_string();
|
||||
|
||||
// Remove the secret param if it exists
|
||||
if let Some(secret) = uri.secret() {
|
||||
value = value.replace(secret, "");
|
||||
}
|
||||
|
||||
let save_credential =
|
||||
cx.write_credentials(KEYRING_USER_PATH, KEYRING_BUNKER, value.as_bytes());
|
||||
|
||||
cx.background_spawn(async move {
|
||||
if let Err(e) = save_credential.await {
|
||||
log::error!("Failed to save the Bunker URI: {}", e)
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn shutdown_active_signer(&self, cx: &Context<Self>) {
|
||||
if let Some(signer) = self.active_signer.read(cx).clone() {
|
||||
cx.background_spawn(async move {
|
||||
signer.shutdown().await;
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
fn set_error(&mut self, message: impl Into<SharedString>, cx: &mut Context<Self>) {
|
||||
self.set_logging_in(false, cx);
|
||||
self.error.update(cx, |this, cx| {
|
||||
*this = Some(SharedString::new(message));
|
||||
*this = Some(message.into());
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
// Clear the error message after 3 secs
|
||||
cx.spawn(async move |this, cx| {
|
||||
cx.background_executor().timer(Duration::from_secs(3)).await;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.error.update(cx, |this, cx| {
|
||||
*this = None;
|
||||
cx.notify();
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn set_logging_in(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||
@@ -243,14 +402,6 @@ impl Panel for Login {
|
||||
self.name.clone().into_any_element()
|
||||
}
|
||||
|
||||
fn closable(&self, _cx: &App) -> bool {
|
||||
self.closable
|
||||
}
|
||||
|
||||
fn zoomable(&self, _cx: &App) -> bool {
|
||||
self.zoomable
|
||||
}
|
||||
|
||||
fn popup_menu(&self, menu: PopupMenu, _cx: &App) -> PopupMenu {
|
||||
menu.track_focus(&self.focus_handle)
|
||||
}
|
||||
@@ -365,9 +516,10 @@ impl Render for Login {
|
||||
.child("Use Nostr Connect apps to scan the code"),
|
||||
),
|
||||
)
|
||||
.when_some(self.qr.read(cx).clone(), |this, qr| {
|
||||
.when_some(self.qr_image.read(cx).clone(), |this, qr| {
|
||||
this.child(
|
||||
div()
|
||||
.id("")
|
||||
.mb_2()
|
||||
.p_2()
|
||||
.size_72()
|
||||
@@ -384,7 +536,27 @@ impl Render for Login {
|
||||
.border_color(cx.theme().border)
|
||||
})
|
||||
.bg(cx.theme().background)
|
||||
.child(img(qr).h_64()),
|
||||
.child(img(qr).h_64())
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
#[cfg(any(
|
||||
target_os = "linux",
|
||||
target_os = "freebsd"
|
||||
))]
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(
|
||||
this.connection_string.read(cx).to_string(),
|
||||
));
|
||||
#[cfg(any(
|
||||
target_os = "macos",
|
||||
target_os = "windows"
|
||||
))]
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(
|
||||
this.connection_string.read(cx).to_string(),
|
||||
));
|
||||
window.push_notification(
|
||||
"Connection String has been copied",
|
||||
cx,
|
||||
);
|
||||
})),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
@@ -394,7 +566,7 @@ impl Render for Login {
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.gap_1()
|
||||
.child(TextInput::new(&self.connect_relay).xsmall())
|
||||
.child(TextInput::new(&self.relay_input).xsmall())
|
||||
.child(
|
||||
Button::new("change")
|
||||
.label("Change")
|
||||
|
||||
@@ -6,5 +6,6 @@ pub mod onboarding;
|
||||
pub mod profile;
|
||||
pub mod relays;
|
||||
pub mod sidebar;
|
||||
pub mod startup;
|
||||
pub mod subject;
|
||||
pub mod welcome;
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use account::Account;
|
||||
use async_utility::task::spawn;
|
||||
use common::nip96_upload;
|
||||
use global::get_client;
|
||||
use global::constants::KEYRING_USER_PATH;
|
||||
use global::shared_state;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, img, prelude::FluentBuilder, relative, AnyElement, App, AppContext, Context, Entity,
|
||||
EventEmitter, Flatten, FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions,
|
||||
Render, SharedString, Styled, Window,
|
||||
div, img, relative, AnyElement, App, AppContext, Context, Entity, EventEmitter, Flatten,
|
||||
FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions, Render, SharedString,
|
||||
Styled, Window,
|
||||
};
|
||||
use nostr_sdk::prelude::*;
|
||||
use smol::fs;
|
||||
use theme::ActiveTheme;
|
||||
use ui::{
|
||||
button::{Button, ButtonVariants},
|
||||
dock_area::panel::{Panel, PanelEvent},
|
||||
input::{InputState, TextInput},
|
||||
popup_menu::PopupMenu,
|
||||
Disableable, Icon, IconName, Sizable, StyledExt,
|
||||
};
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::input::{InputState, TextInput};
|
||||
use ui::popup_menu::PopupMenu;
|
||||
use ui::{Disableable, Icon, IconName, Sizable, StyledExt};
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<NewAccount> {
|
||||
NewAccount::new(window, cx)
|
||||
@@ -44,8 +41,10 @@ impl NewAccount {
|
||||
|
||||
fn view(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let name_input = cx.new(|cx| InputState::new(window, cx).placeholder("Alice"));
|
||||
|
||||
let avatar_input =
|
||||
cx.new(|cx| InputState::new(window, cx).placeholder("https://example.com/avatar.jpg"));
|
||||
|
||||
let bio_input = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.multi_line()
|
||||
@@ -65,22 +64,33 @@ impl NewAccount {
|
||||
}
|
||||
}
|
||||
|
||||
fn submit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
fn submit(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.set_submitting(true, cx);
|
||||
|
||||
let avatar = self.avatar_input.read(cx).value().to_string();
|
||||
let name = self.name_input.read(cx).value().to_string();
|
||||
let bio = self.bio_input.read(cx).value().to_string();
|
||||
|
||||
let keys = Keys::generate();
|
||||
let mut metadata = Metadata::new().display_name(name).about(bio);
|
||||
|
||||
if let Ok(url) = Url::from_str(&avatar) {
|
||||
if let Ok(url) = Url::parse(&avatar) {
|
||||
metadata = metadata.picture(url);
|
||||
};
|
||||
|
||||
Account::global(cx).update(cx, |this, cx| {
|
||||
this.new_account(metadata, window, cx);
|
||||
});
|
||||
let save_credential = cx.write_credentials(
|
||||
KEYRING_USER_PATH,
|
||||
keys.public_key().to_hex().as_str(),
|
||||
keys.secret_key().as_secret_bytes(),
|
||||
);
|
||||
|
||||
cx.background_spawn(async move {
|
||||
if let Err(e) = save_credential.await {
|
||||
log::error!("Failed to save keys: {}", e)
|
||||
};
|
||||
shared_state().new_account(keys, metadata).await;
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn upload(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
@@ -109,11 +119,10 @@ impl NewAccount {
|
||||
};
|
||||
|
||||
if let Ok(file_data) = fs::read(path).await {
|
||||
let client = get_client();
|
||||
let (tx, rx) = oneshot::channel::<Url>();
|
||||
|
||||
spawn(async move {
|
||||
if let Ok(url) = nip96_upload(client, file_data).await {
|
||||
if let Ok(url) = nip96_upload(&shared_state().client, file_data).await {
|
||||
_ = tx.send(url);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,12 +3,10 @@ use gpui::{
|
||||
Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Window,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{
|
||||
button::{Button, ButtonVariants},
|
||||
dock_area::panel::{Panel, PanelEvent},
|
||||
popup_menu::PopupMenu,
|
||||
Icon, IconName, StyledExt,
|
||||
};
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::popup_menu::PopupMenu;
|
||||
use ui::{Icon, IconName, StyledExt};
|
||||
|
||||
use crate::chatspace;
|
||||
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use async_utility::task::spawn;
|
||||
use common::nip96_upload;
|
||||
use global::get_client;
|
||||
use global::shared_state;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, img, prelude::FluentBuilder, App, AppContext, Context, Entity, Flatten, IntoElement,
|
||||
ParentElement, PathPromptOptions, Render, Styled, Task, Window,
|
||||
div, img, App, AppContext, Context, Entity, Flatten, IntoElement, ParentElement,
|
||||
PathPromptOptions, Render, Styled, Task, Window,
|
||||
};
|
||||
use nostr_sdk::prelude::*;
|
||||
use smol::fs;
|
||||
use std::{str::FromStr, time::Duration};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{
|
||||
button::{Button, ButtonVariants},
|
||||
input::{InputState, TextInput},
|
||||
ContextModal, Disableable, IconName, Sizable,
|
||||
};
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::input::{InputState, TextInput};
|
||||
use ui::{ContextModal, Disableable, IconName, Sizable};
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Profile> {
|
||||
Profile::new(window, cx)
|
||||
@@ -54,10 +55,10 @@ impl Profile {
|
||||
};
|
||||
|
||||
let task: Task<Result<Option<Metadata>, Error>> = cx.background_spawn(async move {
|
||||
let client = get_client();
|
||||
let signer = client.signer().await?;
|
||||
let signer = shared_state().client.signer().await?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
let metadata = client
|
||||
let metadata = shared_state()
|
||||
.client
|
||||
.fetch_metadata(public_key, Duration::from_secs(2))
|
||||
.await?;
|
||||
|
||||
@@ -122,8 +123,7 @@ impl Profile {
|
||||
let (tx, rx) = oneshot::channel::<Url>();
|
||||
|
||||
spawn(async move {
|
||||
let client = get_client();
|
||||
if let Ok(url) = nip96_upload(client, file_data).await {
|
||||
if let Ok(url) = nip96_upload(&shared_state().client, file_data).await {
|
||||
_ = tx.send(url);
|
||||
}
|
||||
});
|
||||
@@ -189,9 +189,7 @@ impl Profile {
|
||||
}
|
||||
|
||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||
let client = get_client();
|
||||
_ = client.set_metadata(&new_metadata).await?;
|
||||
|
||||
let _ = shared_state().client.set_metadata(&new_metadata).await?;
|
||||
Ok(())
|
||||
});
|
||||
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
use anyhow::Error;
|
||||
use global::{constants::NEW_MESSAGE_SUB_ID, get_client};
|
||||
use global::constants::NEW_MESSAGE_SUB_ID;
|
||||
use global::shared_state;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, prelude::FluentBuilder, px, uniform_list, App, AppContext, Context, Entity, FocusHandle,
|
||||
InteractiveElement, IntoElement, ParentElement, Render, Styled, Subscription, Task, TextAlign,
|
||||
UniformList, Window,
|
||||
div, px, uniform_list, App, AppContext, Context, Entity, FocusHandle, InteractiveElement,
|
||||
IntoElement, ParentElement, Render, Styled, Subscription, Task, TextAlign, UniformList, Window,
|
||||
};
|
||||
use nostr_sdk::prelude::*;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{
|
||||
button::{Button, ButtonVariants},
|
||||
input::{InputEvent, InputState, TextInput},
|
||||
ContextModal, Disableable, IconName, Sizable,
|
||||
};
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::input::{InputEvent, InputState, TextInput};
|
||||
use ui::{ContextModal, Disableable, IconName, Sizable};
|
||||
|
||||
const MIN_HEIGHT: f32 = 200.0;
|
||||
const MESSAGE: &str = "In order to receive messages from others, you need to setup at least one Messaging Relay. You can use the recommend relays or add more.";
|
||||
@@ -36,16 +35,20 @@ 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 = get_client();
|
||||
let signer = client.signer().await?;
|
||||
let signer = shared_state().client.signer().await?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::InboxRelays)
|
||||
.author(public_key)
|
||||
.limit(1);
|
||||
|
||||
if let Some(event) = client.database().query(filter).await?.first_owned() {
|
||||
if let Some(event) = shared_state()
|
||||
.client
|
||||
.database()
|
||||
.query(filter)
|
||||
.await?
|
||||
.first_owned()
|
||||
{
|
||||
let relays = event
|
||||
.tags
|
||||
.filter(TagKind::Relay)
|
||||
@@ -108,18 +111,23 @@ impl Relays {
|
||||
|
||||
let relays = self.relays.read(cx).clone();
|
||||
let task: Task<Result<EventId, Error>> = cx.background_spawn(async move {
|
||||
let client = get_client();
|
||||
let signer = client.signer().await?;
|
||||
let signer = shared_state().client.signer().await?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
|
||||
// If user didn't have any NIP-65 relays, add default ones
|
||||
if client.database().relay_list(public_key).await?.is_empty() {
|
||||
if shared_state()
|
||||
.client
|
||||
.database()
|
||||
.relay_list(public_key)
|
||||
.await?
|
||||
.is_empty()
|
||||
{
|
||||
let builder = EventBuilder::relay_list(vec![
|
||||
(RelayUrl::parse("wss://relay.damus.io/").unwrap(), None),
|
||||
(RelayUrl::parse("wss://relay.primal.net/").unwrap(), None),
|
||||
]);
|
||||
|
||||
if let Err(e) = client.send_event_builder(builder).await {
|
||||
if let Err(e) = shared_state().client.send_event_builder(builder).await {
|
||||
log::error!("Failed to send relay list event: {}", e);
|
||||
}
|
||||
}
|
||||
@@ -130,21 +138,22 @@ impl Relays {
|
||||
.collect();
|
||||
|
||||
let builder = EventBuilder::new(Kind::InboxRelays, "").tags(tags);
|
||||
let output = client.send_event_builder(builder).await?;
|
||||
let output = shared_state().client.send_event_builder(builder).await?;
|
||||
|
||||
// Connect to messaging relays
|
||||
for relay in relays.into_iter() {
|
||||
_ = client.add_relay(&relay).await;
|
||||
_ = client.connect_relay(&relay).await;
|
||||
_ = shared_state().client.add_relay(&relay).await;
|
||||
_ = shared_state().client.connect_relay(&relay).await;
|
||||
}
|
||||
|
||||
let sub_id = SubscriptionId::new(NEW_MESSAGE_SUB_ID);
|
||||
|
||||
// Close old subscription
|
||||
client.unsubscribe(&sub_id).await;
|
||||
shared_state().client.unsubscribe(&sub_id).await;
|
||||
|
||||
// Subscribe to new messages
|
||||
if let Err(e) = client
|
||||
if let Err(e) = shared_state()
|
||||
.client
|
||||
.subscribe_with_id(
|
||||
sub_id,
|
||||
Filter::new()
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, img, prelude::FluentBuilder, rems, App, ClickEvent, Div, InteractiveElement, IntoElement,
|
||||
ParentElement as _, RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window,
|
||||
div, img, rems, App, ClickEvent, Div, InteractiveElement, IntoElement, ParentElement as _,
|
||||
RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{avatar::Avatar, StyledExt};
|
||||
use ui::avatar::Avatar;
|
||||
use ui::StyledExt;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct DisplayRoom {
|
||||
|
||||
@@ -1,33 +1,32 @@
|
||||
use std::{collections::BTreeSet, ops::Range, time::Duration};
|
||||
use std::collections::BTreeSet;
|
||||
use std::ops::Range;
|
||||
use std::time::Duration;
|
||||
|
||||
use account::Account;
|
||||
use async_utility::task::spawn;
|
||||
use chats::{
|
||||
room::{Room, RoomKind},
|
||||
ChatRegistry, RoomEmitter,
|
||||
};
|
||||
|
||||
use common::{debounced_delay::DebouncedDelay, profile::RenderProfile};
|
||||
use chats::room::{Room, RoomKind};
|
||||
use chats::{ChatRegistry, RoomEmitter};
|
||||
use common::debounced_delay::DebouncedDelay;
|
||||
use common::profile::RenderProfile;
|
||||
use element::DisplayRoom;
|
||||
use global::{constants::SEARCH_RELAYS, get_client};
|
||||
use global::constants::SEARCH_RELAYS;
|
||||
use global::shared_state;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, prelude::FluentBuilder, rems, uniform_list, AnyElement, App, AppContext, Context, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, Render, RetainAllImageCache,
|
||||
SharedString, Styled, Subscription, Task, Window,
|
||||
div, rems, uniform_list, AnyElement, App, AppContext, Context, Entity, EventEmitter,
|
||||
FocusHandle, Focusable, IntoElement, ParentElement, Render, RetainAllImageCache, SharedString,
|
||||
Styled, Subscription, Task, Window,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use nostr_sdk::prelude::*;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{
|
||||
avatar::Avatar,
|
||||
button::{Button, ButtonRounded, ButtonVariants},
|
||||
dock_area::panel::{Panel, PanelEvent},
|
||||
input::{InputEvent, InputState, TextInput},
|
||||
popup_menu::{PopupMenu, PopupMenuExt},
|
||||
skeleton::Skeleton,
|
||||
ContextModal, IconName, Selectable, Sizable, StyledExt,
|
||||
};
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonRounded, ButtonVariants};
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::input::{InputEvent, InputState, TextInput};
|
||||
use ui::popup_menu::{PopupMenu, PopupMenuExt};
|
||||
use ui::skeleton::Skeleton;
|
||||
use ui::{ContextModal, IconName, Selectable, Sizable, StyledExt};
|
||||
|
||||
use crate::chatspace::{ModalKind, ToggleModal};
|
||||
|
||||
@@ -142,14 +141,13 @@ impl Sidebar {
|
||||
let query = self.find_input.read(cx).value().clone();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let client = get_client();
|
||||
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::Metadata)
|
||||
.search(query.to_lowercase())
|
||||
.limit(FIND_LIMIT);
|
||||
|
||||
let events = client
|
||||
let events = shared_state()
|
||||
.client
|
||||
.fetch_events_from(SEARCH_RELAYS, filter, Duration::from_secs(3))
|
||||
.await?
|
||||
.into_iter()
|
||||
@@ -160,8 +158,11 @@ impl Sidebar {
|
||||
let (tx, rx) = smol::channel::bounded::<Room>(10);
|
||||
|
||||
spawn(async move {
|
||||
let client = get_client();
|
||||
let signer = client.signer().await.expect("signer is required");
|
||||
let signer = shared_state()
|
||||
.client
|
||||
.signer()
|
||||
.await
|
||||
.expect("signer is required");
|
||||
let public_key = signer.get_public_key().await.expect("error");
|
||||
|
||||
for event in events.into_iter() {
|
||||
@@ -492,9 +493,10 @@ impl Render for Sidebar {
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
// Account
|
||||
.when_some(Account::get_global(cx).profile_ref(), |this, profile| {
|
||||
this.child(self.render_account(profile, cx))
|
||||
})
|
||||
.when_some(
|
||||
shared_state().identity.read_blocking().as_ref(),
|
||||
|this, profile| this.child(self.render_account(profile, cx)),
|
||||
)
|
||||
// Search Input
|
||||
.child(
|
||||
div().px_3().w_full().h_7().flex_none().child(
|
||||
|
||||
88
crates/coop/src/views/startup.rs
Normal file
88
crates/coop/src/views/startup.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
use gpui::{
|
||||
div, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
IntoElement, ParentElement, Render, SharedString, Styled, Window,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use ui::button::Button;
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::indicator::Indicator;
|
||||
use ui::popup_menu::PopupMenu;
|
||||
use ui::Sizable;
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Startup> {
|
||||
Startup::new(window, cx)
|
||||
}
|
||||
|
||||
pub struct Startup {
|
||||
name: SharedString,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
impl Startup {
|
||||
fn new(_window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||
cx.new(|cx| Self {
|
||||
name: "Welcome".into(),
|
||||
focus_handle: cx.focus_handle(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Panel for Startup {
|
||||
fn panel_id(&self) -> SharedString {
|
||||
self.name.clone()
|
||||
}
|
||||
|
||||
fn title(&self, _cx: &App) -> AnyElement {
|
||||
"Startup".into_any_element()
|
||||
}
|
||||
|
||||
fn popup_menu(&self, menu: PopupMenu, _cx: &App) -> PopupMenu {
|
||||
menu.track_focus(&self.focus_handle)
|
||||
}
|
||||
|
||||
fn toolbar_buttons(&self, _window: &Window, _cx: &App) -> Vec<Button> {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<PanelEvent> for Startup {}
|
||||
|
||||
impl Focusable for Startup {
|
||||
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Startup {
|
||||
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.size_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.gap_6()
|
||||
.child(
|
||||
svg()
|
||||
.path("brand/coop.svg")
|
||||
.size_12()
|
||||
.text_color(cx.theme().elevated_surface_background),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_1p5()
|
||||
.text_xs()
|
||||
.text_center()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child("Connection in progress")
|
||||
.child(Indicator::new().small()),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,9 @@ use gpui::{
|
||||
ParentElement, Render, Styled, Window,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{
|
||||
button::{Button, ButtonVariants},
|
||||
input::{InputState, TextInput},
|
||||
ContextModal, Sizable,
|
||||
};
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::input::{InputState, TextInput};
|
||||
use ui::{ContextModal, Sizable};
|
||||
|
||||
pub fn init(
|
||||
id: u64,
|
||||
|
||||
@@ -3,12 +3,10 @@ use gpui::{
|
||||
IntoElement, ParentElement, Render, SharedString, Styled, Window,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{
|
||||
button::Button,
|
||||
dock_area::panel::{Panel, PanelEvent},
|
||||
popup_menu::PopupMenu,
|
||||
StyledExt,
|
||||
};
|
||||
use ui::button::Button;
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::popup_menu::PopupMenu;
|
||||
use ui::StyledExt;
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Welcome> {
|
||||
Welcome::new(window, cx)
|
||||
|
||||
Reference in New Issue
Block a user