This commit is contained in:
2026-10-23 11:06:34 +07:00
parent 1c00bc7956
commit e4ebe96077
10 changed files with 200 additions and 182 deletions
+55 -67
View File
@@ -8,13 +8,13 @@ use gpui::{
}; };
use gpui_kit::component::button::{Button, ButtonVariants}; use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::dialog::DialogButtonProps; use gpui_kit::component::dialog::DialogButtonProps;
use gpui_kit::component::form::field as form_field;
use gpui_kit::component::input::InputState; use gpui_kit::component::input::InputState;
use gpui_kit::component::notification::Notification; use gpui_kit::component::notification::Notification;
use gpui_kit::component::switch::Switch; use gpui_kit::component::switch::Switch;
use gpui_kit::component::{ActiveTheme, Sizable, WindowExt, h_flex, v_flex}; use gpui_kit::component::{ActiveTheme, Sizable, WindowExt, h_flex, v_flex};
use nip29::{Group, GroupId, GroupMetadata}; use nip29::{Group, GroupId, GroupMetadata};
use nostr_sdk::prelude::RelayUrl; use nostr_sdk::prelude::RelayUrl;
use ui::settings_row;
use crate::util::field; use crate::util::field;
@@ -126,73 +126,61 @@ pub(crate) fn open(group: WeakEntity<Group>, window: &mut Window, cx: &mut App)
.child( .child(
v_flex() v_flex()
.gap_2() .gap_2()
.child( .child(settings_row(
form_field() "Private",
.label("Private") "Only members can read group messages",
.description("Only members can read group messages") Switch::new("private")
.child( .checked(current.private)
Switch::new("private") .on_click(toggle(&flags, |flags| &mut flags.private)),
.checked(current.private) cx,
.on_click(toggle(&flags, |flags| &mut flags.private)), ))
), .child(settings_row(
) "Restricted",
.child( "Only members can post messages",
form_field() Switch::new("restricted")
.label("Restricted") .checked(current.restricted)
.description("Only members can post messages") .on_click(toggle(&flags, |flags| &mut flags.restricted)),
.child( cx,
Switch::new("restricted") ))
.checked(current.restricted) .child(settings_row(
.on_click(toggle(&flags, |flags| { "Hidden",
&mut flags.restricted "Hidden from non-members, not discoverable",
})), Switch::new("hidden")
), .checked(current.hidden)
) .on_click(toggle(&flags, |flags| &mut flags.hidden)),
.child( cx,
form_field() ))
.label("Hidden") .child(settings_row(
.description("Hidden from non-members, not discoverable") "Closed",
.child( "Join requests are ignored (invite-only)",
Switch::new("hidden") Switch::new("closed")
.checked(current.hidden) .checked(current.closed)
.on_click(toggle(&flags, |flags| &mut flags.hidden)), .on_click(toggle(&flags, |flags| &mut flags.closed)),
), cx,
) ))
.child( .child(settings_row(
form_field() "LiveKit",
.label("Closed") if supported {
.description("Join requests are ignored (invite-only)") "Announce audio and video support."
.child( } else {
Switch::new("closed") "The relay doesn't announce LiveKit support."
.checked(current.closed) },
.on_click(toggle(&flags, |flags| &mut flags.closed)), Switch::new("livekit").checked(current.livekit).on_click({
), let flags = flags.clone();
) let livekit = livekit.clone();
.child(
form_field()
.label("LiveKit")
.description(if supported {
"Announce audio and video support."
} else {
"The relay doesn't announce LiveKit support."
})
.child(
Switch::new("livekit").checked(current.livekit).on_click({
let flags = flags.clone();
let livekit = livekit.clone();
move |next, _window, cx| { move |next, _window, cx| {
if *next && *livekit.read(cx) == Some(false) { if *next && *livekit.read(cx) == Some(false) {
return; return;
} }
flags.update(cx, |flags, cx| { flags.update(cx, |flags, cx| {
flags.livekit = *next; flags.livekit = *next;
cx.notify(); cx.notify();
}); });
} }
}), }),
), cx,
), )),
), ),
) )
.on_ok({ .on_ok({
+5
View File
@@ -167,11 +167,16 @@ impl RenderOnce for PixelAvatar {
} }
} }
// Pad the grid so the art stays clear of the circular edge, keeping the
// inset look of signed's rounded-square frame.
let inset = self.side / (GRID_SIZE as f32 * 2.);
div() div()
.grid() .grid()
.grid_cols(GRID_SIZE as u16) .grid_cols(GRID_SIZE as u16)
.grid_rows(GRID_SIZE as u16) .grid_rows(GRID_SIZE as u16)
.size(self.side) .size(self.side)
.p(inset)
.flex_shrink_0() .flex_shrink_0()
.rounded_full() .rounded_full()
.overflow_hidden() .overflow_hidden()
+28
View File
@@ -0,0 +1,28 @@
use gpui::{App, Div, IntoElement, ParentElement, SharedString, Styled, div};
use gpui_kit::component::{ActiveTheme, h_flex, v_flex};
/// A settings row: the label and description on the left, the control on the right.
pub fn settings_row(
label: impl Into<SharedString>,
description: impl Into<SharedString>,
control: impl IntoElement,
cx: &App,
) -> Div {
h_flex()
.gap_3()
.items_center()
.justify_between()
.child(
v_flex()
.flex_1()
.min_w_0()
.child(div().text_sm().child(label.into()))
.child(
div()
.text_xs()
.text_color(cx.theme().muted_foreground)
.child(description.into()),
),
)
.child(control)
}
+2
View File
@@ -1,10 +1,12 @@
pub use avatar::Avatar; pub use avatar::Avatar;
pub use form::*;
pub use icon::IconName; pub use icon::IconName;
pub use message::*; pub use message::*;
pub use text::*; pub use text::*;
pub use window_drag::title_bar_drag_handlers; pub use window_drag::title_bar_drag_handlers;
pub mod avatar; pub mod avatar;
pub mod form;
pub mod icon; pub mod icon;
pub mod message; pub mod message;
pub mod text; pub mod text;
+1 -1
View File
@@ -338,7 +338,7 @@ impl NewChat {
Nav::new(start + offset) Nav::new(start + offset)
.label(profile.name()) .label(profile.name())
.text_xs() .text_sm()
.when(!AppSettings::get_hide_avatar(cx), |this| { .when(!AppSettings::get_hide_avatar(cx), |this| {
this.prefix( this.prefix(
Avatar::new(profile.avatar()) Avatar::new(profile.avatar())
+61 -61
View File
@@ -3,14 +3,14 @@ use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
App, AppContext, Context, Div, Entity, FocusHandle, InteractiveElement as _, IntoElement, App, AppContext, Context, Div, Entity, FocusHandle, InteractiveElement as _, IntoElement,
ParentElement, PathPromptOptions, Render, SharedString, StatefulInteractiveElement as _, ParentElement, PathPromptOptions, Render, SharedString, StatefulInteractiveElement as _,
Styled, Subscription, Task, WeakEntity, Window, div, px, rems, StyleRefinement, Styled, Subscription, Task, WeakEntity, Window, div, px, rems,
}; };
use gpui_base::{Popup, Select}; use gpui_base::{Popup, Select};
use gpui_kit::assets::IconName; use gpui_kit::assets::IconName;
use gpui_kit::base::StyledExt as _; use gpui_kit::base::StyledExt as _;
use gpui_kit::component::button::{Button, ButtonVariants}; use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::dock::{DockArea, DockPlacement, PanelHandle}; use gpui_kit::component::dock::{DockArea, DockPlacement, PanelHandle};
use gpui_kit::component::form::field; use gpui_kit::component::group_box::{GroupBox, GroupBoxVariants};
use gpui_kit::component::input::{Input, InputEvent, InputState, Textarea, TextareaState}; use gpui_kit::component::input::{Input, InputEvent, InputState, Textarea, TextareaState};
use gpui_kit::component::separator::Separator; use gpui_kit::component::separator::Separator;
use gpui_kit::component::switch::Switch; use gpui_kit::component::switch::Switch;
@@ -19,14 +19,14 @@ use nip29::{GroupCreateOptions, GroupKey, GroupsEvent, GroupsRegistry};
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use settings::AppSettings; use settings::AppSettings;
use state::upload; use state::upload;
use ui::Avatar; use ui::{Avatar, settings_row};
const DEFAULT_RELAYS: [&str; 2] = ["wss://relay.ditto.net", "wss://chat.wisp.talk"]; const DEFAULT_RELAYS: [&str; 2] = ["wss://relay.ditto.net", "wss://chat.wisp.talk"];
const PRIVATE_DESC: &str = "Only members can read group messages"; const PRIVATE_DESC: &str = "Only members can read group messages";
const CLOSED_DESC: &str = "Join requests are ignored (invite-only)"; const CLOSED_DESC: &str = "Join requests are ignored (invite-only)";
const RESTRICTED_DESC: &str = "Only members can post messages"; const RESTRICTED_DESC: &str = "Only members can post messages";
const HIDDEN_DESC: &str = "Hidden from non-members, not discoverable"; const HIDDEN_DESC: &str = "Hidden from non-members, not discoverable";
const LIST_PRIVATE_DESC: &str = "Kept encrypted in your group list so others can't see you joined"; const LIST_PRIVATE_DESC: &str = "Kept encrypted in your group list so others can't see";
pub fn open(dock: WeakEntity<DockArea>, window: &mut Window, cx: &mut App) { pub fn open(dock: WeakEntity<DockArea>, window: &mut Window, cx: &mut App) {
open_with(dock, None, window, cx); open_with(dock, None, window, cx);
@@ -462,71 +462,72 @@ impl NewGroup {
} }
fn render_access(&self, cx: &mut Context<Self>) -> impl IntoElement { fn render_access(&self, cx: &mut Context<Self>) -> impl IntoElement {
let private = field().label("Private").description(PRIVATE_DESC).child( GroupBox::new()
Switch::new("private") .fill()
.checked(self.private) .title("Access settings")
.on_click(cx.listener(|this, checked: &bool, _window, cx| { .title_style(group_box_title_style(cx))
this.private = *checked; .child(settings_row(
cx.notify(); "Private",
})), PRIVATE_DESC,
); Switch::new("private")
.checked(self.private)
let closed = field().label("Closed").description(CLOSED_DESC).child( .on_click(cx.listener(|this, checked: &bool, _window, cx| {
Switch::new("closed") this.private = *checked;
.checked(self.closed) cx.notify();
.on_click(cx.listener(|this, checked: &bool, _window, cx| { })),
this.closed = *checked; cx,
cx.notify(); ))
})), .child(settings_row(
); "Closed",
CLOSED_DESC,
let restricted = field() Switch::new("closed")
.label("Restricted") .checked(self.closed)
.description(RESTRICTED_DESC) .on_click(cx.listener(|this, checked: &bool, _window, cx| {
.child( this.closed = *checked;
cx.notify();
})),
cx,
))
.child(settings_row(
"Restricted",
RESTRICTED_DESC,
Switch::new("restricted") Switch::new("restricted")
.checked(self.restricted) .checked(self.restricted)
.on_click(cx.listener(|this, checked: &bool, _window, cx| { .on_click(cx.listener(|this, checked: &bool, _window, cx| {
this.restricted = *checked; this.restricted = *checked;
cx.notify(); cx.notify();
})), })),
); cx,
))
let hidden = field().label("Hidden").description(HIDDEN_DESC).child( .child(settings_row(
Switch::new("hidden") "Hidden",
.checked(self.hidden) HIDDEN_DESC,
.on_click(cx.listener(|this, checked: &bool, _window, cx| { Switch::new("hidden")
this.hidden = *checked; .checked(self.hidden)
cx.notify(); .on_click(cx.listener(|this, checked: &bool, _window, cx| {
})), this.hidden = *checked;
); cx.notify();
})),
v_flex() cx,
.gap_2() ))
.child(field_label("Access settings", cx))
.child(switch_row(private, cx))
.child(switch_row(closed, cx))
.child(switch_row(restricted, cx))
.child(switch_row(hidden, cx))
} }
fn render_list_setting(&self, cx: &mut Context<Self>) -> impl IntoElement { fn render_list_setting(&self, cx: &mut Context<Self>) -> impl IntoElement {
let list_private = field() GroupBox::new()
.label("Private on my list") .fill()
.description(LIST_PRIVATE_DESC) .title("List")
.child( .title_style(group_box_title_style(cx))
.child(settings_row(
"Private on my list",
LIST_PRIVATE_DESC,
Switch::new("list-private") Switch::new("list-private")
.checked(self.list_private) .checked(self.list_private)
.on_click(cx.listener(|this, checked: &bool, _window, cx| { .on_click(cx.listener(|this, checked: &bool, _window, cx| {
this.list_private = *checked; this.list_private = *checked;
cx.notify(); cx.notify();
})), })),
); cx,
))
v_flex()
.gap_2()
.child(field_label("List", cx))
.child(switch_row(list_private, cx))
} }
fn render_error( fn render_error(
@@ -662,13 +663,12 @@ fn field_label(text: &'static str, cx: &App) -> Div {
.child(text) .child(text)
} }
fn switch_row(switch: impl IntoElement, cx: &App) -> Div { /// Match [`field_label`]'s design for GroupBox titles.
div() fn group_box_title_style(cx: &App) -> StyleRefinement {
.w_full() StyleRefinement::default()
.p_2() .text_xs()
.rounded(cx.theme().radius) .text_color(cx.theme().muted_foreground)
.bg(cx.theme().secondary) .font_semibold()
.child(switch)
} }
fn relay_label(relay: &RelayUrl) -> SharedString { fn relay_label(relay: &RelayUrl) -> SharedString {
+41 -37
View File
@@ -6,15 +6,14 @@ use gpui::{
use gpui_kit::assets::IconName; use gpui_kit::assets::IconName;
use gpui_kit::base::StyledExt as _; use gpui_kit::base::StyledExt as _;
use gpui_kit::component::button::{Button, ButtonVariants}; use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::form::field;
use gpui_kit::component::group_box::{GroupBox, GroupBoxVariants}; use gpui_kit::component::group_box::{GroupBox, GroupBoxVariants};
use gpui_kit::component::input::{Input, InputState}; use gpui_kit::component::input::{Input, InputState};
use gpui_kit::component::menu::{DropdownMenu, PopupMenuItem}; use gpui_kit::component::menu::{DropdownMenu, PopupMenuItem};
use gpui_kit::component::notification::Notification; use gpui_kit::component::notification::Notification;
use gpui_kit::component::switch::Switch; use gpui_kit::component::switch::Switch;
use gpui_kit::component::{ActiveTheme, Disableable, Sizable, WindowExt, h_flex, v_flex}; use gpui_kit::component::{ActiveTheme, Disableable, Sizable, WindowExt, h_flex, v_flex};
use settings::AppSettings; use settings::{AppSettings, AppearanceMode};
use settings::AppearanceMode; use ui::settings_row;
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Preferences> { pub fn init(window: &mut Window, cx: &mut App) -> Entity<Preferences> {
cx.new(|cx| Preferences::new(window, cx)) cx.new(|cx| Preferences::new(window, cx))
@@ -114,28 +113,35 @@ impl Render for Preferences {
.id("general") .id("general")
.title("General") .title("General")
.fill() .fill()
.child(field().label("Screening").description(SCREENING).child( .child(settings_row(
"Screening",
SCREENING,
Switch::new("screening").checked(screening).on_click( Switch::new("screening").checked(screening).on_click(
move |_, _window, cx| { move |_, _window, cx| {
AppSettings::update_screening(!screening, cx); AppSettings::update_screening(!screening, cx);
}, },
), ),
cx,
)) ))
.child( .child(settings_row(
field().label("Web of Trust").description(WOT).child( "Web of Trust",
Switch::new("wot-auto-trust") WOT,
.checked(wot_auto_trust) Switch::new("wot-auto-trust")
.on_click(move |_, _window, cx| { .checked(wot_auto_trust)
AppSettings::update_wot_auto_trust(!wot_auto_trust, cx); .on_click(move |_, _window, cx| {
}), AppSettings::update_wot_auto_trust(!wot_auto_trust, cx);
), }),
) cx,
.child(field().label("Hide user avatar").description(AVATAR).child( ))
.child(settings_row(
"Hide user avatar",
AVATAR,
Switch::new("avatar").checked(hide_avatar).on_click( Switch::new("avatar").checked(hide_avatar).on_click(
move |_, _window, cx| { move |_, _window, cx| {
AppSettings::update_hide_avatar(!hide_avatar, cx); AppSettings::update_hide_avatar(!hide_avatar, cx);
}, },
), ),
cx,
)), )),
) )
.child( .child(
@@ -179,19 +185,17 @@ impl Render for Preferences {
.id("background") .id("background")
.title("Background") .title("Background")
.fill() .fill()
.child( .child(settings_row(
field() "Panel background",
.label("Panel background") BACKGROUND,
.description(BACKGROUND) Switch::new("panel-background")
.child( .checked(panel_background)
Switch::new("panel-background") .on_click(move |_, _window, cx| {
.checked(panel_background) AppSettings::update_panel_background(!panel_background, cx);
.on_click(move |_, _window, cx| { cx.refresh_windows();
AppSettings::update_panel_background(!panel_background, cx); }),
cx.refresh_windows(); cx,
}), ))
),
)
.child( .child(
h_flex() h_flex()
.gap_3() .gap_3()
@@ -245,16 +249,16 @@ impl Render for Preferences {
.id("experiments") .id("experiments")
.title("Experiments") .title("Experiments")
.fill() .fill()
.child( .child(settings_row(
field() "Decoupling Encryption Key",
.label("Decoupling Encryption Key") NIP4E,
.description(NIP4E) Switch::new("nip4e")
.child(Switch::new("nip4e").checked(nip4e).on_click( .checked(nip4e)
move |_, _window, cx| { .on_click(move |_, _window, cx| {
AppSettings::update_nip4e(!nip4e, cx); AppSettings::update_nip4e(!nip4e, cx);
}, }),
)), cx,
), )),
) )
.child( .child(
GroupBox::new() GroupBox::new()
+1 -1
View File
@@ -358,7 +358,7 @@ impl CoopTabGroupSkin {
this this
} else { } else {
this.text_color(cx.theme().tab_foreground) this.text_color(cx.theme().tab_foreground)
.bg(cx.theme().tab) .bg(cx.theme().list_hover)
} }
}) })
.selected(active) .selected(active)
+3 -15
View File
@@ -87,22 +87,10 @@ impl Sidebar {
.small() .small()
.dropdown_menu(|menu, _window, _cx| { .dropdown_menu(|menu, _window, _cx| {
menu.label("Public Group") menu.label("Public Group")
.menu_with_icon( .menu("New Group", Box::new(Command::NewGroup))
"New Group", .menu("Join Group", Box::new(Command::JoinGroup))
IconName::Group,
Box::new(Command::NewGroup),
)
.menu_with_icon(
"Join Group",
IconName::Door,
Box::new(Command::JoinGroup),
)
.separator() .separator()
.menu_with_icon( .menu("New Chat", Box::new(Command::NewChat))
"New Chat",
IconName::Message,
Box::new(Command::NewChat),
)
}), }),
) )
.when(self.group_open, |this| { .when(self.group_open, |this| {
+3
View File
@@ -193,6 +193,9 @@ fn apply_appearance(appearance: AppearanceMode, cx: &mut App) {
AppearanceMode::Light => Theme::change(ThemeMode::Light, None, cx), AppearanceMode::Light => Theme::change(ThemeMode::Light, None, cx),
AppearanceMode::Dark => Theme::change(ThemeMode::Dark, None, cx), AppearanceMode::Dark => Theme::change(ThemeMode::Dark, None, cx),
} }
// Like signed, inputs show only their tinted border when focused.
Theme::global_mut(cx).focus_ring = false;
} }
/// Holds the watcher subscription so it lives as long as the app. /// Holds the watcher subscription so it lives as long as the app.