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

@@ -17,6 +17,7 @@ person = { path = "../person" }
gpui.workspace = true
nostr-sdk.workspace = true
instant.workspace = true
nostr-connect.workspace = true
anyhow.workspace = true

View File

@@ -1,11 +1,10 @@
use std::time::Duration;
use anyhow::{Error, anyhow};
use gpui::prelude::FluentBuilder;
use gpui::{
AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled,
Subscription, Task, Window, div,
};
use instant::Duration;
use nostr_connect::prelude::*;
use state::{CoopAuthUrlHandler, NostrRegistry, USER_KEYRING};
use theme::ActiveTheme;
@@ -36,7 +35,7 @@ pub struct ImportIdentity {
impl ImportIdentity {
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let key_input = cx.new(|cx| InputState::new(window, cx));
let key_input = cx.new(|cx| InputState::new(window, cx).placeholder("nsec or bunker://"));
let pass_input = cx.new(|cx| InputState::new(window, cx).masked(true));
let error = cx.new(|_| None);
@@ -135,7 +134,6 @@ impl ImportIdentity {
})?;
}
}
Ok(())
}));
}
@@ -146,7 +144,7 @@ impl ImportIdentity {
let password = uri.to_string();
let save = cx.write_credentials(USER_KEYRING, "bunker", password.as_bytes());
cx.spawn_in(window, async move |_this, cx| {
self.tasks.push(cx.spawn_in(window, async move |_this, cx| {
let keys = master_keys.await;
let timeout = Duration::from_secs(30);
@@ -162,9 +160,8 @@ impl ImportIdentity {
cx.notify();
});
Ok::<(), anyhow::Error>(())
})
.detach();
Ok(())
}));
}
fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) {
@@ -209,40 +206,45 @@ impl Render for ImportIdentity {
v_flex()
.size_full()
.gap_2()
.gap_4()
.text_sm()
.child(
v_flex()
.gap_1()
.text_color(cx.theme().text_muted)
.child("Continue with existing key or bunker connection")
.child(Input::new(&self.key_input)),
.gap_2()
.child(
v_flex()
.gap_1()
.text_color(cx.theme().text_muted)
.child("Continue with existing key or bunker connection")
.child(Input::new(&self.key_input)),
)
.when(require_password, |this| {
this.child(
v_flex()
.gap_1()
.text_color(cx.theme().text_muted)
.child("Decrypt Password:")
.child(Input::new(&self.pass_input)),
)
})
.when(key_warning, |this| {
this.child(
div()
.text_xs()
.text_color(cx.theme().text_warning)
.child(div().font_semibold().child("Warning"))
.child(div().child(MSG)),
)
}),
)
.when(require_password, |this| {
this.child(
v_flex()
.gap_1()
.text_color(cx.theme().text_muted)
.child("Decrypt Password:")
.child(Input::new(&self.pass_input)),
)
})
.when(key_warning, |this| {
this.child(
div()
.v_flex()
.text_xs()
.text_color(cx.theme().text_warning)
.child(div().font_semibold().child("Warning"))
.child(div().child(MSG)),
)
})
.child(
Button::new("login")
.label("Continue")
.primary()
.font_semibold()
.loading(self.loading)
.disabled(self.loading)
.on_click(cx.listener(move |this, _, window, cx| {
.on_click(cx.listener(move |this, _ev, window, cx| {
this.login(window, cx);
})),
)

View File

@@ -1,4 +1,4 @@
use std::time::Duration;
use instant::Duration;
use anyhow::Error;
use device::DeviceRegistry;

View File

@@ -1,5 +1,4 @@
use std::collections::HashMap;
use std::time::Duration;
use anyhow::Error;
use common::TimestampExt;
@@ -8,6 +7,7 @@ use gpui::{
App, AppContext, Context, Div, Entity, InteractiveElement, IntoElement, ParentElement, Render,
SharedString, Styled, Subscription, Task, Window, div, px, relative, uniform_list,
};
use instant::Duration;
use nostr_sdk::prelude::*;
use person::{Person, PersonRegistry, shorten_pubkey};
use smallvec::{SmallVec, smallvec};
@@ -55,7 +55,7 @@ impl Screening {
window.close_all_modals(cx);
}));
cx.defer_in(window, move |this, _window, cx| {
cx.defer_in(window, |this, _window, cx| {
this.check_contact(cx);
this.check_wot(cx);
this.check_last_activity(cx);
@@ -234,7 +234,8 @@ impl Screening {
}
.to_tag();
let event = EventBuilder::report(vec![tag], "")
let event = EventBuilder::new(Kind::Reporting, "")
.tag(tag)
.finalize_async(&signer)
.await?;

View File

@@ -3,7 +3,7 @@ use gpui::{
App, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled,
Window, div, px,
};
use settings::{AppSettings, AuthMode};
use settings::AppSettings;
use theme::{ActiveTheme, Theme, ThemeMode};
use ui::button::{Button, ButtonVariants};
use ui::group_box::{GroupBox, GroupBoxVariants};
@@ -60,13 +60,11 @@ impl Render for Preferences {
const AVATAR: &str = "Hide all avatar pictures to improve performance.";
const MODE: &str = "Use the selected light or dark theme, or to follow the OS.";
const NIP4E: &str = "Use a dedicated key to encrypt and decrypt messages.";
const AUTH: &str = "Choose the authentication behavior for relays.";
const RESET: &str = "Reset the theme to the default one.";
let screening = AppSettings::get_screening(cx);
let hide_avatar = AppSettings::get_hide_avatar(cx);
let nip4e = AppSettings::get_nip4e(cx);
let auth_mode = AppSettings::get_auth_mode(cx);
let theme_mode = AppSettings::get_theme_mode(cx);
v_flex()
@@ -93,52 +91,6 @@ impl Render for Preferences {
.on_click(move |_, _window, cx| {
AppSettings::update_hide_avatar(!hide_avatar, cx);
}),
)
.child(
h_flex()
.gap_3()
.justify_between()
.child(
v_flex()
.child(
div()
.text_sm()
.child(SharedString::from("Relay authentication")),
)
.child(
div()
.text_xs()
.text_color(cx.theme().text_muted)
.child(SharedString::from(AUTH)),
),
)
.child(
Button::new("auth")
.label(auth_mode.to_string())
.ghost_alt()
.small()
.dropdown_menu(|this, _window, _cx| {
this.min_w(px(256.))
.item(
PopupMenuItem::new("Auto authentication").on_click(
|_ev, _window, cx| {
AppSettings::update_auth_mode(
AuthMode::Auto,
cx,
);
},
),
)
.item(PopupMenuItem::new("Ask every time").on_click(
|_ev, _window, cx| {
AppSettings::update_auth_mode(
AuthMode::Manual,
cx,
);
},
))
}),
),
),
)
.child(

View File

@@ -1,14 +1,14 @@
use std::sync::Arc;
use ::settings::AppSettings;
use anyhow::Error;
use chat::{ChatEvent, ChatRegistry};
use common::{CoopImageCache, download_dir};
use device::{DeviceEvent, DeviceRegistry};
use gpui::prelude::FluentBuilder;
use gpui::{
Action, App, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement, ParentElement,
Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Window, div,
image_cache, px, relative,
Render, SharedString, Styled, Subscription, Task, Window, div, image_cache, px,
};
use nostr_sdk::prelude::*;
use person::{PersonRegistry, shorten_pubkey};
@@ -26,7 +26,8 @@ use ui::{Icon, IconName, Root, Sizable, TitleBar, WindowExtension, h_flex, v_fle
use crate::dialogs::import::ImportIdentity;
use crate::dialogs::restore::RestoreEncryption;
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;
mod dialogs;
mod panels;
@@ -59,12 +60,16 @@ enum Command {
}
pub struct Workspace {
sidebar: Entity<Sidebar>,
/// App's Dock Area
dock: Entity<DockArea>,
/// App's Image Cache
image_cache: Entity<CoopImageCache>,
/// Async tasks
tasks: Vec<Task<Result<(), Error>>>,
/// Event subscriptions
_subscriptions: SmallVec<[Subscription; 6]>,
}
@@ -75,20 +80,12 @@ impl Workspace {
let device = DeviceRegistry::global(cx);
let nostr = NostrRegistry::global(cx);
let sidebar = cx.new(|cx| Sidebar::new(window, cx));
let dock = cx.new(|cx| DockArea::new(window, cx));
let image_cache = CoopImageCache::new(IMAGE_CACHE_SIZE, cx);
let mut subscriptions = smallvec![];
subscriptions.push(
// Observe sign in state changes
cx.observe_in(&nostr, window, move |this, nostr, window, cx| {
if nostr.read(cx).current_user().is_some() {
this.set_center_layout(window, cx);
}
}),
);
subscriptions.push(
// Observe system appearance and update theme
cx.observe_window_appearance(window, |_this, window, cx| {
@@ -101,11 +98,7 @@ impl Workspace {
cx.subscribe_in(&nostr, window, move |this, _state, event, window, cx| {
match event {
StateEvent::SignerChanged => {
this.set_center_layout(window, cx);
cx.defer_in(window, |_this, window, cx| {
window.close_all_modals(cx);
});
window.close_all_modals(cx);
}
StateEvent::NoSigner => {
this.import_identity(window, cx);
@@ -226,25 +219,22 @@ impl Workspace {
}),
);
subscriptions.push(
// Observe the chat registry
cx.observe(&chat, move |this, chat, cx| {
let ids = this.panel_ids(cx);
chat.update(cx, |this, cx| {
this.refresh_rooms(&ids, cx);
});
}),
);
// Set the layout at the end of cycle
cx.defer_in(window, |this, window, cx| {
this.set_layout(window, cx);
let dock = this.dock.downgrade();
let greeter = Arc::new(greeter::init(window, cx));
let tabs = DockItem::tabs(vec![greeter], None, &dock, window, cx);
let center = DockItem::split(Axis::Vertical, vec![tabs], &dock, window, cx);
this.dock.update(cx, |this, cx| {
this.set_center(center, window, cx);
});
});
Self {
sidebar,
dock,
image_cache,
tasks: vec![],
_subscriptions: subscriptions,
}
}
@@ -265,40 +255,6 @@ impl Workspace {
}
}
/// Get all panel ids
fn panel_ids(&self, cx: &App) -> Vec<u64> {
self.dock
.read(cx)
.items
.panel_ids(cx)
.into_iter()
.filter_map(|panel| panel.parse::<u64>().ok())
.collect()
}
/// Set the dock layout
fn set_layout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let left = DockItem::panel(Arc::new(sidebar::init(window, cx)));
// Update the dock layout with sidebar on the left
self.dock.update(cx, |this, cx| {
this.set_left_dock(left, Some(SIDEBAR_WIDTH), true, window, cx);
});
}
/// Set the center dock layout
fn set_center_layout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let dock = self.dock.downgrade();
let greeter = Arc::new(greeter::init(window, cx));
let tabs = DockItem::tabs(vec![greeter], None, &dock, window, cx);
let center = DockItem::split(Axis::Vertical, vec![tabs], &dock, window, cx);
// Update the layout with center dock
self.dock.update(cx, |this, cx| {
this.set_center(center, window, cx);
});
}
/// Handle command events
fn on_command(&mut self, command: &Command, window: &mut Window, cx: &mut Context<Self>) {
match command {
@@ -320,7 +276,7 @@ impl Workspace {
self.dock.update(cx, |this, cx| {
this.add_panel(
Arc::new(profile::init(public_key, window, cx)),
DockPlacement::Right,
DockPlacement::Left,
window,
cx,
);
@@ -331,7 +287,7 @@ impl Workspace {
self.dock.update(cx, |this, cx| {
this.add_panel(
Arc::new(contact_list::init(window, cx)),
DockPlacement::Right,
DockPlacement::Left,
window,
cx,
);
@@ -341,7 +297,7 @@ impl Workspace {
self.dock.update(cx, |this, cx| {
this.add_panel(
Arc::new(backup::init(window, cx)),
DockPlacement::Right,
DockPlacement::Left,
window,
cx,
);
@@ -351,7 +307,7 @@ impl Workspace {
self.dock.update(cx, |this, cx| {
this.add_panel(
Arc::new(messaging_relays::init(window, cx)),
DockPlacement::Right,
DockPlacement::Left,
window,
cx,
);
@@ -361,7 +317,7 @@ impl Workspace {
let chat = ChatRegistry::global(cx);
// Trigger a refresh of the chat registry
chat.update(cx, |this, cx| {
this.refresh(cx);
this.reload(cx);
});
}
Command::ShowRelayList => {
@@ -390,7 +346,7 @@ impl Workspace {
let device = DeviceRegistry::global(cx).downgrade();
let save_dialog = cx.prompt_for_new_path(download_dir(), Some("encryption.txt"));
cx.spawn_in(window, async move |_this, cx| {
self.tasks.push(cx.spawn_in(window, async move |_this, cx| {
// Get the output path from the save dialog
let output_path = match save_dialog.await {
Ok(Ok(Some(path))) => path,
@@ -417,9 +373,8 @@ impl Workspace {
cx.open_with_system(output_path.as_path());
})?;
Ok::<_, anyhow::Error>(())
})
.detach();
Ok(())
}));
}
Command::ImportEncryption => {
self.import_encryption(window, cx);
@@ -479,10 +434,11 @@ impl Workspace {
let import = cx.new(|cx| ImportIdentity::new(window, cx));
window.open_modal(cx, move |this, _window, _cx| {
this.width(px(420.))
this.width(px(450.))
.show_close(false)
.overlay_closable(false)
.title("Import Identity")
.keyboard(false)
.title("Onboarding")
.child(import.clone())
});
}
@@ -642,9 +598,7 @@ impl Workspace {
fn titlebar_right(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
let chat = ChatRegistry::global(cx);
let trash_messages = chat.read(cx).count_trash_messages(cx);
let is_nip4e_enabled = AppSettings::get_nip4e(cx);
let nip4e_enabled = AppSettings::get_nip4e(cx);
let nostr = NostrRegistry::global(cx);
let Some(public_key) = nostr.read(cx).current_user() else {
@@ -658,39 +612,7 @@ impl Workspace {
h_flex()
.when(!cx.theme().platform.is_mac(), |this| this.pr_2())
.gap_2()
.when(trash_messages > 0, |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| {
.when(nip4e_enabled, |this| {
this.child(
Button::new("key")
.icon(IconName::UserKey)
@@ -782,17 +704,7 @@ impl Workspace {
.w_full()
.text_sm()
.justify_between()
.child(
h_flex()
.gap_2()
.child(
div()
.size_1p5()
.rounded_full()
.bg(cx.theme().icon_accent),
)
.child(url.clone()),
)
.child(url.clone())
.child(
div()
.text_xs()
@@ -804,11 +716,6 @@ impl Workspace {
// Footer
menu.separator()
.menu_with_icon(
"Reload",
IconName::Refresh,
Box::new(Command::RefreshMessagingRelays),
)
.menu_with_icon(
"Manage gossip relays",
IconName::Relay,
@@ -816,9 +723,15 @@ impl Workspace {
)
.menu_with_icon(
"Manage messaging relays",
IconName::Settings,
IconName::Relay,
Box::new(Command::ShowMessaging),
)
.separator()
.menu_with_icon(
"Reload",
IconName::Refresh,
Box::new(Command::RefreshMessagingRelays),
)
}),
)
}
@@ -830,7 +743,7 @@ impl Render for Workspace {
let notification_layer = Root::render_notification_layer(window, cx);
div()
.id(SharedString::from("workspace"))
.id("workspace")
.on_action(cx.listener(Self::on_command))
.relative()
.size_full()
@@ -847,8 +760,19 @@ impl Render for Workspace {
.child(self.titlebar_left(cx))
.child(self.titlebar_right(cx)),
)
// Dock
.child(self.dock.clone()),
// Main
.child(
h_flex()
.size_full()
.child(
div()
.flex_shrink_0()
.h_full()
.w(SIDEBAR_WIDTH)
.child(self.sidebar.clone()),
)
.child(self.dock.clone()),
),
),
)
// Notifications

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)),
)
}
}

View File

@@ -1,6 +1,5 @@
use std::collections::HashSet;
use std::ops::Range;
use std::time::Duration;
use anyhow::Error;
use chat::{ChatEvent, ChatRegistry, Room, RoomKind};
@@ -12,11 +11,12 @@ use gpui::{
ParentElement, Render, SharedString, Styled, Subscription, Task, UniformListScrollHandle,
Window, div, uniform_list,
};
use instant::Duration;
use nostr_sdk::prelude::*;
use person::PersonRegistry;
use smallvec::{SmallVec, smallvec};
use state::{FIND_DELAY, IMAGE_CACHE_SIZE, NostrRegistry};
use theme::{ActiveTheme, SIDEBAR_WIDTH, TABBAR_HEIGHT};
use theme::{ActiveTheme, SIDEBAR_WIDTH};
use ui::button::{Button, ButtonVariants};
use ui::dock::{Panel, PanelEvent};
use ui::indicator::Indicator;
@@ -29,13 +29,8 @@ mod entry;
const INPUT_PLACEHOLDER: &str = "Find or start a conversation";
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Sidebar> {
cx.new(|cx| Sidebar::new(window, cx))
}
/// Sidebar.
pub struct Sidebar {
name: SharedString,
focus_handle: FocusHandle,
scroll_handle: UniformListScrollHandle,
@@ -80,7 +75,7 @@ pub struct Sidebar {
}
impl Sidebar {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let chat = ChatRegistry::global(cx);
let filter = cx.new(|_| RoomKind::Ongoing);
let contact_list = cx.new(|_| None);
@@ -135,7 +130,6 @@ impl Sidebar {
);
Self {
name: "Sidebar".into(),
focus_handle: cx.focus_handle(),
scroll_handle: UniformListScrollHandle::new(),
find_input,
@@ -335,7 +329,7 @@ impl Sidebar {
.organize(&public_key)
.kind(RoomKind::Ongoing)
});
this.emit_room(&room, cx);
this.emit_room(&room, _window, cx);
})?;
// Reset the find panel
@@ -379,9 +373,9 @@ impl Sidebar {
let room = item.read(cx);
let room_clone = item.clone();
let public_key = room.display_member(cx).public_key();
let handler = cx.listener(move |_this, _ev, _window, cx| {
let handler = cx.listener(move |_this, _ev, window, cx| {
ChatRegistry::global(cx).update(cx, |s, cx| {
s.emit_room(&room_clone, cx);
s.emit_room(&room_clone, window, cx);
});
});
@@ -474,7 +468,7 @@ impl Sidebar {
impl Panel for Sidebar {
fn panel_id(&self) -> SharedString {
self.name.clone()
"Sidebar".into()
}
}
@@ -488,8 +482,11 @@ impl Focusable for Sidebar {
impl Render for Sidebar {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let nostr = NostrRegistry::global(cx);
let chat = ChatRegistry::global(cx);
let loading = chat.read(cx).loading();
let logged_in = nostr.read(cx).current_user().is_some();
let loading = chat.read(cx).loading() && logged_in;
let total_rooms = chat.read(cx).count(self.filter.read(cx), cx);
// Whether the find panel should be shown
@@ -507,27 +504,21 @@ impl Render for Sidebar {
.size_full()
.gap_2()
.child(
h_flex()
.h(TABBAR_HEIGHT)
.border_b_1()
.border_color(cx.theme().border)
.bg(cx.theme().tab_background)
.child(
Input::new(&self.find_input)
.appearance(false)
.bordered(false)
.small()
.text_xs()
.when(!self.find_input.read(cx).loading, |this| {
this.suffix(
Button::new("find-icon")
.icon(IconName::Search)
.tooltip("Press Enter to search")
.transparent()
.small(),
)
}),
),
h_flex().px_2().py_1().child(
Input::new(&self.find_input)
.small()
.text_xs()
.disabled(loading)
.when(!self.find_input.read(cx).loading, |this| {
this.suffix(
Button::new("find-icon")
.icon(IconName::Search)
.tooltip("Press Enter to search")
.transparent()
.small(),
)
}),
),
)
.child(
h_flex()
@@ -539,7 +530,6 @@ impl Render for Sidebar {
Button::new("search-results")
.icon(IconName::Search)
.tooltip("All search results")
.small()
.ghost_alt()
.font_semibold()
.flex_1()
@@ -555,9 +545,8 @@ impl Render for Sidebar {
this.icon(IconName::Inbox)
}
})
.when(!show_find_panel, |this| this.label("Inbox"))
.when(!show_find_panel, |this| this.label("Inbox").small())
.tooltip("All ongoing conversations")
.small()
.ghost_alt()
.font_semibold()
.flex_1()
@@ -577,9 +566,8 @@ impl Render for Sidebar {
this.icon(IconName::Fistbump)
}
})
.when(!show_find_panel, |this| this.label("Requests"))
.when(!show_find_panel, |this| this.label("Requests").small())
.tooltip("Incoming new conversations")
.small()
.ghost_alt()
.font_semibold()
.flex_1()
@@ -608,17 +596,13 @@ impl Render for Sidebar {
.items_center()
.justify_center()
.text_center()
.child(div().text_sm().font_semibold().child("No conversations"))
.child(
div()
.text_sm()
.font_semibold()
.child(SharedString::from("No conversations")),
)
.child(div().text_xs().text_color(cx.theme().text_muted).child(
SharedString::from(
"Start a conversation with someone to get started.",
),
)),
.text_xs()
.text_color(cx.theme().text_muted)
.child("Start a conversation with someone to get started."),
),
),
)
})
@@ -643,7 +627,7 @@ impl Render for Sidebar {
.font_semibold()
.text_color(cx.theme().text_muted)
.child(Icon::new(IconName::ChevronDown))
.child(SharedString::from("Results")),
.child("Results"),
)
.child(
uniform_list(
@@ -670,7 +654,7 @@ impl Render for Sidebar {
.font_semibold()
.text_color(cx.theme().text_muted)
.child(Icon::new(IconName::ChevronDown).small())
.child(SharedString::from("Suggestions")),
.child("Contacts"),
)
.child(
uniform_list(
@@ -707,17 +691,17 @@ impl Render for Sidebar {
this.child(
div()
.absolute()
.bottom_0()
.bottom_2()
.left_0()
.h_9()
.w_full()
.px_2()
.px_4()
.child(
Button::new("create")
.label(button_label)
.primary()
.small()
.shadow_lg()
.rounded()
.shadow_md()
.on_click(cx.listener(move |this, _ev, window, cx| {
this.create_room(window, cx);
})),
@@ -740,15 +724,13 @@ impl Render for Sidebar {
.h_9()
.justify_center()
.bg(cx.theme().background.opacity(0.85))
.border_color(cx.theme().border_disabled)
.border_1()
.when(cx.theme().shadow, |this| this.shadow_xs())
.when(cx.theme().shadow, |this| this.shadow_md())
.rounded_full()
.text_xs()
.font_semibold()
.text_color(cx.theme().text_muted)
.child(Indicator::new().small().color(cx.theme().icon_accent))
.child(SharedString::from("Getting messages...")),
.child("Getting messages..."),
),
)
})