chore: improve web support (#36)

Reviewed-on: #36
This commit was merged in pull request #36.
This commit is contained in:
2026-07-30 08:47:30 +00:00
parent b518c729f6
commit 6d9284b37a
86 changed files with 1537 additions and 5215 deletions

View File

@@ -1,10 +1,9 @@
use std::time::Duration;
use anyhow::Error;
use gpui::{
AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Task, Window, div,
};
use instant::Duration;
use nostr_sdk::prelude::*;
use state::USER_KEYRING;
use theme::ActiveTheme;
@@ -155,12 +154,7 @@ impl Render for BackupPanel {
.text_color(cx.theme().text_muted)
.child(SharedString::from("Public Key:")),
)
.child(
Input::new(&self.npub_input)
.small()
.bordered(false)
.disabled(true),
),
.child(Input::new(&self.npub_input).small().disabled(true)),
)
.child(
v_flex()
@@ -173,12 +167,7 @@ impl Render for BackupPanel {
.text_color(cx.theme().text_muted)
.child(SharedString::from("Secret Key:")),
)
.child(
Input::new(&self.nsec_input)
.small()
.bordered(false)
.disabled(true),
),
.child(Input::new(&self.nsec_input).small().disabled(true)),
)
.child(
Button::new("copy")

View File

@@ -1,5 +1,4 @@
use std::collections::HashSet;
use std::time::Duration;
use anyhow::Error;
use gpui::prelude::FluentBuilder;
@@ -8,6 +7,7 @@ use gpui::{
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
Task, TextAlign, Window, div, rems,
};
use instant::Duration;
use nostr_sdk::prelude::*;
use person::PersonRegistry;
use smallvec::{SmallVec, smallvec};
@@ -17,6 +17,7 @@ use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants};
use ui::dock::{Panel, PanelEvent};
use ui::input::{Input, InputEvent, InputState};
use ui::scroll::ScrollableElement;
use ui::{Disableable, IconName, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
pub fn init(window: &mut Window, cx: &mut App) -> Entity<ContactListPanel> {
@@ -220,8 +221,7 @@ impl ContactListPanel {
.px_2()
.justify_between()
.rounded(cx.theme().radius)
.bg(cx.theme().secondary_background)
.text_color(cx.theme().secondary_foreground)
.hover(|this| this.bg(cx.theme().ghost_element_hover))
.child(
h_flex()
.gap_2()
@@ -283,79 +283,78 @@ impl Focusable for ContactListPanel {
impl Render for ContactListPanel {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex().p_3().gap_3().w_full().child(
v_flex()
.gap_2()
.flex_1()
.w_full()
.text_sm()
.child(
div()
.text_xs()
.font_semibold()
.text_color(cx.theme().text_muted)
.child(SharedString::from("New contact:")),
)
.child(
v_flex()
.gap_1()
.child(
h_flex()
.gap_1()
.w_full()
.child(
Input::new(&self.input)
.small()
.bordered(false)
.cleanable(true),
)
.child(
Button::new("add")
.icon(IconName::Plus)
.tooltip("Add contact")
.ghost()
.size(rems(2.))
.on_click(cx.listener(move |this, _, window, cx| {
this.add(window, cx);
})),
),
)
.when_some(self.error.as_ref(), |this, error| {
this.child(
div()
.italic()
.text_xs()
.text_color(cx.theme().text_danger)
.child(error.clone()),
v_flex()
.p_3()
.gap_3()
.w_full()
.overflow_y_scrollbar()
.child(
v_flex()
.gap_2()
.flex_1()
.w_full()
.text_sm()
.child(
div()
.text_xs()
.font_semibold()
.text_color(cx.theme().text_muted)
.child("New contact:"),
)
.child(
v_flex()
.gap_1()
.child(
h_flex()
.gap_1()
.w_full()
.child(Input::new(&self.input).small().cleanable(true))
.child(
Button::new("add")
.icon(IconName::Plus)
.tooltip("Add contact")
.ghost()
.size(rems(2.))
.on_click(cx.listener(move |this, _, window, cx| {
this.add(window, cx);
})),
),
)
}),
)
.map(|this| {
if self.contacts.is_empty() {
this.child(self.render_empty(window, cx))
} else {
this.child(
v_flex()
.gap_1()
.flex_1()
.w_full()
.children(self.render_list_items(cx)),
)
}
})
.child(
Button::new("submit")
.icon(IconName::CheckCircle)
.label("Update")
.primary()
.small()
.font_semibold()
.loading(self.updating)
.disabled(self.updating)
.on_click(cx.listener(move |this, _ev, window, cx| {
this.update(window, cx);
})),
),
)
.when_some(self.error.as_ref(), |this, error| {
this.child(
div()
.italic()
.text_xs()
.text_color(cx.theme().text_danger)
.child(error.clone()),
)
}),
)
.map(|this| {
if self.contacts.is_empty() {
this.child(self.render_empty(window, cx))
} else {
this.child(
v_flex()
.gap_1()
.flex_1()
.w_full()
.children(self.render_list_items(cx)),
)
}
})
.child(
Button::new("submit")
.icon(IconName::CheckCircle)
.label("Update")
.primary()
.font_semibold()
.loading(self.updating)
.disabled(self.updating)
.on_click(cx.listener(move |this, _ev, window, cx| {
this.update(window, cx);
})),
),
)
}
}

View File

@@ -1,6 +1,7 @@
use anyhow::Error;
use gpui::{
AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
IntoElement, ParentElement, Render, SharedString, Styled, Window, div, svg,
IntoElement, ParentElement, Render, SharedString, Styled, Task, Window, div, svg,
};
use state::NostrRegistry;
use theme::ActiveTheme;
@@ -8,8 +9,8 @@ use ui::button::{Button, ButtonVariants};
use ui::dock::{DockPlacement, Panel, PanelEvent};
use ui::{Icon, IconName, Sizable, StyledExt, h_flex, v_flex};
use crate::Workspace;
use crate::panels::profile;
use crate::{Command, Workspace};
pub fn init(window: &mut Window, cx: &mut App) -> Entity<GreeterPanel> {
cx.new(|cx| GreeterPanel::new(window, cx))
@@ -18,6 +19,7 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity<GreeterPanel> {
pub struct GreeterPanel {
name: SharedString,
focus_handle: FocusHandle,
tasks: Vec<Task<Result<(), Error>>>,
}
impl GreeterPanel {
@@ -25,6 +27,7 @@ impl GreeterPanel {
Self {
name: "Onboarding".into(),
focus_handle: cx.focus_handle(),
tasks: vec![],
}
}
@@ -32,7 +35,7 @@ impl GreeterPanel {
let nostr = NostrRegistry::global(cx);
if let Some(public_key) = nostr.read(cx).current_user() {
cx.spawn_in(window, async move |_this, cx| {
self.tasks.push(cx.spawn_in(window, async move |_this, cx| {
cx.update(|window, cx| {
Workspace::add_panel(
profile::init(public_key, window, cx),
@@ -42,8 +45,9 @@ impl GreeterPanel {
);
})
.ok();
})
.detach();
Ok(())
}));
}
}
}
@@ -142,17 +146,20 @@ impl Render for GreeterPanel {
.ghost()
.small()
.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)
})),
)
.child(
Button::new("invite")
.icon(Icon::new(IconName::Invite))
.label("Invite friends")
Button::new("theme")
.icon(Icon::new(IconName::Moon))
.label("Change theme")
.ghost()
.small()
.justify_start(),
.justify_start()
.on_click(cx.listener(move |_, _, _, cx| {
cx.dispatch_action(&Command::ToggleTheme);
})),
),
),
),

View File

@@ -1,5 +1,4 @@
use std::collections::HashSet;
use std::time::Duration;
use anyhow::{Error, anyhow};
use gpui::prelude::FluentBuilder;
@@ -8,6 +7,7 @@ use gpui::{
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
Task, TextAlign, Window, div, rems,
};
use instant::Duration;
use nostr_sdk::prelude::*;
use smallvec::{SmallVec, smallvec};
use state::NostrRegistry;
@@ -320,12 +320,7 @@ impl Render for MessagingRelayPanel {
h_flex()
.gap_1()
.w_full()
.child(
Input::new(&self.input)
.small()
.bordered(false)
.cleanable(true),
)
.child(Input::new(&self.input).small().cleanable(true))
.child(
Button::new("add")
.icon(IconName::Plus)
@@ -365,7 +360,6 @@ impl Render for MessagingRelayPanel {
.icon(IconName::CheckCircle)
.label("Update")
.primary()
.small()
.font_semibold()
.loading(self.updating)
.disabled(self.updating)

View File

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

View File

@@ -1,5 +1,4 @@
use std::str::FromStr;
use std::time::Duration;
use anyhow::{Context as AnyhowContext, Error};
use gpui::{
@@ -7,6 +6,7 @@ use gpui::{
Focusable, IntoElement, ParentElement, PathPromptOptions, Render, SharedString, Styled, Task,
Window, div,
};
use instant::Duration;
use nostr_sdk::prelude::*;
use person::{Person, PersonRegistry, shorten_pubkey};
use settings::AppSettings;
@@ -132,7 +132,7 @@ impl ProfilePanel {
cx.notify();
if status {
cx.spawn_in(window, async move |this, cx| {
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
cx.background_executor().timer(Duration::from_secs(2)).await;
// Reset the copied state after a delay
@@ -143,8 +143,9 @@ impl ProfilePanel {
.ok();
})
.ok();
})
.detach();
Ok(())
}));
}
}
@@ -352,7 +353,7 @@ impl Render for ProfilePanel {
.text_color(cx.theme().text_muted)
.child(SharedString::from("What should people call you?")),
)
.child(Input::new(&self.name_input).bordered(false).small()),
.child(Input::new(&self.name_input).small()),
)
.child(
v_flex()
@@ -363,7 +364,7 @@ impl Render for ProfilePanel {
.text_color(cx.theme().text_muted)
.child(SharedString::from("A short introduction about you:")),
)
.child(Input::new(&self.bio_input).bordered(false).small()),
.child(Input::new(&self.bio_input).small()),
)
.child(
v_flex()
@@ -374,7 +375,7 @@ impl Render for ProfilePanel {
.text_color(cx.theme().text_muted)
.child(SharedString::from("Website:")),
)
.child(Input::new(&self.website_input).bordered(false).small()),
.child(Input::new(&self.website_input).small()),
)
.child(
v_flex()
@@ -418,7 +419,6 @@ impl Render for ProfilePanel {
.icon(IconName::CheckCircle)
.label("Update")
.primary()
.small()
.font_semibold()
.loading(self.updating)
.disabled(self.updating)

View File

@@ -1,5 +1,4 @@
use std::collections::HashSet;
use std::time::Duration;
use anyhow::{Error, anyhow};
use gpui::prelude::FluentBuilder;
@@ -8,6 +7,7 @@ use gpui::{
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
Task, TextAlign, Window, div, px, rems,
};
use instant::Duration;
use nostr_sdk::prelude::*;
use serde::Deserialize;
use smallvec::{SmallVec, smallvec};
@@ -216,7 +216,7 @@ impl RelayListPanel {
self.set_updating(true, cx);
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let event = EventBuilder::relay_list(relays)
let event = nip65::RelayList::new(relays)
.finalize_async(&signer)
.await?;
@@ -371,12 +371,7 @@ impl Render for RelayListPanel {
h_flex()
.gap_1()
.w_full()
.child(
Input::new(&self.input)
.small()
.bordered(false)
.cleanable(true),
)
.child(Input::new(&self.input).small().cleanable(true))
.child(
Button::new("metadata")
.map(|this| {
@@ -434,7 +429,6 @@ impl Render for RelayListPanel {
.icon(IconName::CheckCircle)
.label("Update")
.primary()
.small()
.font_semibold()
.loading(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 trashes = chat.read(cx).trashes();
if let Some(message) = trashes.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 trashes = chat.read(cx).trashes();
if let Some(message) = trashes.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)),
)
}
}