wip: revamp title bar elements
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 12m59s
Rust / build (ubuntu-latest, stable) (pull_request) Failing after 9m53s
Rust / build (macos-latest, stable) (push) Has been cancelled
Rust / build (windows-latest, stable) (push) Has been cancelled
Rust / build (macos-latest, stable) (pull_request) Has been cancelled
Rust / build (windows-latest, stable) (pull_request) Has been cancelled

This commit is contained in:
2026-02-23 15:48:35 +07:00
parent 31df6d7937
commit 2ec98e14d0
32 changed files with 595 additions and 1177 deletions

View File

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

View File

@@ -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<EncryptionPanel> {
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 {
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<PanelEvent> 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<Self>) -> impl IntoElement {
v_flex()
.size_full()
.items_center()
.justify_center()
.p_2()
.gap_10()
}
}

View File

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

View File

@@ -1,4 +1,5 @@
pub mod connect;
pub mod encryption_key;
pub mod greeter;
pub mod import;
pub mod messaging_relays;

View File

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

View File

@@ -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<Workspace> {
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<TitleBar>,
@@ -41,7 +52,7 @@ impl Workspace {
fn new(window: &mut Window, cx: &mut Context<Self>) -> 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<Self>) {
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<Self>) -> 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<Self>) -> impl IntoElement {
h_flex().h(TITLEBAR_HEIGHT).flex_shrink_0()
fn titlebar_right(&mut self, _window: &mut Window, cx: &Context<Self>) -> 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(