diff --git a/Cargo.lock b/Cargo.lock
index 0bcc1fc..4a0d6c1 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2735,6 +2735,7 @@ dependencies = [
"strum",
"util",
"uuid",
+ "zed-font-kit",
]
[[package]]
@@ -2812,6 +2813,7 @@ dependencies = [
"windows-core 0.61.2",
"windows-numerics 0.2.0",
"windows-registry 0.5.3",
+ "zed-scap",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index 547811e..1780bd6 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -12,7 +12,7 @@ publish = false
# GPUI
gpui = { git = "https://github.com/zed-industries/zed" }
-gpui_platform = { git = "https://github.com/zed-industries/zed" }
+gpui_platform = { git = "https://github.com/zed-industries/zed", features = ["font-kit", "screen-capture", "x11", "wayland", "runtime_shaders"] }
gpui_linux = { git = "https://github.com/zed-industries/zed" }
gpui_windows = { git = "https://github.com/zed-industries/zed" }
gpui_macos = { git = "https://github.com/zed-industries/zed" }
diff --git a/assets/icons/panel-left-open.svg b/assets/icons/panel-left-open.svg
index a0f79d3..1281f6e 100644
--- a/assets/icons/panel-left-open.svg
+++ b/assets/icons/panel-left-open.svg
@@ -1,3 +1,3 @@
diff --git a/assets/icons/panel-left.svg b/assets/icons/panel-left.svg
index 814388d..1c98d39 100644
--- a/assets/icons/panel-left.svg
+++ b/assets/icons/panel-left.svg
@@ -1,3 +1,3 @@
diff --git a/assets/icons/panel-right-open.svg b/assets/icons/panel-right-open.svg
index 2d3e722..6387eae 100644
--- a/assets/icons/panel-right-open.svg
+++ b/assets/icons/panel-right-open.svg
@@ -1,3 +1,3 @@
diff --git a/assets/icons/panel-right.svg b/assets/icons/panel-right.svg
index ea70e43..636e01d 100644
--- a/assets/icons/panel-right.svg
+++ b/assets/icons/panel-right.svg
@@ -1,3 +1,3 @@
diff --git a/assets/icons/refresh.svg b/assets/icons/refresh.svg
new file mode 100644
index 0000000..b7ec11e
--- /dev/null
+++ b/assets/icons/refresh.svg
@@ -0,0 +1,3 @@
+
diff --git a/crates/chat/src/lib.rs b/crates/chat/src/lib.rs
index 21d2188..65c2cbb 100644
--- a/crates/chat/src/lib.rs
+++ b/crates/chat/src/lib.rs
@@ -65,6 +65,10 @@ impl InboxState {
pub fn not_configured(&self) -> bool {
matches!(self, InboxState::RelayNotAvailable)
}
+
+ pub fn subscribing(&self) -> bool {
+ matches!(self, InboxState::Subscribing)
+ }
}
/// Chat Registry
@@ -133,14 +137,14 @@ impl ChatRegistry {
// Run at the end of the current cycle
cx.defer_in(window, |this, _window, cx| {
+ // Load chat rooms
+ this.get_rooms(cx);
+
// Handle nostr notifications
this.handle_notifications(cx);
// Track unwrap gift wrap progress
this.tracking(cx);
-
- // Load chat rooms
- this.get_rooms(cx);
});
Self {
@@ -190,7 +194,7 @@ impl ChatRegistry {
}
// Extract the rumor from the gift wrap event
- match Self::extract_rumor(&client, &device_signer, event.as_ref()).await {
+ match extract_rumor(&client, &device_signer, event.as_ref()).await {
Ok(rumor) => match rumor.created_at >= initialized_at {
true => {
let new_message = NewMessage::new(event.id, rumor);
@@ -256,7 +260,7 @@ impl ChatRegistry {
}
/// Ensure messaging relays are set up for the current user.
- fn ensure_messaging_relays(&mut self, cx: &mut Context) {
+ pub fn ensure_messaging_relays(&mut self, cx: &mut Context) {
let task = self.verify_relays(cx);
// Set state to checking
@@ -556,7 +560,11 @@ impl ChatRegistry {
let public_key = signer.get_public_key().await?;
// Get contacts
- let contacts = client.database().contacts_public_keys(public_key).await?;
+ let contacts = client
+ .database()
+ .contacts_public_keys(public_key)
+ .await
+ .unwrap_or_default();
// Construct authored filter
let authored_filter = Filter::new()
@@ -652,147 +660,149 @@ impl ChatRegistry {
}
}
}
+}
- /// Unwraps a gift-wrapped event and processes its contents.
- async fn extract_rumor(
- client: &Client,
- device_signer: &Option>,
- gift_wrap: &Event,
- ) -> Result {
- // Try to get cached rumor first
- if let Ok(event) = Self::get_rumor(client, gift_wrap.id).await {
- return Ok(event);
- }
-
- // Try to unwrap with the available signer
- let unwrapped = Self::try_unwrap(client, device_signer, gift_wrap).await?;
- let mut rumor_unsigned = unwrapped.rumor;
-
- // Generate event id for the rumor if it doesn't have one
- rumor_unsigned.ensure_id();
-
- // Cache the rumor
- Self::set_rumor(client, gift_wrap.id, &rumor_unsigned).await?;
-
- Ok(rumor_unsigned)
+/// Unwraps a gift-wrapped event and processes its contents.
+async fn extract_rumor(
+ client: &Client,
+ device_signer: &Option>,
+ gift_wrap: &Event,
+) -> Result {
+ // Try to get cached rumor first
+ if let Ok(event) = get_rumor(client, gift_wrap.id).await {
+ return Ok(event);
}
- /// Helper method to try unwrapping with different signers
- async fn try_unwrap(
- client: &Client,
- device_signer: &Option>,
- gift_wrap: &Event,
- ) -> Result {
- // Try with the device signer first
- if let Some(signer) = device_signer {
- if let Ok(unwrapped) = Self::try_unwrap_with(gift_wrap, signer).await {
- return Ok(unwrapped);
- };
+ // Try to unwrap with the available signer
+ let unwrapped = try_unwrap(client, device_signer, gift_wrap).await?;
+ let mut rumor = unwrapped.rumor;
+
+ // Generate event id for the rumor if it doesn't have one
+ rumor.ensure_id();
+
+ // Cache the rumor
+ if let Err(e) = set_rumor(client, gift_wrap.id, &rumor).await {
+ log::error!("Failed to cache rumor: {e:?}");
+ }
+
+ Ok(rumor)
+}
+
+/// Helper method to try unwrapping with different signers
+async fn try_unwrap(
+ client: &Client,
+ device_signer: &Option>,
+ gift_wrap: &Event,
+) -> Result {
+ // Try with the device signer first
+ if let Some(signer) = device_signer {
+ if let Ok(unwrapped) = try_unwrap_with(gift_wrap, signer).await {
+ return Ok(unwrapped);
};
+ };
- // Try with the user's signer
- let user_signer = client.signer().context("Signer not found")?;
- let unwrapped = UnwrappedGift::from_gift_wrap(user_signer, gift_wrap).await?;
+ // Try with the user's signer
+ let user_signer = client.signer().context("Signer not found")?;
+ let unwrapped = UnwrappedGift::from_gift_wrap(user_signer, gift_wrap).await?;
- Ok(unwrapped)
- }
+ Ok(unwrapped)
+}
- /// Attempts to unwrap a gift wrap event with a given signer.
- async fn try_unwrap_with(
- gift_wrap: &Event,
- signer: &Arc,
- ) -> Result {
- // Get the sealed event
- let seal = signer
- .nip44_decrypt(&gift_wrap.pubkey, &gift_wrap.content)
- .await?;
+/// Attempts to unwrap a gift wrap event with a given signer.
+async fn try_unwrap_with(
+ gift_wrap: &Event,
+ signer: &Arc,
+) -> Result {
+ // Get the sealed event
+ let seal = signer
+ .nip44_decrypt(&gift_wrap.pubkey, &gift_wrap.content)
+ .await?;
- // Verify the sealed event
- let seal: Event = Event::from_json(seal)?;
- seal.verify_with_ctx(&SECP256K1)?;
+ // Verify the sealed event
+ let seal: Event = Event::from_json(seal)?;
+ seal.verify_with_ctx(&SECP256K1)?;
- // Get the rumor event
- let rumor = signer.nip44_decrypt(&seal.pubkey, &seal.content).await?;
- let rumor = UnsignedEvent::from_json(rumor)?;
+ // Get the rumor event
+ let rumor = signer.nip44_decrypt(&seal.pubkey, &seal.content).await?;
+ let rumor = UnsignedEvent::from_json(rumor)?;
- Ok(UnwrappedGift {
- sender: seal.pubkey,
- rumor,
- })
- }
+ Ok(UnwrappedGift {
+ sender: seal.pubkey,
+ rumor,
+ })
+}
- /// Stores an unwrapped event in local database with reference to original
- async fn set_rumor(client: &Client, id: EventId, rumor: &UnsignedEvent) -> Result<(), Error> {
- let rumor_id = rumor.id.context("Rumor is missing an event id")?;
- let author = rumor.pubkey;
- let conversation = Self::conversation_id(rumor);
+/// Stores an unwrapped event in local database with reference to original
+async fn set_rumor(client: &Client, id: EventId, rumor: &UnsignedEvent) -> Result<(), Error> {
+ let rumor_id = rumor.id.context("Rumor is missing an event id")?;
+ let author = rumor.pubkey;
+ let conversation = conversation_id(rumor);
- let mut tags = rumor.tags.clone().to_vec();
+ let mut tags = rumor.tags.clone().to_vec();
- // Add a unique identifier
- tags.push(Tag::identifier(id));
+ // Add a unique identifier
+ tags.push(Tag::identifier(id));
- // Add a reference to the rumor's author
+ // Add a reference to the rumor's author
+ tags.push(Tag::custom(
+ TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::A)),
+ [author],
+ ));
+
+ // Add a conversation id
+ tags.push(Tag::custom(
+ TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::C)),
+ [conversation.to_string()],
+ ));
+
+ // Add a reference to the rumor's id
+ tags.push(Tag::event(rumor_id));
+
+ // Add references to the rumor's participants
+ for receiver in rumor.tags.public_keys().copied() {
tags.push(Tag::custom(
- TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::A)),
- [author],
+ TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::P)),
+ [receiver],
));
-
- // Add a conversation id
- tags.push(Tag::custom(
- TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::C)),
- [conversation.to_string()],
- ));
-
- // Add a reference to the rumor's id
- tags.push(Tag::event(rumor_id));
-
- // Add references to the rumor's participants
- for receiver in rumor.tags.public_keys().copied() {
- tags.push(Tag::custom(
- TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::P)),
- [receiver],
- ));
- }
-
- // Convert rumor to json
- let content = rumor.as_json();
-
- // Construct the event
- let event = EventBuilder::new(Kind::ApplicationSpecificData, content)
- .tags(tags)
- .sign(&Keys::generate())
- .await?;
-
- // Save the event to the database
- client.database().save_event(&event).await?;
-
- Ok(())
}
- /// Retrieves a previously unwrapped event from local database
- async fn get_rumor(client: &Client, gift_wrap: EventId) -> Result {
- let filter = Filter::new()
- .kind(Kind::ApplicationSpecificData)
- .identifier(gift_wrap)
- .limit(1);
+ // Convert rumor to json
+ let content = rumor.as_json();
- if let Some(event) = client.database().query(filter).await?.first_owned() {
- UnsignedEvent::from_json(event.content).map_err(|e| anyhow!(e))
- } else {
- Err(anyhow!("Event is not cached yet."))
- }
- }
+ // Construct the event
+ let event = EventBuilder::new(Kind::ApplicationSpecificData, content)
+ .tags(tags)
+ .sign(&Keys::generate())
+ .await?;
- /// Get the conversation ID for a given rumor (message).
- fn conversation_id(rumor: &UnsignedEvent) -> u64 {
- let mut hasher = DefaultHasher::new();
- let mut pubkeys: Vec = rumor.tags.public_keys().copied().collect();
- pubkeys.push(rumor.pubkey);
- pubkeys.sort();
- pubkeys.dedup();
- pubkeys.hash(&mut hasher);
+ // Save the event to the database
+ client.database().save_event(&event).await?;
- hasher.finish()
+ Ok(())
+}
+
+/// Retrieves a previously unwrapped event from local database
+async fn get_rumor(client: &Client, gift_wrap: EventId) -> Result {
+ let filter = Filter::new()
+ .kind(Kind::ApplicationSpecificData)
+ .identifier(gift_wrap)
+ .limit(1);
+
+ if let Some(event) = client.database().query(filter).await?.first_owned() {
+ UnsignedEvent::from_json(event.content).map_err(|e| anyhow!(e))
+ } else {
+ Err(anyhow!("Event is not cached yet."))
}
}
+
+/// Get the conversation ID for a given rumor (message).
+fn conversation_id(rumor: &UnsignedEvent) -> u64 {
+ let mut hasher = DefaultHasher::new();
+ let mut pubkeys: Vec = rumor.tags.public_keys().copied().collect();
+ pubkeys.push(rumor.pubkey);
+ pubkeys.sort();
+ pubkeys.dedup();
+ pubkeys.hash(&mut hasher);
+
+ hasher.finish()
+}
diff --git a/crates/chat_ui/src/lib.rs b/crates/chat_ui/src/lib.rs
index f307595..b346f1e 100644
--- a/crates/chat_ui/src/lib.rs
+++ b/crates/chat_ui/src/lib.rs
@@ -657,7 +657,7 @@ impl ChatPanel {
.id(ix)
.relative()
.w_full()
- .py_1()
+ .py_2()
.px_3()
.bg(cx.theme().warning_background)
.child(
diff --git a/crates/coop/src/actions.rs b/crates/coop/src/actions.rs
deleted file mode 100644
index 8a5798b..0000000
--- a/crates/coop/src/actions.rs
+++ /dev/null
@@ -1,94 +0,0 @@
-use std::sync::Mutex;
-
-use gpui::{actions, App};
-use key_store::{KeyItem, KeyStore};
-use nostr_connect::prelude::*;
-use state::NostrRegistry;
-
-// Sidebar actions
-actions!(sidebar, [Reload, RelayStatus]);
-
-// User actions
-actions!(
- coop,
- [
- KeyringPopup,
- DarkMode,
- ViewProfile,
- ViewRelays,
- Themes,
- Settings,
- Logout,
- Quit
- ]
-);
-
-#[derive(Debug, Clone)]
-pub struct CoopAuthUrlHandler;
-
-impl AuthUrlHandler for CoopAuthUrlHandler {
- #[allow(mismatched_lifetime_syntaxes)]
- fn on_auth_url(&self, auth_url: Url) -> BoxedFuture> {
- Box::pin(async move {
- log::info!("Received Auth URL: {auth_url}");
- webbrowser::open(auth_url.as_str())?;
- Ok(())
- })
- }
-}
-
-pub fn load_embedded_fonts(cx: &App) {
- let asset_source = cx.asset_source();
- let font_paths = asset_source.list("fonts").unwrap();
- let embedded_fonts = Mutex::new(Vec::new());
- let executor = cx.background_executor();
-
- cx.foreground_executor().block_on(executor.scoped(|scope| {
- for font_path in &font_paths {
- if !font_path.ends_with(".ttf") {
- continue;
- }
-
- scope.spawn(async {
- let font_bytes = asset_source.load(font_path).unwrap().unwrap();
- embedded_fonts.lock().unwrap().push(font_bytes);
- });
- }
- }));
-
- cx.text_system()
- .add_fonts(embedded_fonts.into_inner().unwrap())
- .unwrap();
-}
-
-pub fn reset(cx: &mut App) {
- let backend = KeyStore::global(cx).read(cx).backend();
- let client = NostrRegistry::global(cx).read(cx).client();
-
- cx.spawn(async move |cx| {
- // Remove the signer
- client.unset_signer().await;
-
- // Delete user's credentials
- backend
- .delete_credentials(&KeyItem::User.to_string(), cx)
- .await
- .ok();
-
- // Remove bunker's credentials if available
- backend
- .delete_credentials(&KeyItem::Bunker.to_string(), cx)
- .await
- .ok();
-
- cx.update(|cx| {
- cx.restart();
- });
- })
- .detach();
-}
-
-pub fn quit(_: &Quit, cx: &mut App) {
- log::info!("Gracefully quitting the application . . .");
- cx.quit();
-}
diff --git a/crates/coop/src/panels/encryption_key.rs b/crates/coop/src/panels/encryption_key.rs
new file mode 100644
index 0000000..0f243ac
--- /dev/null
+++ b/crates/coop/src/panels/encryption_key.rs
@@ -0,0 +1,54 @@
+use gpui::{
+ AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
+ IntoElement, Render, SharedString, Styled, Window,
+};
+use ui::dock_area::panel::{Panel, PanelEvent};
+use ui::v_flex;
+
+pub fn init(window: &mut Window, cx: &mut App) -> Entity {
+ cx.new(|cx| EncryptionPanel::new(window, cx))
+}
+
+#[derive(Debug)]
+pub struct EncryptionPanel {
+ name: SharedString,
+ focus_handle: FocusHandle,
+}
+
+impl EncryptionPanel {
+ fn new(_window: &mut Window, cx: &mut Context) -> Self {
+ Self {
+ name: "Encryption".into(),
+ focus_handle: cx.focus_handle(),
+ }
+ }
+}
+
+impl Panel for EncryptionPanel {
+ fn panel_id(&self) -> SharedString {
+ self.name.clone()
+ }
+
+ fn title(&self, _cx: &App) -> AnyElement {
+ self.name.clone().into_any_element()
+ }
+}
+
+impl EventEmitter for EncryptionPanel {}
+
+impl Focusable for EncryptionPanel {
+ fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
+ self.focus_handle.clone()
+ }
+}
+
+impl Render for EncryptionPanel {
+ fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement {
+ v_flex()
+ .size_full()
+ .items_center()
+ .justify_center()
+ .p_2()
+ .gap_10()
+ }
+}
diff --git a/crates/coop/src/panels/greeter.rs b/crates/coop/src/panels/greeter.rs
index 99cc7d6..bdee671 100644
--- a/crates/coop/src/panels/greeter.rs
+++ b/crates/coop/src/panels/greeter.rs
@@ -1,8 +1,8 @@
use chat::{ChatRegistry, InboxState};
use gpui::prelude::FluentBuilder;
use gpui::{
- div, relative, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle,
- Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Window,
+ div, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
+ IntoElement, ParentElement, Render, SharedString, Styled, Window,
};
use state::{NostrRegistry, RelayState};
use theme::ActiveTheme;
@@ -122,14 +122,13 @@ impl Render for GreeterPanel {
.child(
div()
.font_semibold()
- .line_height(relative(1.25))
+ .text_color(cx.theme().text)
.child(SharedString::from(TITLE)),
)
.child(
div()
- .text_sm()
+ .text_xs()
.text_color(cx.theme().text_muted)
- .line_height(relative(1.25))
.child(SharedString::from(DESCRIPTION)),
),
),
@@ -141,9 +140,9 @@ impl Render for GreeterPanel {
.w_full()
.child(
h_flex()
- .gap_1()
+ .gap_2()
.w_full()
- .text_sm()
+ .text_xs()
.font_semibold()
.text_color(cx.theme().text_muted)
.child(SharedString::from("Required Actions"))
@@ -199,9 +198,9 @@ impl Render for GreeterPanel {
.w_full()
.child(
h_flex()
- .gap_1()
+ .gap_2()
.w_full()
- .text_sm()
+ .text_xs()
.font_semibold()
.text_color(cx.theme().text_muted)
.child(SharedString::from("Use your own identity"))
@@ -252,9 +251,9 @@ impl Render for GreeterPanel {
.w_full()
.child(
h_flex()
- .gap_1()
+ .gap_2()
.w_full()
- .text_sm()
+ .text_xs()
.font_semibold()
.text_color(cx.theme().text_muted)
.child(SharedString::from("Get Started"))
@@ -264,14 +263,6 @@ impl Render for GreeterPanel {
v_flex()
.gap_2()
.w_full()
- .child(
- Button::new("backup")
- .icon(Icon::new(IconName::Shield))
- .label("Backup account")
- .ghost()
- .small()
- .justify_start(),
- )
.child(
Button::new("profile")
.icon(Icon::new(IconName::Profile))
diff --git a/crates/coop/src/panels/mod.rs b/crates/coop/src/panels/mod.rs
index 7e6e20a..259167c 100644
--- a/crates/coop/src/panels/mod.rs
+++ b/crates/coop/src/panels/mod.rs
@@ -1,4 +1,5 @@
pub mod connect;
+pub mod encryption_key;
pub mod greeter;
pub mod import;
pub mod messaging_relays;
diff --git a/crates/coop/src/sidebar/mod.rs b/crates/coop/src/sidebar/mod.rs
index 9c54cc1..a07ad17 100644
--- a/crates/coop/src/sidebar/mod.rs
+++ b/crates/coop/src/sidebar/mod.rs
@@ -659,7 +659,7 @@ impl Render for Sidebar {
.text_xs()
.font_semibold()
.text_color(cx.theme().text_muted)
- .child(Icon::new(IconName::ChevronDown))
+ .child(Icon::new(IconName::ChevronDown).small())
.child(SharedString::from("Suggestions")),
)
.child(
diff --git a/crates/coop/src/workspace.rs b/crates/coop/src/workspace.rs
index 532ee81..6554137 100644
--- a/crates/coop/src/workspace.rs
+++ b/crates/coop/src/workspace.rs
@@ -3,29 +3,40 @@ use std::sync::Arc;
use chat::{ChatEvent, ChatRegistry, InboxState};
use gpui::prelude::FluentBuilder;
use gpui::{
- div, rems, App, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement,
+ div, px, rems, Action, App, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement,
ParentElement, Render, SharedString, Styled, Subscription, Window,
};
use person::PersonRegistry;
+use serde::Deserialize;
use smallvec::{smallvec, SmallVec};
use state::{NostrRegistry, RelayState};
-use theme::{ActiveTheme, Theme, SIDEBAR_WIDTH, TITLEBAR_HEIGHT};
+use theme::{ActiveTheme, Theme, SIDEBAR_WIDTH};
use title_bar::TitleBar;
use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants};
use ui::dock_area::dock::DockPlacement;
-use ui::dock_area::panel::{PanelStyle, PanelView};
+use ui::dock_area::panel::PanelView;
use ui::dock_area::{ClosePanel, DockArea, DockItem};
use ui::menu::DropdownMenu;
-use ui::{h_flex, v_flex, Root, Sizable, WindowExtension};
+use ui::{h_flex, v_flex, IconName, Root, Sizable, WindowExtension};
-use crate::panels::greeter;
+use crate::panels::{encryption_key, greeter, messaging_relays, relay_list};
use crate::sidebar;
pub fn init(window: &mut Window, cx: &mut App) -> Entity {
cx.new(|cx| Workspace::new(window, cx))
}
+#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
+#[action(namespace = workspace, no_json)]
+enum Command {
+ ReloadRelayList,
+ OpenRelayPanel,
+ ReloadInbox,
+ OpenInboxPanel,
+ OpenEncryptionPanel,
+}
+
pub struct Workspace {
/// App's Title Bar
titlebar: Entity,
@@ -41,7 +52,7 @@ impl Workspace {
fn new(window: &mut Window, cx: &mut Context) -> Self {
let chat = ChatRegistry::global(cx);
let titlebar = cx.new(|_| TitleBar::new());
- let dock = cx.new(|cx| DockArea::new(window, cx).style(PanelStyle::TabBar));
+ let dock = cx.new(|cx| DockArea::new(window, cx));
let mut subscriptions = smallvec![];
@@ -168,14 +179,59 @@ impl Workspace {
});
}
+ fn on_command(&mut self, command: &Command, window: &mut Window, cx: &mut Context) {
+ match command {
+ Command::OpenEncryptionPanel => {
+ self.dock.update(cx, |this, cx| {
+ this.add_panel(
+ Arc::new(encryption_key::init(window, cx)),
+ DockPlacement::Right,
+ window,
+ cx,
+ );
+ });
+ }
+ Command::OpenInboxPanel => {
+ self.dock.update(cx, |this, cx| {
+ this.add_panel(
+ Arc::new(messaging_relays::init(window, cx)),
+ DockPlacement::Right,
+ window,
+ cx,
+ );
+ });
+ }
+ Command::OpenRelayPanel => {
+ self.dock.update(cx, |this, cx| {
+ this.add_panel(
+ Arc::new(relay_list::init(window, cx)),
+ DockPlacement::Right,
+ window,
+ cx,
+ );
+ });
+ }
+ Command::ReloadInbox => {
+ let nostr = NostrRegistry::global(cx);
+ nostr.update(cx, |this, cx| {
+ this.ensure_relay_list(cx);
+ });
+ }
+ Command::ReloadRelayList => {
+ let chat = ChatRegistry::global(cx);
+ chat.update(cx, |this, cx| {
+ this.ensure_messaging_relays(cx);
+ });
+ }
+ }
+ }
+
fn titlebar_left(&mut self, _window: &mut Window, cx: &Context) -> impl IntoElement {
- let chat = ChatRegistry::global(cx);
let nostr = NostrRegistry::global(cx);
let signer = nostr.read(cx).signer();
let current_user = signer.public_key();
h_flex()
- .h(TITLEBAR_HEIGHT)
.flex_shrink_0()
.justify_between()
.gap_2()
@@ -213,51 +269,207 @@ impl Workspace {
.child(SharedString::from("Connecting...")),
)
})
- .map(|this| match nostr.read(cx).relay_list_state() {
- RelayState::Checking => this.child(
- div()
- .text_xs()
- .text_color(cx.theme().text_muted)
- .child(SharedString::from("Fetching user's relay list...")),
- ),
- RelayState::NotConfigured => this.child(
- h_flex()
- .h_6()
- .w_full()
- .px_1()
- .text_xs()
- .text_color(cx.theme().warning_foreground)
- .bg(cx.theme().warning_background)
- .rounded_sm()
- .child(SharedString::from("User hasn't configured a relay list")),
- ),
- _ => this,
- })
- .map(|this| match chat.read(cx).state(cx) {
- InboxState::Checking => {
- this.child(div().text_xs().text_color(cx.theme().text_muted).child(
- SharedString::from("Fetching user's messaging relay list..."),
- ))
- }
- InboxState::RelayNotAvailable => this.child(
- h_flex()
- .h_6()
- .w_full()
- .px_2()
- .text_xs()
- .text_color(cx.theme().warning_foreground)
- .bg(cx.theme().warning_background)
- .rounded_full()
- .child(SharedString::from(
- "User hasn't configured a messaging relay list",
- )),
- ),
- _ => this,
- })
}
- fn titlebar_right(&mut self, _window: &mut Window, _cx: &Context) -> impl IntoElement {
- h_flex().h(TITLEBAR_HEIGHT).flex_shrink_0()
+ fn titlebar_right(&mut self, _window: &mut Window, cx: &Context) -> impl IntoElement {
+ let nostr = NostrRegistry::global(cx);
+ let signer = nostr.read(cx).signer();
+ let relay_list = nostr.read(cx).relay_list_state();
+
+ let chat = ChatRegistry::global(cx);
+ let inbox_state = chat.read(cx).state(cx);
+
+ let Some(pkey) = signer.public_key() else {
+ return div();
+ };
+
+ h_flex()
+ .when(!cx.theme().platform.is_mac(), |this| this.pr_2())
+ .gap_3()
+ .child(
+ Button::new("key")
+ .icon(IconName::UserKey)
+ .tooltip("Decoupled encryption key")
+ .small()
+ .ghost()
+ .on_click(|_ev, window, cx| {
+ window.dispatch_action(Box::new(Command::OpenEncryptionPanel), cx);
+ }),
+ )
+ .child(
+ h_flex()
+ .gap_2()
+ .child(
+ div()
+ .text_xs()
+ .text_color(cx.theme().text_muted)
+ .map(|this| match inbox_state {
+ InboxState::Checking => this.child(div().child(
+ SharedString::from("Fetching user's messaging relay list..."),
+ )),
+ InboxState::RelayNotAvailable => {
+ this.child(div().text_color(cx.theme().warning_active).child(
+ SharedString::from(
+ "User hasn't configured a messaging relay list",
+ ),
+ ))
+ }
+ _ => this,
+ }),
+ )
+ .child(
+ Button::new("inbox")
+ .icon(IconName::Inbox)
+ .tooltip("Inbox")
+ .small()
+ .ghost()
+ .when(inbox_state.subscribing(), |this| this.indicator())
+ .dropdown_menu(move |this, _window, _cx| {
+ this.min_w(px(260.))
+ .label("Messaging Relays")
+ .menu_element_with_disabled(
+ Box::new(Command::OpenRelayPanel),
+ true,
+ move |_window, cx| {
+ let persons = PersonRegistry::global(cx);
+ let profile = persons.read(cx).get(&pkey, cx);
+ let urls = profile.messaging_relays();
+
+ v_flex()
+ .gap_1()
+ .w_full()
+ .items_start()
+ .justify_start()
+ .children({
+ let mut items = vec![];
+
+ for url in urls.iter() {
+ items.push(
+ h_flex()
+ .h_6()
+ .w_full()
+ .gap_2()
+ .px_2()
+ .text_xs()
+ .bg(cx
+ .theme()
+ .elevated_surface_background)
+ .rounded(cx.theme().radius)
+ .child(
+ div()
+ .size_1()
+ .rounded_full()
+ .bg(gpui::green()),
+ )
+ .child(SharedString::from(
+ url.to_string(),
+ )),
+ );
+ }
+
+ items
+ })
+ },
+ )
+ .separator()
+ .menu_with_icon(
+ "Reload",
+ IconName::Refresh,
+ Box::new(Command::ReloadInbox),
+ )
+ .menu_with_icon(
+ "Update relays",
+ IconName::Settings,
+ Box::new(Command::OpenInboxPanel),
+ )
+ }),
+ ),
+ )
+ .child(
+ h_flex()
+ .gap_2()
+ .child(
+ div()
+ .text_xs()
+ .text_color(cx.theme().text_muted)
+ .map(|this| match relay_list {
+ RelayState::Checking => this
+ .child(div().child(SharedString::from(
+ "Fetching user's relay list...",
+ ))),
+ RelayState::NotConfigured => {
+ this.child(div().text_color(cx.theme().warning_active).child(
+ SharedString::from("User hasn't configured a relay list"),
+ ))
+ }
+ _ => this,
+ }),
+ )
+ .child(
+ Button::new("relay-list")
+ .icon(IconName::Relay)
+ .tooltip("User's relay list")
+ .small()
+ .ghost()
+ .when(relay_list.configured(), |this| this.indicator())
+ .dropdown_menu(move |this, _window, _cx| {
+ this.min_w(px(260.))
+ .label("Relays")
+ .menu_element_with_disabled(
+ Box::new(Command::OpenRelayPanel),
+ true,
+ move |_window, cx| {
+ let nostr = NostrRegistry::global(cx);
+ let urls = nostr.read(cx).read_only_relays(&pkey, cx);
+
+ v_flex()
+ .gap_1()
+ .w_full()
+ .items_start()
+ .justify_start()
+ .children({
+ let mut items = vec![];
+
+ for url in urls.into_iter() {
+ items.push(
+ h_flex()
+ .h_6()
+ .w_full()
+ .gap_2()
+ .px_2()
+ .text_xs()
+ .bg(cx
+ .theme()
+ .elevated_surface_background)
+ .rounded(cx.theme().radius)
+ .child(
+ div()
+ .size_1()
+ .rounded_full()
+ .bg(gpui::green()),
+ )
+ .child(url),
+ );
+ }
+
+ items
+ })
+ },
+ )
+ .separator()
+ .menu_with_icon(
+ "Reload",
+ IconName::Refresh,
+ Box::new(Command::ReloadRelayList),
+ )
+ .menu_with_icon(
+ "Update relay list",
+ IconName::Settings,
+ Box::new(Command::OpenRelayPanel),
+ )
+ }),
+ ),
+ )
}
}
@@ -277,6 +489,7 @@ impl Render for Workspace {
div()
.id(SharedString::from("workspace"))
+ .on_action(cx.listener(Self::on_command))
.relative()
.size_full()
.child(
diff --git a/crates/device/src/lib.rs b/crates/device/src/lib.rs
index 28aceb4..f711950 100644
--- a/crates/device/src/lib.rs
+++ b/crates/device/src/lib.rs
@@ -180,7 +180,7 @@ impl DeviceRegistry {
/// Reset the device state
fn reset(&mut self, cx: &mut Context) {
- self.state = DeviceState::Initial;
+ self.state = DeviceState::Idle;
self.requests.update(cx, |this, cx| {
this.clear();
cx.notify();
diff --git a/crates/settings/src/lib.rs b/crates/settings/src/lib.rs
index 8039a6b..c4ce94a 100644
--- a/crates/settings/src/lib.rs
+++ b/crates/settings/src/lib.rs
@@ -52,8 +52,8 @@ pub enum AuthMode {
/// Signer kind
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub enum SignerKind {
- Auto,
#[default]
+ Auto,
User,
Encryption,
}
diff --git a/crates/state/src/device.rs b/crates/state/src/device.rs
index 156be81..93dfd38 100644
--- a/crates/state/src/device.rs
+++ b/crates/state/src/device.rs
@@ -4,7 +4,7 @@ use nostr_sdk::prelude::*;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub enum DeviceState {
#[default]
- Initial,
+ Idle,
Requesting,
Set,
}
diff --git a/crates/state/src/gossip.rs b/crates/state/src/gossip.rs
index 5cbe245..805a1ac 100644
--- a/crates/state/src/gossip.rs
+++ b/crates/state/src/gossip.rs
@@ -1,5 +1,6 @@
use std::collections::{HashMap, HashSet};
+use gpui::SharedString;
use nostr_sdk::prelude::*;
/// Gossip
@@ -9,6 +10,18 @@ pub struct Gossip {
}
impl Gossip {
+ pub fn read_only_relays(&self, public_key: &PublicKey) -> Vec {
+ self.relays
+ .get(public_key)
+ .map(|relays| {
+ relays
+ .iter()
+ .map(|(url, _)| url.to_string().into())
+ .collect()
+ })
+ .unwrap_or_default()
+ }
+
/// Get read relays for a given public key
pub fn read_relays(&self, public_key: &PublicKey) -> Vec {
self.relays
diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs
index 4ab7667..65e5d5c 100644
--- a/crates/state/src/lib.rs
+++ b/crates/state/src/lib.rs
@@ -5,7 +5,7 @@ use std::time::Duration;
use anyhow::{anyhow, Context as AnyhowContext, Error};
use common::config_dir;
-use gpui::{App, AppContext, Context, Entity, Global, Task, Window};
+use gpui::{App, AppContext, Context, Entity, Global, SharedString, Task, Window};
use nostr_connect::prelude::*;
use nostr_lmdb::prelude::*;
use nostr_sdk::prelude::*;
@@ -247,6 +247,11 @@ impl NostrRegistry {
self.relay_list_state.clone()
}
+ /// Get all relays for a given public key without ensuring connections
+ pub fn read_only_relays(&self, public_key: &PublicKey, cx: &App) -> Vec {
+ self.gossip.read(cx).read_only_relays(public_key)
+ }
+
/// Get a list of write relays for a given public key
pub fn write_relays(&self, public_key: &PublicKey, cx: &App) -> Task> {
let client = self.client();
@@ -483,7 +488,7 @@ impl NostrRegistry {
cx.notify();
}
- fn ensure_relay_list(&mut self, cx: &mut Context) {
+ pub fn ensure_relay_list(&mut self, cx: &mut Context) {
let task = self.verify_relay_list(cx);
self.tasks.push(cx.spawn(async move |this, cx| {
diff --git a/crates/title_bar/src/lib.rs b/crates/title_bar/src/lib.rs
index 4bbaa0e..de7f670 100644
--- a/crates/title_bar/src/lib.rs
+++ b/crates/title_bar/src/lib.rs
@@ -4,7 +4,7 @@ use gpui::MouseButton;
#[cfg(not(target_os = "windows"))]
use gpui::Pixels;
use gpui::{
- div, px, AnyElement, Context, Decorations, Hsla, InteractiveElement as _, IntoElement,
+ px, AnyElement, Context, Decorations, Hsla, InteractiveElement as _, IntoElement,
ParentElement, Render, StatefulInteractiveElement as _, Styled, Window, WindowControlArea,
};
use smallvec::{smallvec, SmallVec};
@@ -127,62 +127,48 @@ impl Render for TitleBar {
}
})
})
+ .when(!cx.theme().platform.is_mac(), |this| this.pr_2())
.children(children),
)
- .child(
- h_flex()
- .absolute()
- .top_0()
- .right_0()
- .pr_2()
- .h(height)
- .child(
- div().when(!window.is_fullscreen(), |this| match cx.theme().platform {
- PlatformKind::Linux => {
- #[cfg(target_os = "linux")]
- if matches!(decorations, Decorations::Client { .. }) {
- this.child(LinuxWindowControls::new(None))
- .when(supported_controls.window_menu, |this| {
- this.on_mouse_down(
- MouseButton::Right,
- move |ev, window, _| {
- window.show_window_menu(ev.position)
- },
- )
- })
- .on_mouse_move(cx.listener(move |this, _ev, window, _| {
- if this.should_move {
- this.should_move = false;
- window.start_window_move();
- }
- }))
- .on_mouse_down_out(cx.listener(
- move |this, _ev, _window, _cx| {
- this.should_move = false;
- },
- ))
- .on_mouse_up(
- MouseButton::Left,
- cx.listener(move |this, _ev, _window, _cx| {
- this.should_move = false;
- }),
- )
- .on_mouse_down(
- MouseButton::Left,
- cx.listener(move |this, _ev, _window, _cx| {
- this.should_move = true;
- }),
- )
- } else {
- this
+ .when(!window.is_fullscreen(), |this| match cx.theme().platform {
+ PlatformKind::Linux => {
+ #[cfg(target_os = "linux")]
+ if matches!(decorations, Decorations::Client { .. }) {
+ this.child(LinuxWindowControls::new(None))
+ .when(supported_controls.window_menu, |this| {
+ this.on_mouse_down(MouseButton::Right, move |ev, window, _| {
+ window.show_window_menu(ev.position)
+ })
+ })
+ .on_mouse_move(cx.listener(move |this, _ev, window, _| {
+ if this.should_move {
+ this.should_move = false;
+ window.start_window_move();
}
- #[cfg(not(target_os = "linux"))]
- this
- }
- PlatformKind::Windows => this.child(WindowsWindowControls::new(height)),
- PlatformKind::Mac => this,
- }),
- ),
- )
+ }))
+ .on_mouse_down_out(cx.listener(move |this, _ev, _window, _cx| {
+ this.should_move = false;
+ }))
+ .on_mouse_up(
+ MouseButton::Left,
+ cx.listener(move |this, _ev, _window, _cx| {
+ this.should_move = false;
+ }),
+ )
+ .on_mouse_down(
+ MouseButton::Left,
+ cx.listener(move |this, _ev, _window, _cx| {
+ this.should_move = true;
+ }),
+ )
+ } else {
+ this
+ }
+ #[cfg(not(target_os = "linux"))]
+ this
+ }
+ PlatformKind::Windows => this.child(WindowsWindowControls::new(height)),
+ PlatformKind::Mac => this,
+ })
}
}
diff --git a/crates/ui/src/button.rs b/crates/ui/src/button.rs
index e15b2ea..8f35e1e 100644
--- a/crates/ui/src/button.rs
+++ b/crates/ui/src/button.rs
@@ -131,8 +131,8 @@ pub struct Button {
rounded: bool,
compact: bool,
- underline: bool,
caret: bool,
+ indicator: bool,
on_click: Option>,
on_hover: Option>,
@@ -162,7 +162,7 @@ impl Button {
variant: ButtonVariant::default(),
disabled: false,
selected: false,
- underline: false,
+ indicator: false,
compact: false,
caret: false,
rounded: false,
@@ -219,9 +219,9 @@ impl Button {
self
}
- /// Set true to show the underline indicator.
- pub fn underline(mut self) -> Self {
- self.underline = true;
+ /// Set true to show the indicator.
+ pub fn indicator(mut self) -> Self {
+ self.indicator = true;
self
}
@@ -455,6 +455,17 @@ impl RenderOnce for Button {
})
})
.text_color(normal_style.fg)
+ .when(self.indicator && !self.disabled, |this| {
+ this.child(
+ div()
+ .absolute()
+ .bottom_px()
+ .right_px()
+ .size_1()
+ .rounded_full()
+ .bg(gpui::green()),
+ )
+ })
.when(!self.disabled && !self.selected, |this| {
this.bg(normal_style.bg)
.hover(|this| {
@@ -470,17 +481,6 @@ impl RenderOnce for Button {
let selected_style = style.selected(cx);
this.bg(selected_style.bg).text_color(selected_style.fg)
})
- .when(self.selected && self.underline, |this| {
- this.child(
- div()
- .absolute()
- .bottom_0()
- .left_0()
- .h_px()
- .w_full()
- .bg(cx.theme().element_background),
- )
- })
.when(self.disabled, |this| {
let disabled_style = style.disabled(cx);
this.cursor_not_allowed()
diff --git a/crates/ui/src/dock_area/mod.rs b/crates/ui/src/dock_area/mod.rs
index 670882d..2312740 100644
--- a/crates/ui/src/dock_area/mod.rs
+++ b/crates/ui/src/dock_area/mod.rs
@@ -590,17 +590,13 @@ impl DockArea {
}
}
DockPlacement::Right => {
- if let Some(dock) = self.right_dock.as_ref() {
- dock.update(cx, |dock, cx| dock.add_panel(panel, window, cx))
- } else {
- self.set_right_dock(
- DockItem::tabs(vec![panel], None, &weak_self, window, cx),
- Some(px(320.)),
- true,
- window,
- cx,
- );
- }
+ self.set_right_dock(
+ DockItem::tabs(vec![panel], None, &weak_self, window, cx),
+ Some(px(320.)),
+ true,
+ window,
+ cx,
+ );
}
DockPlacement::Center => {
self.items
diff --git a/crates/ui/src/dock_area/stack_panel.rs b/crates/ui/src/dock_area/stack_panel.rs
index 79ba4ef..c2cb7f5 100644
--- a/crates/ui/src/dock_area/stack_panel.rs
+++ b/crates/ui/src/dock_area/stack_panel.rs
@@ -371,7 +371,6 @@ impl Focusable for StackPanel {
}
impl EventEmitter for StackPanel {}
-
impl EventEmitter for StackPanel {}
impl Render for StackPanel {
diff --git a/crates/ui/src/dock_area/tab_panel.rs b/crates/ui/src/dock_area/tab_panel.rs
index 7ed3817..6e412fa 100644
--- a/crates/ui/src/dock_area/tab_panel.rs
+++ b/crates/ui/src/dock_area/tab_panel.rs
@@ -479,12 +479,12 @@ impl TabPanel {
_window: &mut Window,
cx: &mut Context,
) -> Option