wip
This commit is contained in:
@@ -2,17 +2,18 @@ use anyhow::Error;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
App, AppContext, Context, Div, Entity, IntoElement, ParentElement, Render, SharedString,
|
||||
Styled, Subscription, Task, Window, div, px, relative,
|
||||
Styled, Subscription, Task, Window, div, px,
|
||||
};
|
||||
use gpui_kit::assets::IconName;
|
||||
use gpui_kit::component::alert::{Alert, AlertVariant};
|
||||
use gpui_kit::component::button::{Button, ButtonVariants};
|
||||
use gpui_kit::component::dialog::{DialogDescription, DialogFooter, DialogHeader, DialogTitle};
|
||||
use gpui_kit::component::form::{field, v_form};
|
||||
use gpui_kit::component::input::{Input, InputEvent, InputState};
|
||||
use gpui_kit::component::{ActiveTheme, Disableable, WindowExt, v_flex};
|
||||
use gpui_kit::component::{ActiveTheme, Disableable, Sizable, WindowExt, v_flex};
|
||||
use instant::Duration;
|
||||
use nostr_connect::prelude::*;
|
||||
use state::{NostrRegistry, StateEvent};
|
||||
use ui::IconName;
|
||||
|
||||
pub fn open(window: &mut Window, cx: &mut App) {
|
||||
let import = cx.new(|cx| ImportIdentity::new(window, cx));
|
||||
@@ -277,7 +278,7 @@ impl ImportIdentity {
|
||||
DialogFooter::new().child(
|
||||
Button::new("unlock")
|
||||
.primary()
|
||||
.icon(IconName::Unlock)
|
||||
.icon(IconName::LockOpen)
|
||||
.label("Unlock")
|
||||
.loading(self.loading)
|
||||
.disabled(self.loading)
|
||||
@@ -345,26 +346,18 @@ impl Render for ImportIdentity {
|
||||
})
|
||||
.when(bunker_warning, |this| {
|
||||
this.child(
|
||||
div()
|
||||
.p_2()
|
||||
.rounded(cx.theme().radius_lg)
|
||||
.bg(cx.theme().warning.opacity(0.15))
|
||||
.text_xs()
|
||||
.text_color(cx.theme().warning)
|
||||
.line_height(relative(1.2))
|
||||
.child(div().child(BUNKER_WARN)),
|
||||
Alert::new("bunker-warning", BUNKER_WARN)
|
||||
.with_variant(AlertVariant::Warning)
|
||||
.icon(IconName::TriangleAlert)
|
||||
.small(),
|
||||
)
|
||||
})
|
||||
.when(key_warning, |this| {
|
||||
this.child(
|
||||
div()
|
||||
.p_2()
|
||||
.rounded(cx.theme().radius_lg)
|
||||
.bg(cx.theme().warning.opacity(0.15))
|
||||
.text_xs()
|
||||
.text_color(cx.theme().warning)
|
||||
.line_height(relative(1.2))
|
||||
.child(div().child(KEY_WARN)),
|
||||
Alert::new("key-warning", KEY_WARN)
|
||||
.with_variant(AlertVariant::Warning)
|
||||
.icon(IconName::TriangleAlert)
|
||||
.small(),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -13,6 +13,7 @@ use gpui_kit::assets::IconName;
|
||||
use gpui_kit::component::button::{Button, ButtonVariants};
|
||||
use gpui_kit::component::dock::{DockArea, DockPlacement, PanelHandle};
|
||||
use gpui_kit::component::input::{Input, InputEvent, InputState};
|
||||
use gpui_kit::component::list::ListItem;
|
||||
use gpui_kit::component::{ActiveTheme, Disableable, Sizable, WindowExt, h_flex, v_flex};
|
||||
use nostr_sdk::prelude::*;
|
||||
use person::Directory;
|
||||
@@ -20,8 +21,6 @@ use settings::AppSettings;
|
||||
use state::NostrRegistry;
|
||||
use ui::Avatar;
|
||||
|
||||
use crate::nav::Nav;
|
||||
|
||||
/// Maximum number of members a chat can have.
|
||||
const MAX_MEMBERS: usize = 3;
|
||||
|
||||
@@ -325,27 +324,30 @@ impl NewChat {
|
||||
contacts: &[PublicKey],
|
||||
cx: &mut Context<Self>,
|
||||
) -> Vec<AnyElement> {
|
||||
let start = range.start;
|
||||
|
||||
contacts
|
||||
.get(range)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.enumerate()
|
||||
.map(|(offset, public_key)| {
|
||||
.map(|public_key| {
|
||||
let profile = self.directory.read(cx).person(public_key, cx);
|
||||
let public_key = *public_key;
|
||||
|
||||
Nav::new(start + offset)
|
||||
.label(profile.name())
|
||||
ListItem::new(public_key.to_hex())
|
||||
.accessibility_label(profile.name())
|
||||
.h_8()
|
||||
.rounded(cx.theme().radius)
|
||||
.cursor_pointer()
|
||||
.text_sm()
|
||||
.when(!AppSettings::get_hide_avatar(cx), |this| {
|
||||
this.prefix(
|
||||
Avatar::new(profile.avatar())
|
||||
.seed(profile.avatar_seed())
|
||||
.small(),
|
||||
this.child(
|
||||
div().flex_shrink_0().child(
|
||||
Avatar::new(profile.avatar())
|
||||
.seed(profile.avatar_seed())
|
||||
.small(),
|
||||
),
|
||||
)
|
||||
})
|
||||
.child(div().flex_1().min_w_0().truncate().child(profile.name()))
|
||||
.on_click(cx.listener(move |this, _event, _window, cx| {
|
||||
this.toggle_member(&public_key, cx);
|
||||
}))
|
||||
@@ -369,7 +371,7 @@ impl NewChat {
|
||||
.p_0p5()
|
||||
.rounded_full()
|
||||
.bg(cx.theme().secondary)
|
||||
.hover(|this| this.bg(cx.theme().primary))
|
||||
.hover(|this| this.bg(cx.theme().secondary_hover))
|
||||
.child(
|
||||
Avatar::new(profile.avatar())
|
||||
.seed(profile.avatar_seed())
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
use anyhow::Error;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
App, AppContext, Context, Div, Entity, FocusHandle, InteractiveElement as _, IntoElement,
|
||||
AnyElement, App, AppContext, Context, Div, Entity, InteractiveElement as _, IntoElement,
|
||||
ParentElement, Render, SharedString, StatefulInteractiveElement as _, StyleRefinement, Styled,
|
||||
Subscription, Task, WeakEntity, Window, div, px, rems,
|
||||
Subscription, Task, WeakEntity, Window, div, px,
|
||||
};
|
||||
use gpui_base::{Popup, Select};
|
||||
use gpui_kit::assets::IconName;
|
||||
use gpui_kit::base::StyledExt as _;
|
||||
use gpui_kit::component::button::{Button, ButtonVariants};
|
||||
use gpui_kit::component::dock::{DockArea, DockPlacement, PanelHandle};
|
||||
use gpui_kit::component::group_box::{GroupBox, GroupBoxVariants};
|
||||
use gpui_kit::component::input::{Input, InputEvent, InputState, Textarea, TextareaState};
|
||||
use gpui_kit::component::searchable_list::{SearchableListDelegate, SearchableListItem};
|
||||
use gpui_kit::component::select::{Select, SelectEvent, SelectState};
|
||||
use gpui_kit::component::separator::Separator;
|
||||
use gpui_kit::component::switch::Switch;
|
||||
use gpui_kit::component::{ActiveTheme, Disableable, Icon, Sizable, WindowExt, h_flex, v_flex};
|
||||
use gpui_kit::component::{
|
||||
ActiveTheme, Disableable, IndexPath, Sizable, WindowExt, h_flex, v_flex,
|
||||
};
|
||||
use nip29::{GroupCreateOptions, GroupKey, GroupsEvent, GroupsRegistry};
|
||||
use nostr_sdk::prelude::*;
|
||||
use settings::AppSettings;
|
||||
@@ -72,13 +74,10 @@ struct NewGroup {
|
||||
name_input: Entity<InputState>,
|
||||
about_input: Entity<TextareaState>,
|
||||
custom_relay_input: Entity<InputState>,
|
||||
/// The relays offered in the relay select.
|
||||
relays: Vec<RelayUrl>,
|
||||
/// The selected relay; `None` selects the custom relay field.
|
||||
selected_relay: Option<RelayUrl>,
|
||||
/// Whether the relay select popup is open.
|
||||
relay_select_open: bool,
|
||||
relay_options_focus: FocusHandle,
|
||||
/// The relay select; its options end with the custom relay choice.
|
||||
relay_select: Entity<SelectState<RelayDelegate>>,
|
||||
/// Keep the group in the NIP-44 encrypted section of the user's group list.
|
||||
list_private: bool,
|
||||
/// The uploaded group picture, ready to be published with the metadata.
|
||||
@@ -113,8 +112,6 @@ impl NewGroup {
|
||||
.placeholder("What is this group about?")
|
||||
});
|
||||
|
||||
let relay_options_focus = cx.focus_handle();
|
||||
|
||||
let relays: Vec<RelayUrl> = DEFAULT_RELAYS
|
||||
.iter()
|
||||
.filter_map(|url| match RelayUrl::parse(url) {
|
||||
@@ -127,9 +124,37 @@ impl NewGroup {
|
||||
.collect();
|
||||
let selected_relay = relays.first().cloned();
|
||||
|
||||
let mut options: Vec<RelayOption> = relays
|
||||
.iter()
|
||||
.map(|relay| RelayOption {
|
||||
label: relay_label(relay),
|
||||
value: Some(relay.clone()),
|
||||
})
|
||||
.collect();
|
||||
options.push(RelayOption {
|
||||
label: "Custom relay".into(),
|
||||
value: None,
|
||||
});
|
||||
|
||||
let registry = GroupsRegistry::global(cx);
|
||||
let mut subscriptions = vec![];
|
||||
|
||||
let delegate = RelayDelegate { options };
|
||||
let selected_index = delegate.position(&selected_relay);
|
||||
let relay_select = cx.new(|cx| SelectState::new(delegate, selected_index, window, cx));
|
||||
|
||||
subscriptions.push(cx.subscribe_in(
|
||||
&relay_select,
|
||||
window,
|
||||
|this, _select, event, _window, cx| {
|
||||
let SelectEvent::Confirm(value) = event;
|
||||
|
||||
if let Some(choice) = value {
|
||||
this.select_relay(choice.clone(), cx);
|
||||
}
|
||||
},
|
||||
));
|
||||
|
||||
subscriptions.push(cx.subscribe_in(
|
||||
®istry,
|
||||
window,
|
||||
@@ -165,10 +190,8 @@ impl NewGroup {
|
||||
name_input,
|
||||
about_input,
|
||||
custom_relay_input,
|
||||
relays,
|
||||
selected_relay,
|
||||
relay_select_open: false,
|
||||
relay_options_focus,
|
||||
relay_select,
|
||||
list_private: false,
|
||||
picture: None,
|
||||
uploading: false,
|
||||
@@ -275,20 +298,6 @@ impl NewGroup {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn set_relay_select_open(&mut self, open: bool, cx: &mut Context<Self>) {
|
||||
self.relay_select_open = open;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn activate_relay_option(&mut self, index: usize, cx: &mut Context<Self>) {
|
||||
if index < self.relays.len() {
|
||||
self.select_relay(Some(self.relays[index].clone()), cx);
|
||||
} else {
|
||||
self.select_relay(None, cx);
|
||||
}
|
||||
self.set_relay_select_open(false, cx);
|
||||
}
|
||||
|
||||
fn set_uploading(&mut self, uploading: bool, cx: &mut Context<Self>) {
|
||||
self.uploading = uploading;
|
||||
cx.notify();
|
||||
@@ -339,7 +348,7 @@ impl NewGroup {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_relay(&self, selected: &str, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
fn render_relay(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
if let Some(parent) = &self.parent {
|
||||
return v_flex().gap_1p5().child(field_label("Relay", cx)).child(
|
||||
div()
|
||||
@@ -352,108 +361,13 @@ impl NewGroup {
|
||||
);
|
||||
}
|
||||
|
||||
let this = cx.entity().downgrade();
|
||||
let open = self.relay_select_open;
|
||||
|
||||
let trigger = h_flex()
|
||||
.id("relay-trigger")
|
||||
.justify_between()
|
||||
.w_full()
|
||||
.h_8()
|
||||
.px_3()
|
||||
.rounded(cx.theme().radius)
|
||||
.text_xs()
|
||||
.cursor_default()
|
||||
.bg(if open {
|
||||
cx.theme().list_active
|
||||
} else {
|
||||
cx.theme().secondary
|
||||
})
|
||||
.when(!open, |this| {
|
||||
this.hover(|this| this.bg(cx.theme().primary))
|
||||
.active(|this| this.bg(cx.theme().list_active))
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, _window, cx| {
|
||||
this.relay_select_open = !open;
|
||||
cx.notify();
|
||||
}))
|
||||
.child(selected.to_owned())
|
||||
.child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
.small()
|
||||
.text_color(cx.theme().muted_foreground),
|
||||
);
|
||||
|
||||
let select = Select::new("relay-select")
|
||||
.open(open)
|
||||
.on_open_change({
|
||||
let this = this.clone();
|
||||
move |open, _, cx| {
|
||||
let _ = this.update(cx, |this, cx| {
|
||||
this.set_relay_select_open(open, cx);
|
||||
});
|
||||
}
|
||||
})
|
||||
.content_focus_handle(&self.relay_options_focus)
|
||||
.accessibility_label("Relay")
|
||||
.accessibility_value(selected.to_owned())
|
||||
.w_full()
|
||||
.child(trigger);
|
||||
|
||||
let options = v_flex()
|
||||
.id("relay-options")
|
||||
.occlude()
|
||||
.track_focus(&self.relay_options_focus)
|
||||
.key_context("Select")
|
||||
.on_mouse_down_out(cx.listener(|this, _, _, cx| {
|
||||
this.set_relay_select_open(false, cx);
|
||||
}))
|
||||
.bg(cx.theme().popover)
|
||||
.border_1()
|
||||
.border_color(cx.theme().border)
|
||||
.shadow_md()
|
||||
.rounded(cx.theme().radius_lg)
|
||||
.p_1()
|
||||
.gap_y_1()
|
||||
.min_w(rems(8.))
|
||||
.text_color(cx.theme().foreground)
|
||||
.children(self.relays.iter().enumerate().map(|(index, relay)| {
|
||||
h_flex()
|
||||
.id(SharedString::from(format!("option-{relay}")))
|
||||
.px_1p5()
|
||||
.py_1()
|
||||
.rounded(cx.theme().radius)
|
||||
.text_xs()
|
||||
.cursor_default()
|
||||
.hover(|this| this.bg(cx.theme().primary))
|
||||
.child(relay_label(relay))
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
this.activate_relay_option(index, cx);
|
||||
}))
|
||||
}))
|
||||
.child(Separator::horizontal())
|
||||
.child(
|
||||
h_flex()
|
||||
.id("relay-add")
|
||||
.px_1p5()
|
||||
.py_1()
|
||||
.rounded(cx.theme().radius)
|
||||
.text_xs()
|
||||
.cursor_default()
|
||||
.child("Custom relay")
|
||||
.hover(|this| this.bg(cx.theme().primary))
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.activate_relay_option(this.relays.len(), cx);
|
||||
})),
|
||||
);
|
||||
|
||||
v_flex()
|
||||
.gap_1p5()
|
||||
.child(field_label("Relay", cx))
|
||||
.when(self.selected_relay.is_none(), |this| {
|
||||
this.child(Input::new(&self.custom_relay_input))
|
||||
})
|
||||
.child(Popup::new("relay-popup", select).when(open, |this| this.content(options)))
|
||||
.child(Select::new(&self.relay_select).accessibility_label("Relay"))
|
||||
}
|
||||
|
||||
fn render_access(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
@@ -567,12 +481,6 @@ impl NewGroup {
|
||||
|
||||
impl Render for NewGroup {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let selected = self
|
||||
.selected_relay
|
||||
.as_ref()
|
||||
.map(relay_label)
|
||||
.unwrap_or_else(|| "Add relay".into());
|
||||
|
||||
let web_url = self
|
||||
.effective_relay(cx)
|
||||
.ok()
|
||||
@@ -614,7 +522,7 @@ impl Render for NewGroup {
|
||||
),
|
||||
)
|
||||
.child(input_field("Name", Input::new(&self.name_input), cx))
|
||||
.child(self.render_relay(&selected, cx))
|
||||
.child(self.render_relay(cx))
|
||||
.child(input_field("About", Textarea::new(&self.about_input), cx))
|
||||
.child(self.render_access(cx))
|
||||
.child(self.render_list_setting(cx))
|
||||
@@ -646,6 +554,76 @@ impl Render for NewGroup {
|
||||
}
|
||||
}
|
||||
|
||||
/// One choice in the relay select: a fixed relay, or the custom relay field.
|
||||
#[derive(Clone)]
|
||||
struct RelayOption {
|
||||
label: SharedString,
|
||||
value: Option<RelayUrl>,
|
||||
}
|
||||
|
||||
impl SearchableListItem for RelayOption {
|
||||
type Value = Option<RelayUrl>;
|
||||
|
||||
fn title(&self) -> SharedString {
|
||||
self.label.clone()
|
||||
}
|
||||
|
||||
fn value(&self) -> &Self::Value {
|
||||
&self.value
|
||||
}
|
||||
|
||||
/// The trigger reads "Add relay" while the custom relay field is active.
|
||||
fn display_title(&self) -> Option<AnyElement> {
|
||||
(self.value.is_none()).then(|| "Add relay".into_any_element())
|
||||
}
|
||||
}
|
||||
|
||||
/// Relay options with a separator before the trailing custom relay choice.
|
||||
struct RelayDelegate {
|
||||
options: Vec<RelayOption>,
|
||||
}
|
||||
|
||||
impl SearchableListDelegate for RelayDelegate {
|
||||
type Item = RelayOption;
|
||||
|
||||
fn items_count(&self, _section: usize) -> usize {
|
||||
self.options.len()
|
||||
}
|
||||
|
||||
fn item(&self, ix: IndexPath) -> Option<&RelayOption> {
|
||||
self.options.get(ix.row)
|
||||
}
|
||||
|
||||
fn position<V>(&self, value: &V) -> Option<IndexPath>
|
||||
where
|
||||
Self::Item: SearchableListItem<Value = V>,
|
||||
V: PartialEq,
|
||||
{
|
||||
self.options
|
||||
.iter()
|
||||
.position(|option| option.value() == value)
|
||||
.map(IndexPath::new)
|
||||
}
|
||||
|
||||
fn render_item(
|
||||
&self,
|
||||
_ix: IndexPath,
|
||||
item: &RelayOption,
|
||||
_checked: bool,
|
||||
_window: &mut Window,
|
||||
_cx: &mut App,
|
||||
) -> Option<AnyElement> {
|
||||
(item.value.is_none()).then(|| {
|
||||
v_flex()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.child(Separator::horizontal())
|
||||
.child(item.title())
|
||||
.into_any_element()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn input_field(text: &'static str, input: impl IntoElement, cx: &App) -> Div {
|
||||
v_flex().gap_1p5().child(field_label(text, cx)).child(input)
|
||||
}
|
||||
|
||||
@@ -8,10 +8,9 @@ use gpui::{
|
||||
WeakEntity, Window, div, rems,
|
||||
};
|
||||
use gpui_kit::assets::IconName;
|
||||
use gpui_kit::base::StyledExt as _;
|
||||
use gpui_kit::component::button::{Button, ButtonVariants};
|
||||
use gpui_kit::component::input::{Input, InputEvent, InputState};
|
||||
use gpui_kit::component::menu::{DropdownMenu, PopupMenuItem};
|
||||
use gpui_kit::component::menu::{ContextMenu, ContextMenuExt as _, DropdownMenu, PopupMenuItem};
|
||||
use gpui_kit::component::scroll::ScrollableElement;
|
||||
use gpui_kit::component::tab::{Tab, TabBar};
|
||||
use gpui_kit::component::{ActiveTheme, Disableable, Sizable, WindowExt, h_flex, v_flex};
|
||||
@@ -174,7 +173,7 @@ impl RelayManager {
|
||||
})
|
||||
}
|
||||
|
||||
fn gossip_rows(&self, cx: &mut Context<Self>) -> Vec<Stateful<Div>> {
|
||||
fn gossip_rows(&self, cx: &mut Context<Self>) -> Vec<ContextMenu<Stateful<Div>>> {
|
||||
let mut rows = Vec::new();
|
||||
|
||||
for (relay, metadata) in self.gossip.relays.iter() {
|
||||
@@ -245,7 +244,7 @@ impl RelayManager {
|
||||
})
|
||||
}
|
||||
|
||||
fn inbox_rows(&self, cx: &mut Context<Self>) -> Vec<Stateful<Div>> {
|
||||
fn inbox_rows(&self, cx: &mut Context<Self>) -> Vec<ContextMenu<Stateful<Div>>> {
|
||||
let mut rows = Vec::new();
|
||||
|
||||
for relay in self.inbox.relays.iter() {
|
||||
@@ -635,8 +634,9 @@ fn relay_row(
|
||||
badge: Option<SharedString>,
|
||||
on_remove: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
cx: &App,
|
||||
) -> Stateful<Div> {
|
||||
) -> ContextMenu<Stateful<Div>> {
|
||||
let label = SharedString::from(url.to_string());
|
||||
let on_remove = std::rc::Rc::new(on_remove);
|
||||
|
||||
h_flex()
|
||||
.id(label.clone())
|
||||
@@ -647,6 +647,16 @@ fn relay_row(
|
||||
.justify_between()
|
||||
.rounded(cx.theme().radius)
|
||||
.bg(cx.theme().sidebar)
|
||||
.context_menu({
|
||||
let on_remove = on_remove.clone();
|
||||
|
||||
move |menu, _window, _cx| {
|
||||
menu.item(PopupMenuItem::new("Remove").on_click({
|
||||
let on_remove = on_remove.clone();
|
||||
move |event, window, cx| on_remove(event, window, cx)
|
||||
}))
|
||||
}
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
@@ -654,11 +664,8 @@ fn relay_row(
|
||||
.child(label)
|
||||
.when_some(badge, |this, badge| {
|
||||
this.child(
|
||||
div()
|
||||
.p_0p5()
|
||||
.rounded_xs()
|
||||
.text_xs()
|
||||
.font_medium()
|
||||
gpui_kit::component::tag::Tag::secondary()
|
||||
.xsmall()
|
||||
.child(badge),
|
||||
)
|
||||
}),
|
||||
@@ -667,10 +674,13 @@ fn relay_row(
|
||||
Button::new("remove")
|
||||
.icon(IconName::X)
|
||||
.xsmall()
|
||||
.ghost()
|
||||
.danger()
|
||||
.invisible()
|
||||
.group_hover("", |this| this.visible())
|
||||
.on_click(on_remove),
|
||||
.on_click({
|
||||
let on_remove = on_remove.clone();
|
||||
move |event, window, cx| on_remove(event, window, cx)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ use anyhow::Error;
|
||||
use common::TimestampExt;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
App, AppContext, Context, Div, Entity, InteractiveElement, IntoElement, ParentElement, Render,
|
||||
SharedString, Styled, Subscription, Task, Window, div, px, relative, uniform_list,
|
||||
App, AppContext, Context, Div, Entity, IntoElement, ParentElement, Render, SharedString,
|
||||
Styled, Subscription, Task, Window, div, px, relative, uniform_list,
|
||||
};
|
||||
use gpui_kit::assets::IconName;
|
||||
use gpui_kit::base::StyledExt as _;
|
||||
@@ -313,7 +313,6 @@ impl Screening {
|
||||
.gap_1p5()
|
||||
.rounded(cx.theme().radius)
|
||||
.text_xs()
|
||||
.hover(|this| this.bg(cx.theme().primary))
|
||||
.child(
|
||||
Avatar::new(profile.avatar())
|
||||
.seed(profile.avatar_seed())
|
||||
@@ -545,6 +544,7 @@ impl Render for Screening {
|
||||
.child(
|
||||
Button::new("mutuals")
|
||||
.icon(IconName::Info)
|
||||
.tooltip("Show mutual contacts")
|
||||
.xsmall()
|
||||
.ghost()
|
||||
.disabled(mutuals == 0)
|
||||
|
||||
@@ -662,6 +662,7 @@ impl Render for Preferences {
|
||||
.child(
|
||||
Button::new("update-file-server")
|
||||
.icon(IconName::Check)
|
||||
.tooltip("Update")
|
||||
.ghost()
|
||||
.size_8()
|
||||
.on_click(cx.listener(move |this, _ev, window, cx| {
|
||||
|
||||
@@ -4,6 +4,7 @@ use gpui::{
|
||||
StatefulInteractiveElement, Styled, Window, div, px,
|
||||
};
|
||||
use gpui_kit::assets::IconName;
|
||||
use gpui_kit::component::tooltip::Tooltip;
|
||||
use gpui_kit::component::{ActiveTheme as _, Icon, Sizable as _, h_flex};
|
||||
|
||||
use super::TAB_BAR_HEIGHT;
|
||||
@@ -53,6 +54,15 @@ impl ControlIcon {
|
||||
}
|
||||
}
|
||||
|
||||
fn label(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Minimize => "Minimize",
|
||||
Self::Restore => "Restore",
|
||||
Self::Maximize => "Maximize",
|
||||
Self::Close => "Close",
|
||||
}
|
||||
}
|
||||
|
||||
fn window_control_area(&self) -> gpui::WindowControlArea {
|
||||
match self {
|
||||
Self::Minimize => gpui::WindowControlArea::Min,
|
||||
@@ -102,6 +112,8 @@ impl RenderOnce for ControlIcon {
|
||||
let active_bg = self.active_bg(cx);
|
||||
let icon = self.clone();
|
||||
|
||||
let label = self.label();
|
||||
|
||||
div()
|
||||
.id(self.id())
|
||||
.flex()
|
||||
@@ -112,6 +124,7 @@ impl RenderOnce for ControlIcon {
|
||||
.content_center()
|
||||
.items_center()
|
||||
.text_color(cx.theme().foreground)
|
||||
.tooltip(move |window, cx| Tooltip::new(label).build(window, cx))
|
||||
.hover(|style| style.bg(hover_bg).text_color(hover_fg))
|
||||
.active(|style| style.bg(active_bg).text_color(hover_fg))
|
||||
.when(is_windows, |this| {
|
||||
|
||||
@@ -13,10 +13,11 @@ use gpui::{
|
||||
Action, App, AppContext, Context, Entity, Focusable, InteractiveElement, IntoElement,
|
||||
ParentElement, Pixels, Render, SharedString, Styled, Subscription, Task, Window, div, px,
|
||||
};
|
||||
use gpui_kit::component::button::Button;
|
||||
use gpui_kit::component::button::{Button, ButtonVariant};
|
||||
use gpui_kit::component::command::{
|
||||
Command as CommandPalette, CommandGroup, CommandItem, CommandState,
|
||||
};
|
||||
use gpui_kit::component::dialog::DialogButtonProps;
|
||||
use gpui_kit::component::dock::{DockArea, DockLayout, DockPlacement, Panel, PanelHandle};
|
||||
use gpui_kit::component::notification::{Notification, NotificationType};
|
||||
use gpui_kit::component::{
|
||||
@@ -36,7 +37,6 @@ use crate::sidebar::Sidebar;
|
||||
|
||||
mod dialogs;
|
||||
mod dock;
|
||||
mod nav;
|
||||
mod panels;
|
||||
mod sidebar;
|
||||
|
||||
@@ -618,6 +618,12 @@ impl Workspace {
|
||||
.child(SharedString::from(ENC_WARN)),
|
||||
),
|
||||
)
|
||||
.button_props(
|
||||
DialogButtonProps::default()
|
||||
.cancel_text("Cancel")
|
||||
.ok_text("Reset key")
|
||||
.ok_variant(ButtonVariant::Danger),
|
||||
)
|
||||
.on_ok(move |_ev, _window, cx| {
|
||||
ent.update(cx, |this, cx| {
|
||||
this.set_announcement(Keys::generate(), cx);
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
AnyElement, App, ClickEvent, ElementId, InteractiveElement, Interactivity, IntoElement,
|
||||
ParentElement, RenderOnce, SharedString, StatefulInteractiveElement as _, StyleRefinement,
|
||||
Styled, Window, div,
|
||||
};
|
||||
use gpui_kit::component::{ActiveTheme, Selectable, StyledExt as _, h_flex};
|
||||
|
||||
/// A single navigation row, such as an entry in a sidebar list.
|
||||
#[derive(IntoElement)]
|
||||
pub struct Nav {
|
||||
id: ElementId,
|
||||
interactivity: Interactivity,
|
||||
style: StyleRefinement,
|
||||
prefix: Option<AnyElement>,
|
||||
label: SharedString,
|
||||
suffix: Option<AnyElement>,
|
||||
selected: bool,
|
||||
clickable: bool,
|
||||
center_prefix: bool,
|
||||
#[allow(clippy::type_complexity)]
|
||||
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
|
||||
}
|
||||
|
||||
impl Nav {
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
let id = id.into();
|
||||
let mut interactivity = Interactivity::default();
|
||||
interactivity.element_id = Some(id.clone());
|
||||
|
||||
Self {
|
||||
id,
|
||||
interactivity,
|
||||
style: StyleRefinement::default(),
|
||||
prefix: None,
|
||||
label: SharedString::default(),
|
||||
suffix: None,
|
||||
selected: false,
|
||||
clickable: false,
|
||||
center_prefix: false,
|
||||
on_click: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Marks the row as clickable even without an `on_click` handler, keeping the hover affordance.
|
||||
pub fn clickable(mut self, clickable: bool) -> Self {
|
||||
self.clickable = clickable;
|
||||
self
|
||||
}
|
||||
|
||||
/// Whether to center the prefix in the avatar-width column.
|
||||
pub fn center_prefix(mut self, center: bool) -> Self {
|
||||
self.center_prefix = center;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the element shown before the label, such as an avatar or icon.
|
||||
pub fn prefix(mut self, prefix: impl IntoElement) -> Self {
|
||||
self.prefix = Some(prefix.into_any_element());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the row's label.
|
||||
pub fn label(mut self, label: impl Into<SharedString>) -> Self {
|
||||
self.label = label.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the element shown after the label, such as a timestamp or badge.
|
||||
pub fn suffix(mut self, suffix: impl IntoElement) -> Self {
|
||||
self.suffix = Some(suffix.into_any_element());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_click(
|
||||
mut self,
|
||||
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_click = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Selectable for Nav {
|
||||
fn selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
|
||||
fn is_selected(&self) -> bool {
|
||||
self.selected
|
||||
}
|
||||
}
|
||||
|
||||
impl InteractiveElement for Nav {
|
||||
fn interactivity(&mut self) -> &mut Interactivity {
|
||||
&mut self.interactivity
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for Nav {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
&mut self.style
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Nav {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let clickable = self.on_click.is_some() || self.clickable;
|
||||
|
||||
h_flex()
|
||||
.id(self.id)
|
||||
.h_8()
|
||||
.w_full()
|
||||
.px_2()
|
||||
.gap_2()
|
||||
.rounded(cx.theme().radius)
|
||||
.when_some(self.prefix, |this, prefix| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.flex_shrink_0()
|
||||
.when(self.center_prefix, |this| this.w_6().justify_center())
|
||||
.child(prefix),
|
||||
)
|
||||
})
|
||||
.child(div().flex_1().min_w_0().truncate().child(self.label))
|
||||
.when_some(self.suffix, |this, suffix| {
|
||||
this.child(div().flex_shrink_0().child(suffix))
|
||||
})
|
||||
.when(clickable, |this| {
|
||||
this.cursor_pointer()
|
||||
.hover(|this| this.bg(cx.theme().list_hover))
|
||||
.when(self.selected, |this| this.bg(cx.theme().list_active))
|
||||
})
|
||||
.when_some(self.on_click, |this, handler| {
|
||||
this.on_click(move |event, window, cx| handler(event, window, cx))
|
||||
})
|
||||
.refine_style(&self.style)
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ use gpui_kit::base::StyledExt as _;
|
||||
use gpui_kit::component::button::{Button, ButtonVariants};
|
||||
use gpui_kit::component::dock::{Panel, PanelEvent};
|
||||
use gpui_kit::component::input::{Input, InputEvent, InputState};
|
||||
use gpui_kit::component::menu::{ContextMenuExt as _, PopupMenuItem};
|
||||
use gpui_kit::component::scroll::ScrollableElement;
|
||||
use gpui_kit::component::{ActiveTheme, Disableable, Sizable, WindowExt, h_flex, v_flex};
|
||||
use instant::Duration;
|
||||
@@ -229,6 +230,7 @@ impl ContactListPanel {
|
||||
|
||||
fn render_list_items(&mut self, cx: &mut Context<Self>) -> Vec<impl IntoElement> {
|
||||
let mut items = Vec::new();
|
||||
let panel = cx.weak_entity();
|
||||
|
||||
for (ix, public_key) in self.contacts.iter().enumerate() {
|
||||
let profile = self.directory.read(cx).person(public_key, cx);
|
||||
@@ -243,7 +245,24 @@ impl ContactListPanel {
|
||||
.px_2()
|
||||
.justify_between()
|
||||
.rounded(cx.theme().radius)
|
||||
.hover(|this| this.bg(cx.theme().primary))
|
||||
.hover(|this| this.bg(cx.theme().list_hover))
|
||||
.context_menu({
|
||||
let panel = panel.clone();
|
||||
let public_key = *public_key;
|
||||
|
||||
move |menu, _window, _cx| {
|
||||
menu.item(PopupMenuItem::new("Remove").on_click({
|
||||
let panel = panel.clone();
|
||||
move |_, _window, cx| {
|
||||
if let Err(error) = panel.update(cx, |panel, cx| {
|
||||
panel.remove(&public_key, cx);
|
||||
}) {
|
||||
log::warn!("removing a contact failed: {error}");
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
@@ -256,18 +275,21 @@ impl ContactListPanel {
|
||||
.child(profile.name()),
|
||||
)
|
||||
.child(
|
||||
Button::new("remove_{ix}")
|
||||
.icon(IconName::X)
|
||||
.xsmall()
|
||||
.ghost()
|
||||
.invisible()
|
||||
.group_hover("", |this| this.visible())
|
||||
.on_click({
|
||||
let public_key = public_key.to_owned();
|
||||
cx.listener(move |this, _ev, _window, cx| {
|
||||
this.remove(&public_key, cx);
|
||||
})
|
||||
}),
|
||||
Button::new(SharedString::from(format!(
|
||||
"remove-{}",
|
||||
public_key.to_hex()
|
||||
)))
|
||||
.icon(IconName::X)
|
||||
.xsmall()
|
||||
.ghost()
|
||||
.invisible()
|
||||
.group_hover("", |this| this.visible())
|
||||
.on_click({
|
||||
let public_key = public_key.to_owned();
|
||||
cx.listener(move |this, _ev, _window, cx| {
|
||||
this.remove(&public_key, cx);
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ use gpui_kit::assets::IconName;
|
||||
use gpui_kit::base::StyledExt as _;
|
||||
use gpui_kit::component::button::{Button, ButtonVariants};
|
||||
use gpui_kit::component::dock::{DockArea, DockPlacement, Panel, PanelEvent, PanelHandle};
|
||||
use gpui_kit::component::list::ListItem;
|
||||
use gpui_kit::component::scroll::ScrollableElement;
|
||||
use gpui_kit::component::{ActiveTheme, Icon, Sizable, h_flex, v_flex};
|
||||
use nostr_sdk::prelude::*;
|
||||
@@ -23,7 +24,6 @@ use ui::Avatar;
|
||||
|
||||
use super::browse;
|
||||
use crate::dock::add_panel_to;
|
||||
use crate::nav::Nav;
|
||||
use crate::sidebar::open_room;
|
||||
|
||||
/// How many requests and follow ups to show per section.
|
||||
@@ -457,9 +457,19 @@ impl GreeterPanel {
|
||||
v_flex()
|
||||
.w_full()
|
||||
.child(
|
||||
Nav::new("browse-group")
|
||||
.prefix(Icon::new(IconName::Compass))
|
||||
.label("Browse Public Groups")
|
||||
ListItem::new("browse-group")
|
||||
.accessibility_label("Browse Public Groups")
|
||||
.h_8()
|
||||
.rounded(cx.theme().radius)
|
||||
.cursor_pointer()
|
||||
.child(div().flex_shrink_0().child(Icon::new(IconName::Compass)))
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.truncate()
|
||||
.child("Browse Public Groups"),
|
||||
)
|
||||
.text_sm()
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
let panel = browse::init(this.dock.clone(), window, cx);
|
||||
@@ -473,9 +483,19 @@ impl GreeterPanel {
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Nav::new("browse-posts")
|
||||
.prefix(Icon::new(IconName::Link))
|
||||
.label("Browse Nostr via Jumble")
|
||||
ListItem::new("browse-posts")
|
||||
.accessibility_label("Browse Nostr via Jumble")
|
||||
.h_8()
|
||||
.rounded(cx.theme().radius)
|
||||
.cursor_pointer()
|
||||
.child(div().flex_shrink_0().child(Icon::new(IconName::Link)))
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.truncate()
|
||||
.child("Browse Nostr via Jumble"),
|
||||
)
|
||||
.text_sm()
|
||||
.on_click(|_, _, cx| {
|
||||
cx.open_url(JUMBLE_URL);
|
||||
|
||||
@@ -6,18 +6,19 @@ use gpui::{
|
||||
AnyElement, ClipboardItem, Context, Entity, InteractiveElement as _, IntoElement,
|
||||
ParentElement, SharedString, Styled, WeakEntity, div, px, relative, uniform_list,
|
||||
};
|
||||
use gpui_kit::assets::IconName;
|
||||
use gpui_kit::base::StyledExt as _;
|
||||
use gpui_kit::component::list::ListItem;
|
||||
use gpui_kit::component::menu::{ContextMenuExt as _, PopupMenuItem};
|
||||
use gpui_kit::component::{ActiveTheme, Icon, Sizable, h_flex, v_flex};
|
||||
use nip29::Group;
|
||||
use nostr_sdk::prelude::*;
|
||||
use settings::AppSettings;
|
||||
use state::NostrRegistry;
|
||||
use ui::{Avatar, IconName};
|
||||
use ui::Avatar;
|
||||
|
||||
use super::Sidebar;
|
||||
use crate::dialogs::profile;
|
||||
use crate::nav::Nav;
|
||||
|
||||
/// A collapsible section of the group view.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -222,19 +223,25 @@ impl Sidebar {
|
||||
GroupSection::Members => self.members_open,
|
||||
};
|
||||
let icon = if open {
|
||||
IconName::CaretDown
|
||||
IconName::ChevronDown
|
||||
} else {
|
||||
IconName::CaretRight
|
||||
IconName::ChevronRight
|
||||
};
|
||||
let label = section.label();
|
||||
|
||||
Nav::new(label)
|
||||
.label(label)
|
||||
.suffix(
|
||||
Icon::new(icon)
|
||||
.small()
|
||||
.text_color(cx.theme().muted_foreground),
|
||||
)
|
||||
ListItem::new(label)
|
||||
.accessibility_label(label)
|
||||
.h_8()
|
||||
.rounded(cx.theme().radius)
|
||||
.cursor_pointer()
|
||||
.child(div().flex_1().min_w_0().truncate().child(label))
|
||||
.suffix(move |_, cx| {
|
||||
div().flex_shrink_0().child(
|
||||
Icon::new(icon)
|
||||
.small()
|
||||
.text_color(cx.theme().muted_foreground),
|
||||
)
|
||||
})
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
@@ -262,6 +269,7 @@ impl Sidebar {
|
||||
let group = group.clone();
|
||||
|
||||
let public_key = *public_key;
|
||||
let member = person.name();
|
||||
let member_is_admin = !roles.is_empty();
|
||||
|
||||
let is_self = NostrRegistry::global(cx)
|
||||
@@ -269,31 +277,40 @@ impl Sidebar {
|
||||
.current_user()
|
||||
.is_some_and(|me| me == public_key);
|
||||
|
||||
let nav = Nav::new(public_key.to_hex())
|
||||
.label(person.name())
|
||||
let item = ListItem::new(public_key.to_hex())
|
||||
.accessibility_label(member.clone())
|
||||
.h_8()
|
||||
.rounded(cx.theme().radius)
|
||||
.cursor_pointer()
|
||||
.text_sm()
|
||||
.clickable(true)
|
||||
.when(!hide_avatar, |this| {
|
||||
this.prefix(
|
||||
Avatar::new(person.avatar())
|
||||
.seed(person.avatar_seed())
|
||||
.small(),
|
||||
this.child(
|
||||
div().flex_shrink_0().child(
|
||||
Avatar::new(person.avatar())
|
||||
.seed(person.avatar_seed())
|
||||
.small(),
|
||||
),
|
||||
)
|
||||
})
|
||||
.child(div().flex_1().min_w_0().truncate().child(person.name()))
|
||||
.when(!roles.is_empty(), |this| {
|
||||
this.suffix(
|
||||
div()
|
||||
.font_normal()
|
||||
.truncate()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(SharedString::from(roles.join(", "))),
|
||||
)
|
||||
let roles = SharedString::from(roles.join(", "));
|
||||
|
||||
this.suffix(move |_, cx| {
|
||||
div().flex_shrink_0().child(
|
||||
div()
|
||||
.font_normal()
|
||||
.truncate()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(roles.clone()),
|
||||
)
|
||||
})
|
||||
});
|
||||
let row = div()
|
||||
.id(public_key.to_hex())
|
||||
.w_full()
|
||||
.child(nav)
|
||||
.child(item)
|
||||
.context_menu(move |menu, _window, _cx| {
|
||||
let menu = menu
|
||||
.item(
|
||||
@@ -326,13 +343,21 @@ impl Sidebar {
|
||||
})
|
||||
});
|
||||
|
||||
menu.separator()
|
||||
.item(PopupMenuItem::new("Remove").on_click({
|
||||
menu.separator().item({
|
||||
let member = member.clone();
|
||||
PopupMenuItem::new("Remove").on_click({
|
||||
let group = group.clone();
|
||||
move |_event, window, cx| {
|
||||
nip29_ui::remove_member(group.clone(), public_key, window, cx);
|
||||
nip29_ui::confirm_remove_member(
|
||||
group.clone(),
|
||||
public_key,
|
||||
member.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}))
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
row.into_any_element()
|
||||
|
||||
@@ -7,12 +7,14 @@ use gpui::{
|
||||
};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use gpui::{AnyElement, Entity};
|
||||
use gpui_kit::assets::IconName;
|
||||
use gpui_kit::base::StyledExt as _;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use gpui_kit::component::Disableable;
|
||||
use gpui_kit::component::button::{Button, ButtonVariant, ButtonVariants};
|
||||
use gpui_kit::component::clipboard::Clipboard;
|
||||
use gpui_kit::component::dialog::DialogButtonProps;
|
||||
use gpui_kit::component::list::ListItem;
|
||||
use gpui_kit::component::menu::{DropdownMenu, PopupMenuItem};
|
||||
use gpui_kit::component::popover::Popover;
|
||||
use gpui_kit::component::separator::Separator;
|
||||
@@ -21,12 +23,11 @@ use nostr_sdk::prelude::*;
|
||||
use person::shorten_pubkey;
|
||||
use settings::AppSettings;
|
||||
use state::{CLIENT_NAME, NostrRegistry};
|
||||
use ui::{Avatar, IconName};
|
||||
use ui::Avatar;
|
||||
|
||||
use super::Sidebar;
|
||||
use crate::Command;
|
||||
use crate::dock::TAB_BAR_HEIGHT;
|
||||
use crate::nav::Nav;
|
||||
|
||||
impl Sidebar {
|
||||
pub(super) fn render_user(
|
||||
@@ -101,7 +102,7 @@ impl Sidebar {
|
||||
)
|
||||
.child(
|
||||
Button::new("sidebar-new")
|
||||
.icon(IconName::PlusChat)
|
||||
.icon(IconName::MessageSquarePlus)
|
||||
.tooltip("New")
|
||||
.ghost()
|
||||
.small()
|
||||
@@ -211,7 +212,7 @@ impl Sidebar {
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn render_footer(&self, loading: bool, _cx: &mut Context<Self>) -> Div {
|
||||
pub(super) fn render_footer(&self, loading: bool, cx: &mut Context<Self>) -> Div {
|
||||
v_flex()
|
||||
.p_2()
|
||||
.flex_shrink_0()
|
||||
@@ -220,10 +221,19 @@ impl Sidebar {
|
||||
.justify_start()
|
||||
.when(!loading, |this| {
|
||||
this.child(
|
||||
Nav::new("settings")
|
||||
.prefix(Icon::new(IconName::Settings).small())
|
||||
.center_prefix(true)
|
||||
.label("Settings")
|
||||
ListItem::new("settings")
|
||||
.accessibility_label("Settings")
|
||||
.h_8()
|
||||
.rounded(cx.theme().radius)
|
||||
.cursor_pointer()
|
||||
.child(
|
||||
div()
|
||||
.w_6()
|
||||
.justify_center()
|
||||
.flex_shrink_0()
|
||||
.child(Icon::new(IconName::Settings).small()),
|
||||
)
|
||||
.child(div().flex_1().min_w_0().truncate().child("Settings"))
|
||||
.text_sm()
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(Box::new(Command::ShowSettings), cx)
|
||||
@@ -243,7 +253,7 @@ impl Sidebar {
|
||||
.when(!up_to_date, |this| {
|
||||
this.child(
|
||||
Button::new("update")
|
||||
.icon(IconName::ArrowDownCircle)
|
||||
.icon(IconName::CircleArrowDown)
|
||||
.tooltip(status)
|
||||
.small()
|
||||
.ghost()
|
||||
@@ -253,7 +263,7 @@ impl Sidebar {
|
||||
.when(staged, |this| {
|
||||
this.child(
|
||||
Button::new("restart")
|
||||
.icon(IconName::ArrowDownCircle)
|
||||
.icon(IconName::CircleArrowDown)
|
||||
.tooltip("Quit and relaunch into the installed update")
|
||||
.small()
|
||||
.ghost()
|
||||
|
||||
@@ -2,19 +2,18 @@ use std::rc::Rc;
|
||||
|
||||
use chat::{ChatRegistry, RoomKind};
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
AnyElement, App, Context, IntoElement, ParentElement, SharedString, Styled, div, uniform_list,
|
||||
};
|
||||
use gpui::{AnyElement, App, Context, IntoElement, ParentElement, Styled, div, uniform_list};
|
||||
use gpui_kit::assets::IconName;
|
||||
use gpui_kit::base::StyledExt as _;
|
||||
use gpui_kit::component::badge::Badge;
|
||||
use gpui_kit::component::list::ListItem;
|
||||
use gpui_kit::component::{ActiveTheme, Icon, Sizable, h_flex, v_flex};
|
||||
use nip29::GroupsRegistry;
|
||||
use settings::AppSettings;
|
||||
use ui::IconName;
|
||||
|
||||
use super::Sidebar;
|
||||
use super::rows::SidebarRow;
|
||||
use crate::Command;
|
||||
use crate::nav::Nav;
|
||||
|
||||
/// A collapsible section of the sidebar's main list.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -38,6 +37,7 @@ impl Sidebar {
|
||||
pub(super) fn render_list(&mut self, cx: &mut Context<Self>) -> AnyElement {
|
||||
let requests_open = self.requests_open;
|
||||
let contacts_open = self.contacts_open && !requests_open;
|
||||
let new_requests = self.new_requests;
|
||||
let rows = Rc::new(self.rows_for(cx));
|
||||
// Whether to show the create button for the contact picker
|
||||
let create_button = contacts_open && !self.selected_contacts.is_empty();
|
||||
@@ -69,9 +69,17 @@ impl Sidebar {
|
||||
.justify_start()
|
||||
.when(cfg!(not(target_arch = "wasm32")), |this| {
|
||||
this.child(
|
||||
Nav::new("wallet")
|
||||
.prefix(Icon::new(gpui_kit::assets::IconName::Wallet).small())
|
||||
.label("Wallet")
|
||||
ListItem::new("wallet")
|
||||
.accessibility_label("Wallet")
|
||||
.h_8()
|
||||
.rounded(cx.theme().radius)
|
||||
.cursor_pointer()
|
||||
.child(
|
||||
div().flex_shrink_0().child(
|
||||
Icon::new(gpui_kit::assets::IconName::Wallet).small(),
|
||||
),
|
||||
)
|
||||
.child(div().flex_1().min_w_0().truncate().child("Wallet"))
|
||||
.text_sm()
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(Box::new(crate::Command::ShowWallet), cx)
|
||||
@@ -79,48 +87,55 @@ impl Sidebar {
|
||||
)
|
||||
})
|
||||
.child(
|
||||
Nav::new("browse")
|
||||
.prefix(Icon::new(IconName::Compass).small())
|
||||
.label("Browse")
|
||||
ListItem::new("browse")
|
||||
.accessibility_label("Browse")
|
||||
.h_8()
|
||||
.rounded(cx.theme().radius)
|
||||
.cursor_pointer()
|
||||
.child(
|
||||
div()
|
||||
.flex_shrink_0()
|
||||
.child(Icon::new(IconName::Compass).small()),
|
||||
)
|
||||
.child(div().flex_1().min_w_0().truncate().child("Browse"))
|
||||
.text_sm()
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(Box::new(Command::ShowBrowse), cx)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Nav::new("reqs")
|
||||
.prefix(Icon::new(IconName::Invite).small())
|
||||
.label("Requests")
|
||||
ListItem::new("reqs")
|
||||
.accessibility_label("Requests")
|
||||
.h_8()
|
||||
.rounded(cx.theme().radius)
|
||||
.cursor_pointer()
|
||||
.child(
|
||||
div()
|
||||
.flex_shrink_0()
|
||||
.child(Icon::new(IconName::UserPlus).small()),
|
||||
)
|
||||
.child(div().flex_1().min_w_0().truncate().child("Requests"))
|
||||
.text_sm()
|
||||
.when(request_count > 0, |this| {
|
||||
this.suffix(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
h_flex()
|
||||
.py_0p5()
|
||||
.px_1()
|
||||
.min_w_6()
|
||||
.justify_center()
|
||||
.text_xs()
|
||||
.font_medium()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.text_center()
|
||||
.rounded(cx.theme().radius)
|
||||
.bg(cx.theme().secondary)
|
||||
.child(SharedString::from(
|
||||
request_count.to_string(),
|
||||
)),
|
||||
)
|
||||
.when(self.new_requests && !requests_open, |this| {
|
||||
this.child(
|
||||
div()
|
||||
.size_1()
|
||||
.rounded_full()
|
||||
.bg(cx.theme().primary),
|
||||
this.suffix(move |_, cx| {
|
||||
div().flex_shrink_0().child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Badge::new()
|
||||
.count(request_count)
|
||||
.child(div().size_4()),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.when(new_requests && !requests_open, |this| {
|
||||
this.child(
|
||||
div()
|
||||
.size_1()
|
||||
.rounded_full()
|
||||
.bg(cx.theme().primary),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
.when(requests_open, |this| this.bg(cx.theme().list_active))
|
||||
.on_click(cx.listener(|this, _event, _window, cx| {
|
||||
@@ -128,9 +143,17 @@ impl Sidebar {
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Nav::new("contacts")
|
||||
.prefix(Icon::new(IconName::Book).small())
|
||||
.label("Contacts")
|
||||
ListItem::new("contacts")
|
||||
.accessibility_label("Contacts")
|
||||
.h_8()
|
||||
.rounded(cx.theme().radius)
|
||||
.cursor_pointer()
|
||||
.child(
|
||||
div()
|
||||
.flex_shrink_0()
|
||||
.child(Icon::new(IconName::Book).small()),
|
||||
)
|
||||
.child(div().flex_1().min_w_0().truncate().child("Contacts"))
|
||||
.text_sm()
|
||||
.when(contacts_open, |this| this.bg(cx.theme().list_active))
|
||||
.on_click(cx.listener(|this, _event, window, cx| {
|
||||
|
||||
@@ -5,23 +5,25 @@ use common::TimestampExt;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
AnyElement, App, Context, Entity, InteractiveElement as _, IntoElement, ParentElement,
|
||||
SharedString, Styled, WeakEntity, Window, div, px,
|
||||
SharedString, Styled, WeakEntity, Window, div,
|
||||
};
|
||||
use gpui_kit::assets::IconName;
|
||||
use gpui_kit::base::StyledExt as _;
|
||||
use gpui_kit::component::dialog::DialogButtonProps;
|
||||
use gpui_kit::component::dock::{ClosePanel, DockArea, DockPlacement, PanelHandle};
|
||||
use gpui_kit::component::list::ListItem;
|
||||
use gpui_kit::component::menu::{ContextMenuExt as _, PopupMenuItem};
|
||||
use gpui_kit::component::{ActiveTheme, Icon, Selectable, Sizable, WindowExt, h_flex};
|
||||
use gpui_kit::component::tag::Tag;
|
||||
use gpui_kit::component::{ActiveTheme, Icon, Sizable, WindowExt, h_flex};
|
||||
use nip29::Group;
|
||||
use nostr_sdk::prelude::*;
|
||||
use settings::AppSettings;
|
||||
use ui::{Avatar, IconName};
|
||||
use ui::Avatar;
|
||||
|
||||
use super::Sidebar;
|
||||
use super::list::ListSection;
|
||||
use super::search::MessageHit;
|
||||
use crate::dialogs::{profile, screening};
|
||||
use crate::nav::Nav;
|
||||
|
||||
/// Open a room in the dock, screening new requests when enabled.
|
||||
pub(crate) fn open_room(
|
||||
@@ -92,13 +94,10 @@ impl Sidebar {
|
||||
) -> Vec<AnyElement> {
|
||||
let hide_avatar = AppSettings::get_hide_avatar(cx);
|
||||
|
||||
rows.get(range.clone())
|
||||
rows.get(range)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.enumerate()
|
||||
.map(|(offset, row)| {
|
||||
let index = range.start + offset;
|
||||
|
||||
.map(|row| {
|
||||
let element = match row {
|
||||
SidebarRow::Section(section) => {
|
||||
let section = *section;
|
||||
@@ -111,18 +110,24 @@ impl Sidebar {
|
||||
};
|
||||
|
||||
let icon = if open {
|
||||
IconName::CaretDown
|
||||
IconName::ChevronDown
|
||||
} else {
|
||||
IconName::CaretRight
|
||||
IconName::ChevronRight
|
||||
};
|
||||
|
||||
Nav::new(label)
|
||||
.label(label)
|
||||
.suffix(
|
||||
Icon::new(icon)
|
||||
.small()
|
||||
.text_color(cx.theme().muted_foreground),
|
||||
)
|
||||
ListItem::new(label)
|
||||
.accessibility_label(label)
|
||||
.h_8()
|
||||
.rounded(cx.theme().radius)
|
||||
.cursor_pointer()
|
||||
.child(div().flex_1().min_w_0().truncate().child(label))
|
||||
.suffix(move |_, cx| {
|
||||
div().flex_shrink_0().child(
|
||||
Icon::new(icon)
|
||||
.small()
|
||||
.text_color(cx.theme().muted_foreground),
|
||||
)
|
||||
})
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
@@ -143,8 +148,11 @@ impl Sidebar {
|
||||
SidebarRow::Heading(label) => {
|
||||
let label = *label;
|
||||
|
||||
Nav::new(label)
|
||||
.label(label)
|
||||
ListItem::new(label)
|
||||
.accessibility_label(label)
|
||||
.h_8()
|
||||
.rounded(cx.theme().radius)
|
||||
.child(div().flex_1().min_w_0().truncate().child(label))
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
@@ -171,48 +179,48 @@ impl Sidebar {
|
||||
let dock = self.dock.clone();
|
||||
let room = room.clone();
|
||||
|
||||
let nav = Nav::new(index)
|
||||
.label(name)
|
||||
let item = ListItem::new(id)
|
||||
.accessibility_label(name.clone())
|
||||
.h_8()
|
||||
.rounded(cx.theme().radius)
|
||||
.cursor_pointer()
|
||||
.text_sm()
|
||||
.when(!hide_avatar, |this| {
|
||||
this.prefix(
|
||||
Avatar::new(room.read(cx).display_image(cx))
|
||||
.seed(seed)
|
||||
.small(),
|
||||
this.child(
|
||||
div().flex_shrink_0().child(
|
||||
Avatar::new(room.read(cx).display_image(cx))
|
||||
.seed(seed)
|
||||
.small(),
|
||||
),
|
||||
)
|
||||
})
|
||||
.child(div().flex_1().min_w_0().truncate().child(name))
|
||||
.suffix(move |_, cx| {
|
||||
div().flex_shrink_0().child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.when(spam, |this| {
|
||||
this.child(Tag::danger().xsmall().child("SPAM"))
|
||||
})
|
||||
.when(!spam, |this| {
|
||||
this.child(
|
||||
div()
|
||||
.font_normal()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(created_at.clone()),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
.suffix(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.when(spam, |this| {
|
||||
this.child(
|
||||
div()
|
||||
.px_1()
|
||||
.rounded(cx.theme().radius)
|
||||
.bg(cx.theme().danger.opacity(0.15))
|
||||
.text_size(px(8.0))
|
||||
.text_color(cx.theme().danger)
|
||||
.child("SPAM"),
|
||||
)
|
||||
})
|
||||
.when(!spam, |this| {
|
||||
this.child(
|
||||
div()
|
||||
.font_normal()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(created_at),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.on_click(move |_event, window, cx| {
|
||||
open_room(&room, &dock, window, cx);
|
||||
});
|
||||
|
||||
div()
|
||||
.id(index)
|
||||
.id(id)
|
||||
.w_full()
|
||||
.child(nav)
|
||||
.child(item)
|
||||
.context_menu(move |menu, _window, _cx| {
|
||||
let menu = menu.item(
|
||||
PopupMenuItem::new(if pinned { "Unpin" } else { "Pin" })
|
||||
@@ -241,16 +249,22 @@ impl Sidebar {
|
||||
let sidebar = cx.entity().downgrade();
|
||||
let public_key = public_key.to_owned();
|
||||
|
||||
Nav::new(index)
|
||||
.label(person.name())
|
||||
ListItem::new(public_key.to_hex())
|
||||
.accessibility_label(person.name())
|
||||
.h_8()
|
||||
.rounded(cx.theme().radius)
|
||||
.cursor_pointer()
|
||||
.text_sm()
|
||||
.when(!hide_avatar, |this| {
|
||||
this.prefix(
|
||||
Avatar::new(person.avatar())
|
||||
.seed(person.avatar_seed())
|
||||
.small(),
|
||||
this.child(
|
||||
div().flex_shrink_0().child(
|
||||
Avatar::new(person.avatar())
|
||||
.seed(person.avatar_seed())
|
||||
.small(),
|
||||
),
|
||||
)
|
||||
})
|
||||
.child(div().flex_1().min_w_0().truncate().child(person.name()))
|
||||
.selected(selected)
|
||||
.on_click(move |_event, _window, cx| {
|
||||
if let Err(error) = sidebar.update(cx, |this, cx| {
|
||||
@@ -265,16 +279,22 @@ impl Sidebar {
|
||||
let person = self.directory.read(cx).person(public_key, cx);
|
||||
let public_key = public_key.to_owned();
|
||||
|
||||
Nav::new(index)
|
||||
.label(person.name())
|
||||
ListItem::new(public_key.to_hex())
|
||||
.accessibility_label(person.name())
|
||||
.h_8()
|
||||
.rounded(cx.theme().radius)
|
||||
.cursor_pointer()
|
||||
.text_sm()
|
||||
.when(!hide_avatar, |this| {
|
||||
this.prefix(
|
||||
Avatar::new(person.avatar())
|
||||
.seed(person.avatar_seed())
|
||||
.small(),
|
||||
this.child(
|
||||
div().flex_shrink_0().child(
|
||||
Avatar::new(person.avatar())
|
||||
.seed(person.avatar_seed())
|
||||
.small(),
|
||||
),
|
||||
)
|
||||
})
|
||||
.child(div().flex_1().min_w_0().truncate().child(person.name()))
|
||||
.on_click(move |_event, window, cx| {
|
||||
profile::open(public_key, window, cx);
|
||||
})
|
||||
@@ -298,24 +318,32 @@ impl Sidebar {
|
||||
.or_else(|| group.read(cx).elsewhere().map(|_| "Moved"))
|
||||
.map(SharedString::from);
|
||||
|
||||
let nav = Nav::new(index)
|
||||
.label(name)
|
||||
let item = ListItem::new(tag.clone())
|
||||
.accessibility_label(name.clone())
|
||||
.h_8()
|
||||
.rounded(cx.theme().radius)
|
||||
.cursor_pointer()
|
||||
.text_sm()
|
||||
.when(!hide_avatar, |this| {
|
||||
this.prefix(
|
||||
Avatar::new(group.read(cx).display_image())
|
||||
.seed(seed)
|
||||
.small(),
|
||||
this.child(
|
||||
div().flex_shrink_0().child(
|
||||
Avatar::new(group.read(cx).display_image())
|
||||
.seed(seed)
|
||||
.small(),
|
||||
),
|
||||
)
|
||||
})
|
||||
.child(div().flex_1().min_w_0().truncate().child(name))
|
||||
.when_some(suffix, |this, suffix| {
|
||||
this.suffix(
|
||||
div()
|
||||
.font_normal()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(suffix),
|
||||
)
|
||||
this.suffix(move |_, cx| {
|
||||
div().flex_shrink_0().child(
|
||||
div()
|
||||
.font_normal()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(suffix.clone()),
|
||||
)
|
||||
})
|
||||
})
|
||||
.on_click(move |_event, window, cx| {
|
||||
crate::dock::add_panel_to(
|
||||
@@ -334,9 +362,9 @@ impl Sidebar {
|
||||
});
|
||||
|
||||
div()
|
||||
.id(index)
|
||||
.id(tag.clone())
|
||||
.w_full()
|
||||
.child(nav)
|
||||
.child(item)
|
||||
.context_menu(move |menu, _window, _cx| {
|
||||
menu.item(
|
||||
PopupMenuItem::new(if pinned { "Unpin" } else { "Pin" })
|
||||
@@ -371,24 +399,32 @@ impl Sidebar {
|
||||
let room = hit.room.clone();
|
||||
let content = hit.content.clone();
|
||||
|
||||
Nav::new(index)
|
||||
.label(content)
|
||||
ListItem::new(hit.author.to_hex())
|
||||
.accessibility_label(content.clone())
|
||||
.h_8()
|
||||
.rounded(cx.theme().radius)
|
||||
.cursor_pointer()
|
||||
.text_sm()
|
||||
.font_medium()
|
||||
.when(!hide_avatar, |this| {
|
||||
this.prefix(
|
||||
Avatar::new(person.avatar())
|
||||
.seed(person.avatar_seed())
|
||||
.small(),
|
||||
this.child(
|
||||
div().flex_shrink_0().child(
|
||||
Avatar::new(person.avatar())
|
||||
.seed(person.avatar_seed())
|
||||
.small(),
|
||||
),
|
||||
)
|
||||
})
|
||||
.child(div().flex_1().min_w_0().truncate().child(content))
|
||||
.suffix(move |_, cx| {
|
||||
div().flex_shrink_0().child(
|
||||
div()
|
||||
.font_normal()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(detail.clone()),
|
||||
)
|
||||
})
|
||||
.suffix(
|
||||
div()
|
||||
.font_normal()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(detail),
|
||||
)
|
||||
.on_click(move |_event, window, cx| {
|
||||
open_room(&room, &dock, window, cx);
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user