finish ui

This commit is contained in:
2026-07-30 14:23:07 +07:00
parent 5b60dc1453
commit ed8ce41583
12 changed files with 109 additions and 298 deletions

View File

@@ -29,7 +29,7 @@ nostr = { git = "https://github.com/rust-nostr/nostr", features = [ "nip59", "ni
# Others # Others
anyhow = "1.0.44" anyhow = "1.0.44"
chrono = "0.4.38" chrono = { version = "0.4.38", features = ["wasmbind"] }
futures = "0.3" futures = "0.3"
itertools = "0.13.0" itertools = "0.13.0"
log = "0.4" log = "0.4"

View File

@@ -87,7 +87,7 @@ pub trait StyledExt: Styled + Sized {
.border_1() .border_1()
.border_color(cx.theme().border) .border_color(cx.theme().border)
.shadow_md() .shadow_md()
.rounded(cx.theme().radius) .rounded(cx.theme().radius_lg)
} }
} }

View File

@@ -8,8 +8,7 @@ use device::{DeviceEvent, DeviceRegistry};
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
Action, App, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement, ParentElement, Action, App, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement, ParentElement,
Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, Window, div, Render, SharedString, Styled, Subscription, Task, Window, div, image_cache, px,
image_cache, px, relative,
}; };
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use person::{PersonRegistry, shorten_pubkey}; use person::{PersonRegistry, shorten_pubkey};
@@ -27,7 +26,7 @@ use ui::{Icon, IconName, Root, Sizable, TitleBar, WindowExtension, h_flex, v_fle
use crate::dialogs::import::ImportIdentity; use crate::dialogs::import::ImportIdentity;
use crate::dialogs::restore::RestoreEncryption; use crate::dialogs::restore::RestoreEncryption;
use crate::dialogs::settings; use crate::dialogs::settings;
use crate::panels::{backup, contact_list, greeter, messaging_relays, profile, relay_list, trash}; use crate::panels::{backup, contact_list, greeter, messaging_relays, profile, relay_list};
use crate::sidebar::Sidebar; use crate::sidebar::Sidebar;
mod dialogs; mod dialogs;
@@ -277,7 +276,7 @@ impl Workspace {
self.dock.update(cx, |this, cx| { self.dock.update(cx, |this, cx| {
this.add_panel( this.add_panel(
Arc::new(profile::init(public_key, window, cx)), Arc::new(profile::init(public_key, window, cx)),
DockPlacement::Right, DockPlacement::Left,
window, window,
cx, cx,
); );
@@ -288,7 +287,7 @@ impl Workspace {
self.dock.update(cx, |this, cx| { self.dock.update(cx, |this, cx| {
this.add_panel( this.add_panel(
Arc::new(contact_list::init(window, cx)), Arc::new(contact_list::init(window, cx)),
DockPlacement::Right, DockPlacement::Left,
window, window,
cx, cx,
); );
@@ -298,7 +297,7 @@ impl Workspace {
self.dock.update(cx, |this, cx| { self.dock.update(cx, |this, cx| {
this.add_panel( this.add_panel(
Arc::new(backup::init(window, cx)), Arc::new(backup::init(window, cx)),
DockPlacement::Right, DockPlacement::Left,
window, window,
cx, cx,
); );
@@ -308,7 +307,7 @@ impl Workspace {
self.dock.update(cx, |this, cx| { self.dock.update(cx, |this, cx| {
this.add_panel( this.add_panel(
Arc::new(messaging_relays::init(window, cx)), Arc::new(messaging_relays::init(window, cx)),
DockPlacement::Right, DockPlacement::Left,
window, window,
cx, cx,
); );
@@ -435,10 +434,11 @@ impl Workspace {
let import = cx.new(|cx| ImportIdentity::new(window, cx)); let import = cx.new(|cx| ImportIdentity::new(window, cx));
window.open_modal(cx, move |this, _window, _cx| { window.open_modal(cx, move |this, _window, _cx| {
this.width(px(420.)) this.width(px(450.))
.show_close(false) .show_close(false)
.overlay_closable(false) .overlay_closable(false)
.title("Import Identity") .keyboard(false)
.title("Onboarding")
.child(import.clone()) .child(import.clone())
}); });
} }
@@ -598,9 +598,7 @@ impl Workspace {
fn titlebar_right(&mut self, cx: &mut Context<Self>) -> impl IntoElement { fn titlebar_right(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
let chat = ChatRegistry::global(cx); let chat = ChatRegistry::global(cx);
let trash_messages = chat.read(cx).count_trash_messages(cx); let nip4e_enabled = AppSettings::get_nip4e(cx);
let is_nip4e_enabled = AppSettings::get_nip4e(cx);
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let Some(public_key) = nostr.read(cx).current_user() else { let Some(public_key) = nostr.read(cx).current_user() else {
@@ -614,39 +612,7 @@ impl Workspace {
h_flex() h_flex()
.when(!cx.theme().platform.is_mac(), |this| this.pr_2()) .when(!cx.theme().platform.is_mac(), |this| this.pr_2())
.gap_2() .gap_2()
.when(trash_messages > 0, |this| { .when(nip4e_enabled, |this| {
this.child(
h_flex()
.id("trash-messages")
.h_6()
.px_1()
.gap_1()
.rounded(cx.theme().radius)
.hover(|this| this.bg(cx.theme().ghost_element_hover))
.child(
Icon::new(IconName::Warning)
.small()
.text_color(cx.theme().text_danger),
)
.child(
div()
.text_xs()
.line_height(relative(1.))
.child(format!("{trash_messages}")),
)
.on_click(move |_ev, window, cx| {
cx.stop_propagation();
// Add the trash panel to the center workspace
Self::add_panel(
trash::init(window, cx),
DockPlacement::Center,
window,
cx,
);
}),
)
})
.when(is_nip4e_enabled, |this| {
this.child( this.child(
Button::new("key") Button::new("key")
.icon(IconName::UserKey) .icon(IconName::UserKey)
@@ -738,17 +704,7 @@ impl Workspace {
.w_full() .w_full()
.text_sm() .text_sm()
.justify_between() .justify_between()
.child( .child(url.clone())
h_flex()
.gap_2()
.child(
div()
.size_1p5()
.rounded_full()
.bg(cx.theme().icon_accent),
)
.child(url.clone()),
)
.child( .child(
div() div()
.text_xs() .text_xs()
@@ -760,11 +716,6 @@ impl Workspace {
// Footer // Footer
menu.separator() menu.separator()
.menu_with_icon(
"Reload",
IconName::Refresh,
Box::new(Command::RefreshMessagingRelays),
)
.menu_with_icon( .menu_with_icon(
"Manage gossip relays", "Manage gossip relays",
IconName::Relay, IconName::Relay,
@@ -772,9 +723,15 @@ impl Workspace {
) )
.menu_with_icon( .menu_with_icon(
"Manage messaging relays", "Manage messaging relays",
IconName::Settings, IconName::Relay,
Box::new(Command::ShowMessaging), Box::new(Command::ShowMessaging),
) )
.separator()
.menu_with_icon(
"Reload",
IconName::Refresh,
Box::new(Command::RefreshMessagingRelays),
)
}), }),
) )
} }

View File

@@ -17,6 +17,7 @@ use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::dock::{Panel, PanelEvent}; use ui::dock::{Panel, PanelEvent};
use ui::input::{Input, InputEvent, InputState}; use ui::input::{Input, InputEvent, InputState};
use ui::scroll::ScrollableElement;
use ui::{Disableable, IconName, Sizable, StyledExt, WindowExtension, h_flex, v_flex}; use ui::{Disableable, IconName, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
pub fn init(window: &mut Window, cx: &mut App) -> Entity<ContactListPanel> { pub fn init(window: &mut Window, cx: &mut App) -> Entity<ContactListPanel> {
@@ -220,8 +221,7 @@ impl ContactListPanel {
.px_2() .px_2()
.justify_between() .justify_between()
.rounded(cx.theme().radius) .rounded(cx.theme().radius)
.bg(cx.theme().secondary_background) .hover(|this| this.bg(cx.theme().ghost_element_hover))
.text_color(cx.theme().secondary_foreground)
.child( .child(
h_flex() h_flex()
.gap_2() .gap_2()
@@ -283,7 +283,12 @@ impl Focusable for ContactListPanel {
impl Render for ContactListPanel { impl Render for ContactListPanel {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex().p_3().gap_3().w_full().child( v_flex()
.p_3()
.gap_3()
.w_full()
.overflow_y_scrollbar()
.child(
v_flex() v_flex()
.gap_2() .gap_2()
.flex_1() .flex_1()
@@ -294,7 +299,7 @@ impl Render for ContactListPanel {
.text_xs() .text_xs()
.font_semibold() .font_semibold()
.text_color(cx.theme().text_muted) .text_color(cx.theme().text_muted)
.child(SharedString::from("New contact:")), .child("New contact:"),
) )
.child( .child(
v_flex() v_flex()
@@ -343,7 +348,6 @@ impl Render for ContactListPanel {
.icon(IconName::CheckCircle) .icon(IconName::CheckCircle)
.label("Update") .label("Update")
.primary() .primary()
.small()
.font_semibold() .font_semibold()
.loading(self.updating) .loading(self.updating)
.disabled(self.updating) .disabled(self.updating)

View File

@@ -9,8 +9,8 @@ use ui::button::{Button, ButtonVariants};
use ui::dock::{DockPlacement, Panel, PanelEvent}; use ui::dock::{DockPlacement, Panel, PanelEvent};
use ui::{Icon, IconName, Sizable, StyledExt, h_flex, v_flex}; use ui::{Icon, IconName, Sizable, StyledExt, h_flex, v_flex};
use crate::Workspace;
use crate::panels::profile; use crate::panels::profile;
use crate::{Command, Workspace};
pub fn init(window: &mut Window, cx: &mut App) -> Entity<GreeterPanel> { pub fn init(window: &mut Window, cx: &mut App) -> Entity<GreeterPanel> {
cx.new(|cx| GreeterPanel::new(window, cx)) cx.new(|cx| GreeterPanel::new(window, cx))
@@ -146,17 +146,20 @@ impl Render for GreeterPanel {
.ghost() .ghost()
.small() .small()
.justify_start() .justify_start()
.on_click(cx.listener(move |this, _ev, window, cx| { .on_click(cx.listener(move |this, _, window, cx| {
this.add_profile_panel(window, cx) this.add_profile_panel(window, cx)
})), })),
) )
.child( .child(
Button::new("invite") Button::new("theme")
.icon(Icon::new(IconName::Invite)) .icon(Icon::new(IconName::Moon))
.label("Invite friends") .label("Change theme")
.ghost() .ghost()
.small() .small()
.justify_start(), .justify_start()
.on_click(cx.listener(move |_, _, _, cx| {
cx.dispatch_action(&Command::ToggleTheme);
})),
), ),
), ),
), ),

View File

@@ -360,7 +360,6 @@ impl Render for MessagingRelayPanel {
.icon(IconName::CheckCircle) .icon(IconName::CheckCircle)
.label("Update") .label("Update")
.primary() .primary()
.small()
.font_semibold() .font_semibold()
.loading(self.updating) .loading(self.updating)
.disabled(self.updating) .disabled(self.updating)

View File

@@ -4,4 +4,3 @@ pub mod greeter;
pub mod messaging_relays; pub mod messaging_relays;
pub mod profile; pub mod profile;
pub mod relay_list; pub mod relay_list;
pub mod trash;

View File

@@ -419,7 +419,6 @@ impl Render for ProfilePanel {
.icon(IconName::CheckCircle) .icon(IconName::CheckCircle)
.label("Update") .label("Update")
.primary() .primary()
.small()
.font_semibold() .font_semibold()
.loading(self.updating) .loading(self.updating)
.disabled(self.updating) .disabled(self.updating)

View File

@@ -429,7 +429,6 @@ impl Render for RelayListPanel {
.icon(IconName::CheckCircle) .icon(IconName::CheckCircle)
.label("Update") .label("Update")
.primary() .primary()
.small()
.font_semibold() .font_semibold()
.loading(self.updating) .loading(self.updating)
.disabled(self.updating) .disabled(self.updating)

View File

@@ -1,152 +0,0 @@
use chat::ChatRegistry;
use gpui::{
AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
Focusable, InteractiveElement, IntoElement, ListAlignment, ListState, ParentElement, Render,
SharedString, Styled, Window, div, list, px, relative,
};
use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants};
use ui::dock::{Panel, PanelEvent};
use ui::scroll::Scrollbar;
use ui::{Icon, IconName, Sizable, h_flex, v_flex};
pub fn init(window: &mut Window, cx: &mut App) -> Entity<TrashPanel> {
cx.new(|cx| TrashPanel::new(window, cx))
}
pub struct TrashPanel {
name: SharedString,
focus_handle: FocusHandle,
/// List state for messages
list_state: ListState,
}
impl TrashPanel {
fn new(_window: &mut Window, cx: &mut App) -> Self {
let chat = ChatRegistry::global(cx);
let count = chat.read(cx).count_trash_messages(cx);
let list_state = ListState::new(count, ListAlignment::Bottom, px(1024.));
Self {
name: "Trash".into(),
focus_handle: cx.focus_handle(),
list_state,
}
}
fn copy(&self, ix: usize, cx: &App) {
let chat = ChatRegistry::global(cx);
let trash_events = chat.read(cx).trash();
if let Some(message) = trash_events.read(cx).iter().nth(ix) {
let item = ClipboardItem::new_string(message.raw_event.to_string());
cx.write_to_clipboard(item);
}
}
fn render_list_item(
&mut self,
ix: usize,
_window: &mut Window,
cx: &mut Context<Self>,
) -> AnyElement {
let chat = ChatRegistry::global(cx);
let trash_events = chat.read(cx).trash();
if let Some(message) = trash_events.read(cx).iter().nth(ix) {
v_flex()
.id(ix)
.p_2()
.w_full()
.child(
v_flex()
.p_2()
.w_full()
.gap_1()
.rounded(cx.theme().radius_lg)
.bg(cx.theme().surface_background)
.text_sm()
.child(
div()
.text_color(cx.theme().text_danger)
.child(message.reason.clone()),
)
.child(
h_flex()
.h_10()
.w_full()
.px_2()
.justify_between()
.bg(cx.theme().elevated_surface_background)
.border_1()
.border_color(cx.theme().border)
.rounded(cx.theme().radius)
.child(
div()
.truncate()
.text_ellipsis()
.text_xs()
.line_height(relative(1.))
.child(message.raw_event.clone()),
)
.child(
Button::new(format!("copy-{ix}"))
.icon(IconName::Copy)
.ghost()
.small()
.on_click(cx.listener(move |this, _ev, _window, cx| {
this.copy(ix, cx);
})),
),
),
)
.into_any_element()
} else {
div().id(ix).into_any_element()
}
}
}
impl Panel for TrashPanel {
fn panel_id(&self) -> SharedString {
self.name.clone()
}
fn title(&self, _cx: &App) -> AnyElement {
h_flex()
.gap_1()
.text_sm()
.child(Icon::new(IconName::Warning).small())
.child("Errors")
.into_any_element()
}
}
impl EventEmitter<PanelEvent> for TrashPanel {}
impl Focusable for TrashPanel {
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
self.focus_handle.clone()
}
}
impl Render for TrashPanel {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex().size_full().relative().child(
v_flex()
.flex_1()
.relative()
.child(
list(
self.list_state.clone(),
cx.processor(move |this, ix, window, cx| {
this.render_list_item(ix, window, cx)
}),
)
.size_full(),
)
.child(Scrollbar::vertical(&self.list_state)),
)
}
}

View File

@@ -691,15 +691,16 @@ impl Render for Sidebar {
this.child( this.child(
div() div()
.absolute() .absolute()
.bottom_0() .bottom_2()
.left_0() .left_0()
.h_9() .h_9()
.w_full() .w_full()
.p_4() .px_4()
.child( .child(
Button::new("create") Button::new("create")
.label(button_label) .label(button_label)
.primary() .primary()
.rounded()
.shadow_md() .shadow_md()
.on_click(cx.listener(move |this, _ev, window, cx| { .on_click(cx.listener(move |this, _ev, window, cx| {
this.create_room(window, cx); this.create_room(window, cx);

View File

@@ -31,8 +31,10 @@ pub fn run() -> Result<(), JsValue> {
#[cfg(target_family = "wasm")] #[cfg(target_family = "wasm")]
gpui_platform::web_init(); gpui_platform::web_init();
#[cfg(not(target_family = "wasm"))] #[cfg(not(target_family = "wasm"))]
let app = gpui_platform::application(); let app = gpui_platform::application();
#[cfg(target_family = "wasm")] #[cfg(target_family = "wasm")]
let app = { let app = {
let app = gpui_platform::single_threaded_web(); let app = gpui_platform::single_threaded_web();