migrate ui components to gpui-kit

This commit is contained in:
2026-10-21 13:40:29 +07:00
parent 0d4acf5b65
commit d0ad1a6f57
173 changed files with 2775 additions and 10885 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ publish.workspace = true
[dependencies]
ui = { path = "../ui" }
theme = { path = "../theme" }
gpui-kit.workspace = true
common = { path = "../common" }
state = { path = "../state" }
nip4e = { path = "../nip4e" }
+13 -13
View File
@@ -4,20 +4,20 @@ use gpui::{
App, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled,
Subscription, Task, Window, div, px, relative,
};
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::input::{Input, InputEvent, InputState};
use gpui_kit::component::{ActiveTheme, Disableable, WindowExt, v_flex};
use instant::Duration;
use nostr_connect::prelude::*;
use state::NostrRegistry;
use theme::{ActiveTheme, TextStyle, Typeset as _, surface_radius};
use ui::button::{Button, ButtonVariants};
use ui::input::{Input, InputEvent, InputState};
use ui::{Disableable, WindowExtension, v_flex};
use ui::{TextStyle, Typeset as _, surface_radius};
pub fn open(window: &mut Window, cx: &mut App) {
let import = cx.new(|cx| ImportIdentity::new(window, cx));
window.open_dialog(cx, move |this, _window, _cx| {
this.width(px(450.))
.show_close(false)
.close_button(false)
.overlay_closable(false)
.keyboard(false)
.title("Continue with your Nostr Identity")
@@ -31,7 +31,7 @@ pub fn open_unlock(window: &mut Window, cx: &mut App) {
window.open_dialog(cx, move |this, _window, _cx| {
this.width(px(450.))
.show_close(false)
.close_button(false)
.overlay_closable(false)
.keyboard(false)
.title("Unlock your identity")
@@ -242,12 +242,12 @@ impl Render for ImportIdentity {
.text_style(TextStyle::Callout)
.child(
v_flex()
.gap(px(theme::SPACE))
.gap(px(ui::SPACE))
.when(!is_unlock, |this| {
this.child(
v_flex()
.gap_1()
.text_color(cx.theme().text_muted)
.text_color(cx.theme().muted_foreground)
.child("Use secret key or bunker connection")
.child(Input::new(&self.key_input)),
)
@@ -256,7 +256,7 @@ impl Render for ImportIdentity {
this.child(
v_flex()
.gap_1()
.text_color(cx.theme().text_muted)
.text_color(cx.theme().muted_foreground)
.child(if is_unlock {
"Passphrase to unlock your identity:"
} else {
@@ -269,14 +269,14 @@ impl Render for ImportIdentity {
this.child(
v_flex()
.gap_1()
.text_color(cx.theme().text_muted)
.text_color(cx.theme().muted_foreground)
.child("Passphrase to encrypt your key on this device:")
.child(Input::new(&self.pass_input)),
)
.child(
v_flex()
.gap_1()
.text_color(cx.theme().text_muted)
.text_color(cx.theme().muted_foreground)
.child("Repeat Passphrase:")
.child(Input::new(&self.repass_input)),
)
@@ -286,7 +286,7 @@ impl Render for ImportIdentity {
div()
.p_2()
.rounded(px(surface_radius()))
.bg(cx.theme().warning_muted)
.bg(cx.theme().warning.opacity(0.15))
.text_style(TextStyle::Caption)
.text_color(cx.theme().warning)
.line_height(relative(1.2))
@@ -298,7 +298,7 @@ impl Render for ImportIdentity {
div()
.p_2()
.rounded(px(surface_radius()))
.bg(cx.theme().warning_muted)
.bg(cx.theme().warning.opacity(0.15))
.text_style(TextStyle::Caption)
.text_color(cx.theme().warning)
.line_height(relative(1.2))
+6 -6
View File
@@ -1,11 +1,12 @@
use gpui_kit::assets::IconName;
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::input::{Input, InputState};
use gpui_kit::component::notification::Notification;
use gpui_kit::component::{WindowExt, h_flex, v_flex};
use std::rc::Rc;
use gpui::{App, AppContext, ClickEvent, ParentElement, Styled, Window, px};
use nip29::{GroupReference, GroupsRegistry};
use ui::button::{Button, ButtonVariants, IconPosition};
use ui::input::{Input, InputState};
use ui::notification::Notification;
use ui::{IconName, StyledExt, WindowExtension, h_flex, v_flex};
type SubmitFn = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) -> bool>;
@@ -57,10 +58,9 @@ pub fn open(window: &mut Window, cx: &mut App) {
.child(
Button::new("join")
.icon(IconName::ArrowRight)
.icon_position(IconPosition::End)
.label("Join")
.primary()
.font_semibold()
.font_weight(gpui::FontWeight::SEMIBOLD)
.on_click({
let join = join.clone();
move |event, window, cx| {
+21 -25
View File
@@ -1,3 +1,10 @@
use crate::nav::Nav;
use gpui_kit::assets::IconName;
use gpui_kit::component::avatar::Avatar;
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::{ActiveTheme, Disableable, Sizable, WindowExt, h_flex, v_flex};
use std::ops::Range;
use std::rc::Rc;
@@ -13,13 +20,7 @@ use nostr_sdk::prelude::*;
use person::Directory;
use settings::AppSettings;
use state::NostrRegistry;
use theme::{ActiveTheme, TextStyle, Typeset as _};
use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants, IconPosition};
use ui::dock::{DockArea, DockPlacement, PanelHandle};
use ui::input::{Input, InputEvent, InputState};
use ui::nav::Nav;
use ui::{Disableable, IconName, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
use ui::{TextStyle, Typeset as _};
/// Maximum number of members a chat can have.
const MAX_MEMBERS: usize = 3;
@@ -301,7 +302,7 @@ impl NewChat {
room
});
ui::dock::add_panel_to(
crate::dock::add_panel_to(
&self.dock,
PanelHandle::new(chat_ui::init(room.downgrade(), window, cx)),
DockPlacement::Center,
@@ -338,12 +339,10 @@ impl NewChat {
.label(profile.name())
.text_style(TextStyle::Callout)
.when(!AppSettings::get_hide_avatar(cx), |this| {
this.prefix(
Avatar::from_source(profile.avatar())
.seed(profile.avatar_seed())
.small()
.flex_shrink_0(),
)
this.prefix(match profile.avatar() {
Some(picture) => Avatar::new().src(picture).small().into_any_element(),
None => Avatar::new().small().flex_shrink_0().into_any_element(),
})
})
.on_click(cx.listener(move |this, _event, _window, cx| {
this.toggle_member(&public_key, cx);
@@ -367,15 +366,13 @@ impl NewChat {
.gap_1()
.p_0p5()
.rounded_full()
.bg(cx.theme().surface_raised)
.bg(cx.theme().secondary)
.text_style(TextStyle::Caption2)
.hover(|this| this.bg(cx.theme().element_hover))
.child(
Avatar::from_source(profile.avatar())
.seed(profile.avatar_seed())
.xsmall()
.flex_shrink_0(),
)
.hover(|this| this.bg(cx.theme().primary))
.child(match profile.avatar() {
Some(picture) => Avatar::new().src(picture).xsmall().into_any_element(),
None => Avatar::new().xsmall().flex_shrink_0().into_any_element(),
})
.child(div().max_w_32().truncate().child(profile.name()))
.on_click(cx.listener(move |this, _event, _window, cx| {
this.toggle_member(&public_key, cx);
@@ -424,7 +421,7 @@ impl Render for NewChat {
this.child(
div()
.text_style(TextStyle::Caption)
.text_color(cx.theme().text_faint)
.text_color(cx.theme().muted_foreground)
.child(notice),
)
}
@@ -452,10 +449,9 @@ impl Render for NewChat {
.child(
Button::new("start")
.icon(IconName::ArrowRight)
.icon_position(IconPosition::End)
.label("Start")
.primary()
.font_semibold()
.font_weight(gpui::FontWeight::SEMIBOLD)
.on_click(cx.listener(move |this, _event, window, cx| {
if this.start_chat(window, cx) {
window.close_dialog(cx);
+81 -72
View File
@@ -6,19 +6,21 @@ use gpui::{
Styled, Subscription, Task, WeakEntity, Window, div, px, rems,
};
use gpui_base::{Popup, Select};
use gpui_kit::assets::IconName;
use gpui_kit::component::ActiveTheme;
use gpui_kit::component::avatar::Avatar;
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::dock::{DockArea, DockPlacement, PanelHandle};
use gpui_kit::component::form::field;
use gpui_kit::component::input::{Input, InputEvent, InputState, Textarea, TextareaState};
use gpui_kit::component::switch::Switch;
use gpui_kit::component::{Disableable, Icon, Sizable, WindowExt, h_flex, v_flex};
use nip29::{GroupCreateOptions, GroupKey, GroupsEvent, GroupsRegistry};
use nostr_sdk::prelude::*;
use settings::AppSettings;
use state::upload;
use theme::{ActiveTheme, TextStyle, Typeset as _, button_radius, control_radius};
use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants};
use ui::dock::{DockArea, DockPlacement, PanelHandle};
use ui::input::{Input, InputEvent, InputState, Textarea, TextareaState};
use ui::switch::Switch;
use ui::{
Disableable, Icon, IconName, Sizable, StyledExt, WindowExtension, divider, h_flex, v_flex,
};
use ui::divider;
use ui::{TextStyle, Typeset as _, button_radius, control_radius};
const DEFAULT_RELAYS: [&str; 2] = ["wss://relay.ditto.net", "wss://chat.wisp.talk"];
const PRIVATE_DESC: &str = "Only members can read group messages";
@@ -257,7 +259,7 @@ impl NewGroup {
let registry = GroupsRegistry::global(cx);
let group = registry.update(cx, |registry, cx| registry.open(key, cx));
ui::dock::add_panel_to(
crate::dock::add_panel_to(
&self.dock,
PanelHandle::new(nip29_ui::init(group, window, cx)),
DockPlacement::Center,
@@ -348,7 +350,7 @@ impl NewGroup {
return v_flex().gap_1p5().child(field_label("Relay", cx)).child(
div()
.text_style(TextStyle::Caption)
.text_color(cx.theme().text_muted)
.text_color(cx.theme().muted_foreground)
.child(format!(
"Created on {} (same relay as the parent group).",
relay_label(parent.relay())
@@ -369,13 +371,13 @@ impl NewGroup {
.text_style(TextStyle::Callout)
.cursor_default()
.bg(if open {
cx.theme().element_active
cx.theme().list_active
} else {
cx.theme().surface_raised
cx.theme().secondary
})
.when(!open, |this| {
this.hover(|this| this.bg(cx.theme().element_hover))
.active(|this| this.bg(cx.theme().element_active))
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;
@@ -385,7 +387,7 @@ impl NewGroup {
.child(
Icon::new(IconName::ChevronDown)
.small()
.text_color(cx.theme().text_muted),
.text_color(cx.theme().muted_foreground),
);
let select = Select::new("relay-select")
@@ -412,11 +414,15 @@ impl NewGroup {
.on_mouse_down_out(cx.listener(|this, _, _, cx| {
this.set_relay_select_open(false, cx);
}))
.popover_style(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().text)
.text_color(cx.theme().foreground)
.children(self.relays.iter().enumerate().map(|(index, relay)| {
h_flex()
.id(SharedString::from(format!("option-{relay}")))
@@ -425,7 +431,7 @@ impl NewGroup {
.rounded(px(button_radius()))
.text_style(TextStyle::Callout)
.cursor_default()
.hover(|this| this.bg(cx.theme().element_hover))
.hover(|this| this.bg(cx.theme().primary))
.child(relay_label(relay))
.on_click(cx.listener(move |this, _, _, cx| {
this.activate_relay_option(index, cx);
@@ -441,7 +447,7 @@ impl NewGroup {
.text_style(TextStyle::Callout)
.cursor_default()
.child("Custom relay")
.hover(|this| this.bg(cx.theme().element_hover))
.hover(|this| this.bg(cx.theme().primary))
.on_click(cx.listener(|this, _, _, cx| {
this.activate_relay_option(this.relays.len(), cx);
})),
@@ -457,41 +463,44 @@ impl NewGroup {
}
fn render_access(&self, cx: &mut Context<Self>) -> impl IntoElement {
let private = Switch::new("private")
.label("Private")
.description(PRIVATE_DESC)
.checked(self.private)
.on_click(cx.listener(|this, checked: &bool, _window, cx| {
this.private = *checked;
cx.notify();
}));
let private = field().label("Private").description(PRIVATE_DESC).child(
Switch::new("private")
.checked(self.private)
.on_click(cx.listener(|this, checked: &bool, _window, cx| {
this.private = *checked;
cx.notify();
})),
);
let closed = Switch::new("closed")
.label("Closed")
.description(CLOSED_DESC)
.checked(self.closed)
.on_click(cx.listener(|this, checked: &bool, _window, cx| {
this.closed = *checked;
cx.notify();
}));
let closed = field().label("Closed").description(CLOSED_DESC).child(
Switch::new("closed")
.checked(self.closed)
.on_click(cx.listener(|this, checked: &bool, _window, cx| {
this.closed = *checked;
cx.notify();
})),
);
let restricted = Switch::new("restricted")
let restricted = field()
.label("Restricted")
.description(RESTRICTED_DESC)
.checked(self.restricted)
.on_click(cx.listener(|this, checked: &bool, _window, cx| {
this.restricted = *checked;
cx.notify();
}));
.child(
Switch::new("restricted")
.checked(self.restricted)
.on_click(cx.listener(|this, checked: &bool, _window, cx| {
this.restricted = *checked;
cx.notify();
})),
);
let hidden = Switch::new("hidden")
.label("Hidden")
.description(HIDDEN_DESC)
.checked(self.hidden)
.on_click(cx.listener(|this, checked: &bool, _window, cx| {
this.hidden = *checked;
cx.notify();
}));
let hidden = field().label("Hidden").description(HIDDEN_DESC).child(
Switch::new("hidden")
.checked(self.hidden)
.on_click(cx.listener(|this, checked: &bool, _window, cx| {
this.hidden = *checked;
cx.notify();
})),
);
v_flex()
.gap_2()
@@ -503,14 +512,17 @@ impl NewGroup {
}
fn render_list_setting(&self, cx: &mut Context<Self>) -> impl IntoElement {
let list_private = Switch::new("list-private")
let list_private = field()
.label("Private on my list")
.description(LIST_PRIVATE_DESC)
.checked(self.list_private)
.on_click(cx.listener(|this, checked: &bool, _window, cx| {
this.list_private = *checked;
cx.notify();
}));
.child(
Switch::new("list-private")
.checked(self.list_private)
.on_click(cx.listener(|this, checked: &bool, _window, cx| {
this.list_private = *checked;
cx.notify();
})),
);
v_flex()
.gap_2()
@@ -560,14 +572,6 @@ impl NewGroup {
impl Render for NewGroup {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let name = self.name_input.read(cx).value().trim().to_owned();
let seed: String = if name.is_empty() {
"group".to_owned()
} else {
name
};
let selected = self
.selected_relay
.as_ref()
@@ -593,7 +597,12 @@ impl Render for NewGroup {
h_flex()
.items_center()
.gap_3()
.child(Avatar::new(self.picture.clone()).seed(seed).large())
.child(match self.picture.clone() {
Some(picture) => {
Avatar::new().src(picture).large().into_any_element()
}
None => Avatar::new().large().into_any_element(),
})
.child(
Button::new("upload-picture")
.label(if self.picture.is_some() {
@@ -610,9 +619,9 @@ impl Render for NewGroup {
})),
),
)
.child(field("Name", Input::new(&self.name_input), cx))
.child(input_field("Name", Input::new(&self.name_input), cx))
.child(self.render_relay(&selected, cx))
.child(field("About", Textarea::new(&self.about_input), cx))
.child(input_field("About", Textarea::new(&self.about_input), cx))
.child(self.render_access(cx))
.child(self.render_list_setting(cx))
.overflow_y_scroll(),
@@ -635,7 +644,7 @@ impl Render for NewGroup {
"Create Group"
})
.primary()
.font_semibold()
.font_weight(gpui::FontWeight::SEMIBOLD)
.loading(self.creating)
.disabled(self.creating || self.uploading)
.on_click(cx.listener(|this, _event, _window, cx| this.create(cx))),
@@ -644,15 +653,15 @@ impl Render for NewGroup {
}
}
fn field(text: &'static str, input: impl IntoElement, cx: &App) -> Div {
fn input_field(text: &'static str, input: impl IntoElement, cx: &App) -> Div {
v_flex().gap_1p5().child(field_label(text, cx)).child(input)
}
fn field_label(text: &'static str, cx: &App) -> Div {
div()
.text_style(TextStyle::Caption)
.text_color(cx.theme().text_muted)
.font_semibold()
.text_color(cx.theme().muted_foreground)
.font_weight(gpui::FontWeight::SEMIBOLD)
.child(text)
}
@@ -661,7 +670,7 @@ fn switch_row(switch: impl IntoElement, cx: &App) -> Div {
.w_full()
.p_2()
.rounded(px(control_radius()))
.bg(cx.theme().surface_raised)
.bg(cx.theme().secondary)
.child(switch)
}
+25 -26
View File
@@ -1,3 +1,10 @@
use gpui_kit::assets::IconName;
use gpui_kit::component::avatar::Avatar;
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::dock::DockPlacement;
use gpui_kit::component::input::{Input, InputState, Textarea, TextareaState};
use gpui_kit::component::notification::Notification;
use gpui_kit::component::{ActiveTheme, Disableable, Sizable, WindowExt, h_flex, v_flex};
use std::str::FromStr;
use anyhow::Error;
@@ -12,13 +19,7 @@ use nostr_sdk::prelude::*;
use person::{Directory, Person, PersonRegistry, shorten_pubkey};
use settings::AppSettings;
use state::{NostrRegistry, upload};
use theme::{ActiveTheme, TextStyle, Typeset as _, button_radius};
use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants, IconPosition};
use ui::dock::DockPlacement;
use ui::input::{Input, InputState, Textarea, TextareaState};
use ui::notification::Notification;
use ui::{Disableable, IconName, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
use ui::{TextStyle, Typeset as _, button_radius};
use crate::Workspace;
@@ -372,17 +373,18 @@ impl Render for ProfileDialog {
.w_full()
.items_center()
.justify_center()
.bg(cx.theme().surface)
.child(Avatar::new(avatar).seed(profile.avatar_seed()).large())
.bg(cx.theme().sidebar)
.child(match avatar {
Some(picture) => Avatar::new().src(picture).large().into_any_element(),
None => Avatar::new().large().into_any_element(),
})
.when(self.editing, |this| {
this.child(
Button::new("upload")
.icon(IconName::ArrowRight)
.icon_position(IconPosition::End)
.label("Add an avatar")
.xsmall()
.ghost()
.rounded()
.disabled(self.uploading)
.loading(self.uploading)
.on_click(cx.listener(move |this, _, window, cx| {
@@ -399,14 +401,14 @@ impl Render for ProfileDialog {
.justify_center()
.gap_3()
.rounded(px(button_radius()))
.bg(cx.theme().surface)
.bg(cx.theme().sidebar)
.text_style(TextStyle::Callout)
.child(shorten_pkey)
.child(
Button::new("copy")
.icon({
if self.copied {
IconName::CheckCircle
IconName::CircleCheck
} else {
IconName::Copy
}
@@ -485,11 +487,10 @@ impl Render for ProfileDialog {
.when(self.is_current_user && !self.editing, |this| {
this.child(
Button::new("update")
.icon(IconName::Edit)
.icon_position(IconPosition::End)
.icon(IconName::SquarePen)
.label("Update")
.primary()
.font_semibold()
.font_weight(gpui::FontWeight::SEMIBOLD)
.on_click(cx.listener(move |this, _ev, window, cx| {
this.toggle_edit(window, cx);
})),
@@ -498,11 +499,10 @@ impl Render for ProfileDialog {
.when(!self.is_current_user, |this| {
this.child(
Button::new("message")
.icon(IconName::Message)
.icon_position(IconPosition::End)
.icon(IconName::MessageSquare)
.label("Message")
.primary()
.font_semibold()
.font_weight(gpui::FontWeight::SEMIBOLD)
.on_click(cx.listener(move |this, _ev, window, cx| {
this.message(window, cx);
})),
@@ -520,11 +520,10 @@ impl Render for ProfileDialog {
)
.child(
Button::new("save")
.icon(IconName::CheckCircle)
.icon_position(IconPosition::End)
.icon(IconName::CircleCheck)
.label("Save")
.primary()
.font_semibold()
.font_weight(gpui::FontWeight::SEMIBOLD)
.loading(self.updating)
.disabled(self.updating)
.on_click(cx.listener(move |this, _ev, window, cx| {
@@ -539,8 +538,8 @@ impl Render for ProfileDialog {
fn label(label: &'static str, cx: &App) -> Div {
div()
.text_style(TextStyle::Caption)
.text_color(cx.theme().text_muted)
.font_semibold()
.text_color(cx.theme().muted_foreground)
.font_weight(gpui::FontWeight::SEMIBOLD)
.child(label)
}
@@ -551,8 +550,8 @@ fn field(
cx: &App,
) -> Div {
let (content, color) = match value {
Some(value) => (value, cx.theme().text),
None => (SharedString::from(placeholder), cx.theme().text_muted),
Some(value) => (value, cx.theme().foreground),
None => (SharedString::from(placeholder), cx.theme().muted_foreground),
};
v_flex().gap_1p5().child(label(text, cx)).child(
+16 -17
View File
@@ -1,3 +1,10 @@
use gpui_kit::assets::IconName;
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::scroll::ScrollableElement;
use gpui_kit::component::tab::{Tab, TabBar};
use gpui_kit::component::{ActiveTheme, Disableable, Sizable, WindowExt, h_flex, v_flex};
use std::collections::HashSet;
use anyhow::{Error, anyhow};
@@ -11,14 +18,7 @@ use instant::Duration;
use nostr_sdk::prelude::*;
use smallvec::{SmallVec, smallvec};
use state::NostrRegistry;
use theme::{ActiveTheme, TextStyle, Typeset as _, button_radius, panel_radius};
use ui::button::{Button, ButtonVariants, IconPosition};
use ui::input::{Input, InputEvent, InputState};
use ui::menu::{DropdownMenu, PopupMenuItem};
use ui::scroll::ScrollableElement;
use ui::tab::Tab;
use ui::tab::tab_bar::TabBar;
use ui::{Disableable, IconName, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
use ui::{TextStyle, Typeset as _, button_radius, panel_radius};
const GOSSIP_MSG: &str = "Gossip Relays are the relays where you publish all your events and discover events from others.";
const INBOX_MSG: &str = "Messaging Relays host your private messages. Others find these relays and send you messages there.";
@@ -108,11 +108,11 @@ impl RelayManager {
let manager = cx.entity().downgrade();
v_flex()
.gap(px(theme::SPACE))
.gap(px(ui::SPACE))
.child(
div()
.text_style(TextStyle::Caption)
.text_color(cx.theme().text_muted)
.text_color(cx.theme().muted_foreground)
.child(SharedString::from(GOSSIP_MSG)),
)
.child(
@@ -197,11 +197,11 @@ impl RelayManager {
fn render_inbox(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.gap(px(theme::SPACE))
.gap(px(ui::SPACE))
.child(
div()
.text_style(TextStyle::Caption)
.text_color(cx.theme().text_muted)
.text_color(cx.theme().muted_foreground)
.child(SharedString::from(INBOX_MSG)),
)
.child(
@@ -264,7 +264,7 @@ impl RelayManager {
fn render_tab_bar(&self, cx: &mut Context<Self>) -> impl IntoElement {
TabBar::new("relays-tabs")
.segmented(true)
.segmented()
.selected_index(self.active_tab)
.on_click(cx.listener(|this, ix: &usize, _window, cx| {
this.active_tab = *ix;
@@ -284,10 +284,9 @@ impl RelayManager {
h_flex().gap_1().justify_end().child(
Button::new("update")
.icon(IconName::ArrowRight)
.icon_position(IconPosition::End)
.label("Update")
.primary()
.font_semibold()
.font_weight(gpui::FontWeight::SEMIBOLD)
.loading(updating)
.disabled(updating)
.on_click(cx.listener(move |this, _ev, window, cx| {
@@ -648,7 +647,7 @@ fn relay_row(
.px_2()
.justify_between()
.rounded(px(button_radius()))
.bg(cx.theme().surface)
.bg(cx.theme().sidebar)
.child(
h_flex()
.gap_1()
@@ -666,7 +665,7 @@ fn relay_row(
)
.child(
Button::new("remove")
.icon(IconName::Close)
.icon(IconName::X)
.xsmall()
.ghost()
.invisible()
+7 -7
View File
@@ -4,13 +4,14 @@ use gpui::{
AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled,
Subscription, Task, Window, div,
};
use gpui_kit::assets::IconName;
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::input::{Input, InputEvent, InputState};
use gpui_kit::component::{ActiveTheme, WindowExt, h_flex, v_flex};
use instant::Duration;
use nip4e::DeviceRegistry;
use nostr_sdk::prelude::*;
use theme::{ActiveTheme, TextStyle, Typeset as _};
use ui::button::{Button, ButtonVariants, IconPosition};
use ui::input::{Input, InputEvent, InputState};
use ui::{IconName, StyledExt, WindowExtension, h_flex, v_flex};
use ui::{TextStyle, Typeset as _};
#[derive(Debug)]
pub struct RestoreEncryption {
@@ -105,7 +106,7 @@ impl Render for RestoreEncryption {
v_flex()
.gap_1()
.text_style(TextStyle::Callout)
.text_color(cx.theme().text_muted)
.text_color(cx.theme().muted_foreground)
.child("Secret Key")
.child(Input::new(&self.key_input)),
)
@@ -113,10 +114,9 @@ impl Render for RestoreEncryption {
h_flex().gap_1().justify_end().child(
Button::new("restore")
.icon(IconName::ArrowRight)
.icon_position(IconPosition::End)
.label("Restore")
.primary()
.font_semibold()
.font_weight(gpui::FontWeight::SEMIBOLD)
.on_click(cx.listener(move |this, _, window, cx| {
this.restore(window, cx);
})),
+33 -28
View File
@@ -1,3 +1,8 @@
use gpui_kit::assets::IconName;
use gpui_kit::component::avatar::Avatar;
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::spinner::Spinner;
use gpui_kit::component::{ActiveTheme, Disableable, Icon, Sizable, WindowExt, h_flex, v_flex};
use std::collections::HashMap;
use anyhow::Error;
@@ -12,11 +17,7 @@ use nostr_sdk::prelude::*;
use person::{Directory, Person, PersonRegistry, shorten_pubkey};
use smallvec::{SmallVec, smallvec};
use state::{BOOTSTRAP_RELAYS, NostrAddress, NostrRegistry, TIMEOUT};
use theme::{ActiveTheme, TextStyle, Typeset as _, button_radius};
use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants};
use ui::indicator::Indicator;
use ui::{Disableable, Icon, IconName, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
use ui::{TextStyle, Typeset as _, button_radius};
pub fn init(public_key: PublicKey, window: &mut Window, cx: &mut App) -> Entity<Screening> {
cx.new(|cx| Screening::new(public_key, window, cx))
@@ -312,8 +313,13 @@ impl Screening {
.gap_1p5()
.rounded(px(button_radius()))
.text_style(TextStyle::Callout)
.hover(|this| this.bg(cx.theme().element_hover))
.child(Avatar::new(profile.avatar()).small())
.hover(|this| this.bg(cx.theme().primary))
.child(match profile.avatar() {
Some(picture) => {
Avatar::new().src(picture).small().into_any_element()
}
None => Avatar::new().small().into_any_element(),
})
.child(profile.name()),
);
}
@@ -365,10 +371,13 @@ impl Render for Screening {
.items_center()
.justify_center()
.text_center()
.child(Avatar::new(profile.avatar()).large())
.child(match profile.avatar() {
Some(picture) => Avatar::new().src(picture).large().into_any_element(),
None => Avatar::new().large().into_any_element(),
})
.child(
div()
.font_semibold()
.font_weight(gpui::FontWeight::SEMIBOLD)
.line_height(relative(1.25))
.child(profile.name()),
),
@@ -383,7 +392,7 @@ impl Render for Screening {
.h_7()
.justify_center()
.rounded_full()
.bg(cx.theme().surface_raised)
.bg(cx.theme().secondary)
.text_style(TextStyle::Callout)
.truncate()
.text_ellipsis()
@@ -398,9 +407,8 @@ impl Render for Screening {
Button::new("njump")
.icon(IconName::Link)
.label("njump.me")
.ghost_alt()
.ghost()
.small()
.rounded()
.on_click(cx.listener(move |this, _e, window, cx| {
this.open_njump(window, cx);
})),
@@ -408,9 +416,8 @@ impl Render for Screening {
.child(
Button::new("report")
.tooltip("Report as a scam or impostor")
.icon(IconName::Warning)
.icon(IconName::TriangleAlert)
.warning()
.rounded()
.on_click(cx.listener(move |this, _e, window, cx| {
this.report(window, cx);
})),
@@ -423,7 +430,7 @@ impl Render for Screening {
.child(
h_flex()
.items_start()
.gap(px(theme::SPACE))
.gap(px(ui::SPACE))
.text_style(TextStyle::Callout)
.child(status_badge(Some(self.followed), cx))
.child(
@@ -433,7 +440,7 @@ impl Render for Screening {
.child(
div()
.line_clamp(1)
.text_color(cx.theme().text_muted)
.text_color(cx.theme().muted_foreground)
.child({
if self.followed {
SharedString::from(CONTACT)
@@ -447,7 +454,7 @@ impl Render for Screening {
.child(
h_flex()
.items_start()
.gap(px(theme::SPACE))
.gap(px(ui::SPACE))
.text_style(TextStyle::Callout)
.child(status_badge(last_active, cx))
.child(
@@ -462,7 +469,6 @@ impl Render for Screening {
.icon(IconName::Info)
.xsmall()
.ghost()
.rounded()
.tooltip(RELAY_INFO),
),
)
@@ -470,7 +476,7 @@ impl Render for Screening {
div()
.w_full()
.line_clamp(1)
.text_color(cx.theme().text_muted)
.text_color(cx.theme().muted_foreground)
.map(|this| {
if let Some(t) = self.last_active {
this.child(SharedString::from(format!(
@@ -487,7 +493,7 @@ impl Render for Screening {
.child(
h_flex()
.items_start()
.gap(px(theme::SPACE))
.gap(px(ui::SPACE))
.child(status_badge(Some(self.verified), cx))
.child(
v_flex()
@@ -504,7 +510,7 @@ impl Render for Screening {
.child(
div()
.line_clamp(1)
.text_color(cx.theme().text_muted)
.text_color(cx.theme().muted_foreground)
.child({
if self.address(cx).is_some() {
if self.verified {
@@ -522,7 +528,7 @@ impl Render for Screening {
.child(
h_flex()
.items_start()
.gap(px(theme::SPACE))
.gap(px(ui::SPACE))
.child(status_badge(Some(mutuals > 0), cx))
.child(
h_flex()
@@ -530,7 +536,7 @@ impl Render for Screening {
.child(
div()
.line_clamp(1)
.text_color(cx.theme().text_muted)
.text_color(cx.theme().muted_foreground)
.child({
if mutuals > 0 {
SharedString::from(mutuals_str)
@@ -544,7 +550,6 @@ impl Render for Screening {
.icon(IconName::Info)
.xsmall()
.ghost()
.rounded()
.disabled(mutuals == 0)
.on_click(cx.listener(move |this, _, window, cx| {
this.mutual_contacts(
@@ -567,15 +572,15 @@ fn status_badge(status: Option<bool>, cx: &App) -> Div {
.flex_shrink_0()
.map(|this| {
if let Some(status) = status {
this.child(Icon::new(IconName::CheckCircle).small().text_color({
this.child(Icon::new(IconName::CircleCheck).small().text_color({
if status {
cx.theme().accent
cx.theme().primary
} else {
cx.theme().text_faint
cx.theme().muted_foreground
}
}))
} else {
this.child(Indicator::new().small())
this.child(Spinner::new().small())
}
})
}
+54 -49
View File
@@ -3,15 +3,18 @@ use gpui::{
App, AppContext, Context, Entity, IntoElement, ParentElement, PathPromptOptions, Render,
SharedString, Styled, Window, div,
};
use gpui_kit::assets::IconName;
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::input::{Input, InputState};
use gpui_kit::component::menu::{DropdownMenu, PopupMenuItem};
use gpui_kit::component::notification::Notification;
use gpui_kit::component::switch::Switch;
use gpui_kit::component::{ActiveTheme, Disableable, Sizable, WindowExt, h_flex, v_flex};
use settings::AppSettings;
use theme::{ActiveTheme, AppExt as _, AppearanceMode, TextStyle, Typeset as _};
use ui::button::{Button, ButtonVariants};
use ui::group_box::{GroupBox, GroupBoxVariants};
use ui::input::{Input, InputState};
use ui::menu::{DropdownMenu, PopupMenuItem};
use ui::notification::Notification;
use ui::switch::Switch;
use ui::{Disableable, IconName, Sizable, WindowExtension, h_flex, v_flex};
use settings::AppearanceMode;
use ui::{TextStyle, Typeset as _};
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Preferences> {
cx.new(|cx| Preferences::new(window, cx))
@@ -50,7 +53,9 @@ impl Preferences {
/// Set the appearance mode (system, light or dark)
fn set_appearance_mode(mode: AppearanceMode, cx: &mut App) {
AppSettings::update_appearance_mode(mode, cx);
cx.set_appearance_mode(mode);
AppSettings::global(cx).update(cx, |settings, cx| {
settings.apply_theme(cx);
});
}
/// Ask the user for an image to show behind chat and group panels
@@ -109,33 +114,29 @@ impl Render for Preferences {
.id("general")
.title("General")
.fill()
.child(
Switch::new("screening")
.label("Screening")
.description(SCREENING)
.checked(screening)
.on_click(move |_, _window, cx| {
.child(field().label("Screening").description(SCREENING).child(
Switch::new("screening").checked(screening).on_click(
move |_, _window, cx| {
AppSettings::update_screening(!screening, cx);
}),
)
},
),
))
.child(
Switch::new("wot-auto-trust")
.label("Web of Trust")
.description(WOT)
.checked(wot_auto_trust)
.on_click(move |_, _window, cx| {
AppSettings::update_wot_auto_trust(!wot_auto_trust, cx);
}),
field().label("Web of Trust").description(WOT).child(
Switch::new("wot-auto-trust")
.checked(wot_auto_trust)
.on_click(move |_, _window, cx| {
AppSettings::update_wot_auto_trust(!wot_auto_trust, cx);
}),
),
)
.child(
Switch::new("avatar")
.label("Hide user avatar")
.description(AVATAR)
.checked(hide_avatar)
.on_click(move |_, _window, cx| {
.child(field().label("Hide user avatar").description(AVATAR).child(
Switch::new("avatar").checked(hide_avatar).on_click(
move |_, _window, cx| {
AppSettings::update_hide_avatar(!hide_avatar, cx);
}),
),
},
),
)),
)
.child(
GroupBox::new()
@@ -156,14 +157,14 @@ impl Render for Preferences {
.child(
div()
.text_style(TextStyle::Caption)
.text_color(cx.theme().text_muted)
.text_color(cx.theme().muted_foreground)
.child(SharedString::from(MODE)),
),
)
.child(
Button::new("appearance-mode")
.label(appearance_mode.label())
.ghost_alt()
.ghost()
.small()
.dropdown_menu(|this, _window, _cx| {
AppearanceMode::ALL.iter().fold(this, |menu, mode| {
@@ -183,14 +184,17 @@ impl Render for Preferences {
.title("Background")
.fill()
.child(
Switch::new("panel-background")
field()
.label("Panel background")
.description(BACKGROUND)
.checked(panel_background)
.on_click(move |_, _window, cx| {
AppSettings::update_panel_background(!panel_background, cx);
cx.refresh_windows();
}),
.child(
Switch::new("panel-background")
.checked(panel_background)
.on_click(move |_, _window, cx| {
AppSettings::update_panel_background(!panel_background, cx);
cx.refresh_windows();
}),
),
)
.child(
h_flex()
@@ -208,7 +212,7 @@ impl Render for Preferences {
.child(
div()
.text_style(TextStyle::Caption)
.text_color(cx.theme().text_muted)
.text_color(cx.theme().muted_foreground)
.truncate()
.child(SharedString::from(background_label)),
),
@@ -220,7 +224,7 @@ impl Render for Preferences {
.child(
Button::new("choose-background")
.label("Choose image")
.ghost_alt()
.ghost()
.small()
.disabled(!panel_background)
.on_click(|_, _window, cx| {
@@ -230,7 +234,7 @@ impl Render for Preferences {
.child(
Button::new("reset-background")
.label("Reset")
.ghost_alt()
.ghost()
.small()
.disabled(
!panel_background
@@ -250,13 +254,14 @@ impl Render for Preferences {
.title("Experiments")
.fill()
.child(
Switch::new("nip4e")
field()
.label("Decoupling Encryption Key")
.description(NIP4E)
.checked(nip4e)
.on_click(move |_, _window, cx| {
AppSettings::update_nip4e(!nip4e, cx);
}),
.child(Switch::new("nip4e").checked(nip4e).on_click(
move |_, _window, cx| {
AppSettings::update_nip4e(!nip4e, cx);
},
)),
),
)
.child(
@@ -285,7 +290,7 @@ impl Render for Preferences {
div()
.text_style(TextStyle::Caption2)
.italic()
.text_color(cx.theme().text_faint)
.text_color(cx.theme().muted_foreground)
.child(SharedString::from("Only support blossom service")),
),
),
+78
View File
@@ -0,0 +1,78 @@
use std::sync::Arc;
use gpui::{App, WeakEntity, Window};
use gpui_kit::component::dock::{
DockArea, DockPlacement, InsertTarget, NodeId, PaneRef, PanelHandle, PanelId,
};
/// The panel in any region of `area` whose id is `key`.
fn find_panel(area: &DockArea, key: PanelId, _cx: &App) -> Option<(PanelId, NodeId, usize)> {
let placements = [
DockPlacement::Center,
DockPlacement::Left,
DockPlacement::Right,
DockPlacement::Bottom,
];
for placement in placements {
let Some(tree) = area.layout(placement) else {
continue;
};
for id in tree.panels() {
if id != key {
continue;
}
let node = tree.find_panel_node(id)?;
let PaneRef::Tabs { panels, .. } = tree.find_node(node)?.kind() else {
continue;
};
let ix = panels.iter().position(|panel| *panel == key)?;
return Some((id, node, ix));
}
}
None
}
/// Add `panel` to the area, or move it to the end of its existing tab group.
pub fn add_panel(
area: &mut DockArea,
panel: PanelHandle,
placement: DockPlacement,
window: &mut Window,
cx: &mut gpui::Context<DockArea>,
) {
let key = panel.panel_id(cx);
if let Some((id, node, ix)) = find_panel(area, key, cx) {
area.move_panel(
id,
InsertTarget::Tabs {
node,
ix: Some(ix),
activate: true,
},
window,
cx,
);
return;
}
area.add_panel_view(Arc::new(panel), placement, None, window, cx);
}
/// Add a panel to a dock reached through a weak handle.
pub fn add_panel_to(
dock: &WeakEntity<DockArea>,
panel: PanelHandle,
placement: DockPlacement,
window: &mut Window,
cx: &mut App,
) {
if let Err(error) = dock.update(cx, |area, cx| {
add_panel(area, panel, placement, window, cx);
}) {
log::warn!("Failed to add panel to dock: {error}");
}
}
+32 -34
View File
@@ -8,17 +8,17 @@ use gpui::{
Action, App, AppContext, Context, Entity, InteractiveElement, IntoElement, ParentElement,
Render, SharedString, Styled, Subscription, Task, Window, div, px,
};
use gpui_kit::component::button::Button;
use gpui_kit::component::dock::{DockArea, DockLayout, DockPlacement, Panel, PanelHandle};
use gpui_kit::component::notification::{Notification, NotificationType};
use gpui_kit::component::{ActiveTheme, WindowExt, v_flex};
use nip4e::{DeviceEvent, DeviceRegistry};
use nip29::{GroupId, GroupKey};
use nostr_sdk::prelude::*;
use serde::Deserialize;
use smallvec::{SmallVec, smallvec};
use state::{NostrRegistry, StateEvent};
use theme::{ActiveTheme, SIDEBAR_WIDTH, TextStyle, Theme, Typeset as _};
use ui::button::Button;
use ui::dock::{self, DockArea, DockLayout, DockPlacement, Panel, PanelHandle};
use ui::notification::{Notification, NotificationKind};
use ui::{Root, WindowExtension, v_flex};
use ui::{SIDEBAR_WIDTH, TextStyle, Typeset as _};
use crate::dialogs::restore::RestoreEncryption;
use crate::dialogs::{import, join_group, new_chat, new_group, profile, relays, settings};
@@ -26,6 +26,8 @@ use crate::panels::{backup, browse, contact_list, greeter, inbox};
use crate::sidebar::Sidebar;
mod dialogs;
mod dock;
mod nav;
mod panels;
mod sidebar;
@@ -72,7 +74,8 @@ impl Workspace {
let device = DeviceRegistry::global(cx);
let nostr = NostrRegistry::global(cx);
let dock = dock::dock_area("coop", window, cx);
let (dock, _skin) =
gpui_kit::component::dock::DockSkin::dock_area("coop", None, window, cx);
let sidebar = cx.new(|cx| Sidebar::new(window, dock.downgrade(), cx));
let left_sidebar = sidebar.clone();
@@ -82,7 +85,7 @@ impl Workspace {
subscriptions.push(
// Observe system appearance and update theme
cx.observe_window_appearance(window, |_this, window, cx| {
Theme::sync_system_appearance(Some(window), cx);
gpui_kit::component::Theme::sync_system_appearance(Some(window), cx);
}),
);
@@ -109,7 +112,7 @@ impl Workspace {
.autohide(false)
.title(format!("Wait for approval · {code}"))
.message(MSG)
.with_kind(NotificationKind::Info);
.with_type(NotificationType::Info);
window.push_notification(note, cx);
}
@@ -121,7 +124,7 @@ impl Workspace {
let note = Notification::new()
.id::<DeviceNotifcation>()
.message(MSG)
.with_kind(NotificationKind::Info)
.with_type(NotificationType::Info)
.action(|_this, _window, _cx| {
Button::new("retry").label("Retry").on_click(
move |_this, window, cx| {
@@ -129,7 +132,7 @@ impl Workspace {
device.update(cx, |this, cx| {
this.get_announcement(cx);
});
window.clear_notification::<DeviceNotifcation>(cx);
window.remove_notification::<DeviceNotifcation>(cx);
},
)
});
@@ -140,7 +143,7 @@ impl Workspace {
let note = Notification::new()
.id::<DeviceNotifcation>()
.message("Encryption Key has been set")
.with_kind(NotificationKind::Success);
.with_type(NotificationType::Success);
window.push_notification(note, cx);
}
@@ -169,7 +172,7 @@ impl Workspace {
chat.update(cx, |this, cx| {
this.get_metadata(cx);
});
window.clear_notification::<MsgRelayNotification>(cx);
window.remove_notification::<MsgRelayNotification>(cx);
},
)
}),
@@ -230,20 +233,20 @@ impl Workspace {
}
}
/// Add a panel to the dock, from anywhere that has the window but not the workspace.
/// Add a panel to the workspace dock from anywhere that has the window.
/// The workspace is the window's root view, so it is reachable directly.
pub fn add_panel<P: Panel>(
panel: Entity<P>,
placement: DockPlacement,
window: &mut Window,
cx: &mut App,
) {
if let Some(root) = window.root::<Root>().flatten()
&& let Ok(workspace) = root.read(cx).view().clone().downcast::<Self>()
{
workspace.update(cx, |this, cx| {
this.add_panel_to_dock(panel, placement, window, cx)
});
}
let Some(root) = window.root::<Self>().flatten() else {
return;
};
root.update(cx, |this, cx| {
this.add_panel_to_dock(panel, placement, window, cx)
});
}
/// Add a panel to the dock, or focus it if it is already docked.
@@ -255,7 +258,7 @@ impl Workspace {
cx: &mut Context<Self>,
) {
self.dock.update(cx, |area, cx| {
ui::dock::add_panel(area, PanelHandle::new(panel), placement, window, cx)
crate::dock::add_panel(area, PanelHandle::new(panel), placement, window, cx)
});
}
@@ -267,7 +270,7 @@ impl Workspace {
window.open_dialog(cx, move |this, _window, _cx| {
this.width(px(520.))
.show_close(true)
.close_button(true)
.pb_2()
.title("Preferences")
.child(view.clone())
@@ -426,13 +429,13 @@ impl Workspace {
let device = DeviceRegistry::global(cx);
let ent = device.downgrade();
window.open_dialog(cx, move |this, _window, cx| {
window.open_alert_dialog(cx, move |alert, _window, cx| {
let ent = ent.clone();
this.confirm()
.show_close(true)
alert
.confirm()
.title("Reset Encryption Key")
.child(
.description(
v_flex()
.gap_1()
.text_style(TextStyle::Callout)
@@ -466,10 +469,9 @@ impl Workspace {
}
impl Render for Workspace {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let dialog_layer = Root::render_dialog_layer(window, cx);
let notification_layer = Root::render_notification_layer(window, cx);
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
// Dialog, notification and tooltip layers are owned by the framework
// Root that gpui_kit::open_window installed for this window.
div()
.id("workspace")
.on_action(cx.listener(Self::on_command))
@@ -477,9 +479,5 @@ impl Render for Workspace {
.relative()
.size_full()
.child(self.dock.clone())
// Notifications
.children(notification_layer)
// Dialogs
.children(dialog_layer)
}
}
+142
View File
@@ -0,0 +1,142 @@
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, px,
};
use gpui_kit::component::{ActiveTheme, Selectable, StyledExt as _, h_flex, v_flex};
use ui::button_radius;
/// 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,
#[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,
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
}
/// 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;
let text_color = if self.selected {
cx.theme().foreground
} else {
cx.theme().muted_foreground
};
v_flex().h_10().w_full().justify_center().child(
h_flex()
.id(self.id)
.h_9()
.w_full()
.px_1p5()
.gap_1p5()
.rounded(px(button_radius()))
.text_color(text_color)
.when_some(self.prefix, |this, prefix| this.child(prefix))
.child(
h_flex()
.gap_1()
.flex_1()
.min_w_0()
.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().accent))
.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),
)
}
}
+23 -19
View File
@@ -1,16 +1,18 @@
use anyhow::Error;
use gpui::{
AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Task, Window, div, px,
App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle, Focusable,
IntoElement, ParentElement, Render, SharedString, Styled, Task, Window, div, px,
};
use gpui_kit::assets::IconName;
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::dock::{Panel, PanelEvent};
use gpui_kit::component::input::{Input, InputState};
use gpui_kit::component::{ActiveTheme, Sizable, v_flex};
use instant::Duration;
use nostr_sdk::prelude::*;
use state::USER_KEYRING;
use theme::{ActiveTheme, TextStyle, Typeset as _};
use ui::button::{Button, ButtonVariants};
use ui::dock::{Panel, PanelEvent};
use ui::input::{Input, InputState};
use ui::{IconName, Sizable, StyledExt, divider, v_flex};
use ui::divider;
use ui::{TextStyle, Typeset as _};
const MSG: &str = "Store your account keys in a safe location. \
You can restore your account or move to another client anytime you want.";
@@ -106,13 +108,15 @@ impl BackupPanel {
}
}
impl Panel for BackupPanel {
fn panel_id(&self) -> SharedString {
self.name.clone()
impl gpui_kit::component::dock::BasePanel for BackupPanel {
fn panel_name(&self) -> &'static str {
"Backup"
}
}
fn title(&self, _cx: &App) -> AnyElement {
self.name.clone().into_any_element()
impl Panel for BackupPanel {
fn title(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
self.name.clone()
}
}
@@ -133,13 +137,13 @@ impl Render for BackupPanel {
.child(
div()
.text_style(TextStyle::Caption)
.text_color(cx.theme().text_muted)
.text_color(cx.theme().muted_foreground)
.child(SharedString::from(MSG)),
)
.child(divider(cx))
.child(
v_flex()
.gap(px(theme::SPACE))
.gap(px(ui::SPACE))
.flex_1()
.w_full()
.text_style(TextStyle::Callout)
@@ -150,8 +154,8 @@ impl Render for BackupPanel {
.child(
div()
.text_style(TextStyle::Caption)
.font_semibold()
.text_color(cx.theme().text_muted)
.font_weight(gpui::FontWeight::SEMIBOLD)
.text_color(cx.theme().muted_foreground)
.child(SharedString::from("Public Key:")),
)
.child(Input::new(&self.npub_input).small().disabled(true)),
@@ -163,8 +167,8 @@ impl Render for BackupPanel {
.child(
div()
.text_style(TextStyle::Caption)
.font_semibold()
.text_color(cx.theme().text_muted)
.font_weight(gpui::FontWeight::SEMIBOLD)
.text_color(cx.theme().muted_foreground)
.child(SharedString::from("Secret Key:")),
)
.child(Input::new(&self.nsec_input).small().disabled(true)),
@@ -181,7 +185,7 @@ impl Render for BackupPanel {
})
.primary()
.small()
.font_semibold()
.font_weight(gpui::FontWeight::SEMIBOLD)
.on_click(cx.listener(move |this, _ev, _window, cx| {
this.copy_secret(cx);
})),
+41 -35
View File
@@ -1,3 +1,11 @@
use gpui_kit::assets::IconName;
use gpui_kit::component::avatar::Avatar;
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::dock::{DockArea, DockPlacement, Panel, PanelEvent, PanelHandle};
use gpui_kit::component::input::{Input, InputEvent, InputState};
use gpui_kit::component::notification::Notification;
use gpui_kit::component::scroll::Scrollbar;
use gpui_kit::component::{ActiveTheme, Icon, Sizable, WindowExt, h_flex, v_flex};
use std::ops::Range;
use std::rc::Rc;
@@ -13,14 +21,7 @@ use nip29::{Discovery, GroupCandidate, GroupKey, GroupsEvent, GroupsRegistry, Me
use nostr_sdk::prelude::{RelayUrl, Timestamp};
use settings::AppSettings;
use smallvec::{SmallVec, smallvec};
use theme::{ActiveTheme, TextStyle, Typeset as _, button_radius, panel_radius};
use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants};
use ui::dock::{DockArea, DockPlacement, Panel, PanelEvent, PanelHandle};
use ui::input::{Input, InputEvent, InputState};
use ui::notification::Notification;
use ui::scroll::Scrollbar;
use ui::{Icon, IconName, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
use ui::{TextStyle, Typeset as _, button_radius, panel_radius};
const TITLE: &str = "Browse";
const INPUT_PLACEHOLDER: &str = "wss://relay.example.com";
@@ -228,7 +229,7 @@ impl BrowsePanel {
let registry = GroupsRegistry::global(cx);
let group = registry.update(cx, |registry, cx| registry.open(key, cx));
ui::dock::add_panel_to(
crate::dock::add_panel_to(
dock,
PanelHandle::new(nip29_ui::init(group, window, cx)),
DockPlacement::Center,
@@ -269,7 +270,7 @@ impl BrowsePanel {
.child(
div()
.text_style(TextStyle::Callout)
.font_semibold()
.font_weight(gpui::FontWeight::SEMIBOLD)
.line_height(relative(1.2))
.child("Public Groups"),
)
@@ -277,7 +278,7 @@ impl BrowsePanel {
div()
.truncate()
.text_style(TextStyle::Caption)
.text_color(cx.theme().text_muted)
.text_color(cx.theme().muted_foreground)
.map(|this| match &self.relay {
Some(relay) => {
this.child(SharedString::from(relay.as_str().to_owned()))
@@ -295,13 +296,8 @@ impl BrowsePanel {
.max_w_64()
.px_1()
.rounded(px(button_radius()))
.bg(cx.theme().surface)
.child(
Input::new(&self.input)
.appearance(false)
.small()
.flex_1(),
)
.bg(cx.theme().sidebar)
.child(Input::new(&self.input).appearance(false).small().flex_1())
.child(
Button::new("relay-browse")
.icon(IconName::ArrowRight)
@@ -313,6 +309,7 @@ impl BrowsePanel {
})),
),
)
.flex_shrink_0()
.into_any_element()
}
@@ -341,8 +338,9 @@ impl BrowsePanel {
.children(pair.first().map(|card| self.render_card(card, cx)))
.child(match pair.get(1) {
Some(card) => self.render_card(card, cx),
None => div().flex_1().into_any_element(),
None => div().flex_1().flex_shrink_0().into_any_element(),
})
.flex_shrink_0()
.into_any_element()
}
})
@@ -378,24 +376,30 @@ impl BrowsePanel {
.cursor_pointer()
.border_1()
.border_color(transparent_black())
.bg(cx.theme().surface)
.bg(cx.theme().sidebar)
.hover(|this| {
this.bg(cx.theme().surface_raised)
.border_color(cx.theme().border_strong)
this.bg(cx.theme().secondary)
.border_color(cx.theme().border)
})
.child(
v_flex()
.flex_1()
.min_h_0()
.p_2()
.gap(px(theme::SPACE))
.gap(px(ui::SPACE))
.child(
h_flex()
.w_full()
.items_center()
.gap(px(theme::SPACE))
.gap(px(ui::SPACE))
.when(!hide_avatar, |this| {
this.child(Avatar::from_source(picture).seed(seed).flex_shrink_0())
this.child(match picture {
Some(picture) => Avatar::new()
.src(picture)
.flex_shrink_0()
.into_any_element(),
None => Avatar::new().flex_shrink_0().into_any_element(),
})
})
.child(
v_flex()
@@ -405,7 +409,7 @@ impl BrowsePanel {
div()
.text_style(TextStyle::Callout)
.truncate()
.font_semibold()
.font_weight(gpui::FontWeight::SEMIBOLD)
.line_height(relative(1.2))
.child(name),
)
@@ -413,7 +417,7 @@ impl BrowsePanel {
div()
.truncate()
.text_style(TextStyle::Caption)
.text_color(cx.theme().text_faint)
.text_color(cx.theme().muted_foreground)
.child(relay),
),
),
@@ -422,7 +426,7 @@ impl BrowsePanel {
div()
.min_w_0()
.text_style(TextStyle::Callout)
.text_color(cx.theme().text_muted)
.text_color(cx.theme().muted_foreground)
.text_ellipsis()
.line_clamp(2)
.map(|this| {
@@ -438,7 +442,7 @@ impl BrowsePanel {
h_flex()
.w_full()
.gap_1p5()
.text_color(cx.theme().text_muted)
.text_color(cx.theme().muted_foreground)
.child(Icon::new(IconName::Book))
.child(
div()
@@ -454,19 +458,21 @@ impl BrowsePanel {
}
}
impl Panel for BrowsePanel {
fn panel_id(&self) -> SharedString {
TITLE.into()
impl gpui_kit::component::dock::BasePanel for BrowsePanel {
fn panel_name(&self) -> &'static str {
"Browse"
}
}
fn title(&self, cx: &App) -> AnyElement {
impl Panel for BrowsePanel {
fn title(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
h_flex()
.gap_1()
.text_style(TextStyle::Caption)
.child(
Icon::new(IconName::Compass)
.small()
.text_color(cx.theme().text_faint),
.text_color(cx.theme().muted_foreground),
)
.child(TITLE)
.into_any_element()
@@ -543,7 +549,7 @@ fn notice(text: &'static str, cx: &App) -> AnyElement {
.size_full()
.justify_center()
.text_style(TextStyle::Callout)
.text_color(cx.theme().text_muted)
.text_color(cx.theme().muted_foreground)
.child(text)
.into_any_element()
}
+32 -28
View File
@@ -1,24 +1,25 @@
use gpui_kit::assets::IconName;
use gpui_kit::component::avatar::Avatar;
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::scroll::ScrollableElement;
use gpui_kit::component::{ActiveTheme, Disableable, Sizable, WindowExt, h_flex, v_flex};
use std::collections::HashSet;
use anyhow::Error;
use gpui::prelude::FluentBuilder;
use gpui::{
AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
Task, TextAlign, Window, div, px, rems, retain_all,
App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement,
IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Task, TextAlign,
Window, div, px, rems, retain_all,
};
use instant::Duration;
use nostr_sdk::prelude::*;
use person::Directory;
use smallvec::{SmallVec, smallvec};
use state::NostrRegistry;
use theme::{ActiveTheme, TextStyle, Typeset as _, button_radius, panel_radius};
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};
use ui::{TextStyle, Typeset as _, button_radius, panel_radius};
pub fn init(window: &mut Window, cx: &mut App) -> Entity<ContactListPanel> {
cx.new(|cx| ContactListPanel::new(window, cx))
@@ -242,21 +243,22 @@ impl ContactListPanel {
.px_2()
.justify_between()
.rounded(px(button_radius()))
.hover(|this| this.bg(cx.theme().element_hover))
.hover(|this| this.bg(cx.theme().primary))
.child(
h_flex()
.gap(px(theme::SPACE))
.gap(px(ui::SPACE))
.text_style(TextStyle::Callout)
.child(
Avatar::new(profile.avatar())
.seed(profile.avatar_seed())
.small(),
)
.child(match profile.avatar() {
Some(picture) => {
Avatar::new().src(picture).small().into_any_element()
}
None => Avatar::new().small().into_any_element(),
})
.child(profile.name()),
)
.child(
Button::new("remove_{ix}")
.icon(IconName::Close)
.icon(IconName::X)
.xsmall()
.ghost()
.invisible()
@@ -288,13 +290,15 @@ impl ContactListPanel {
}
}
impl Panel for ContactListPanel {
fn panel_id(&self) -> SharedString {
self.name.clone()
impl gpui_kit::component::dock::BasePanel for ContactListPanel {
fn panel_name(&self) -> &'static str {
"Contacts"
}
}
fn title(&self, _cx: &App) -> AnyElement {
self.name.clone().into_any_element()
impl Panel for ContactListPanel {
fn title(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
self.name.clone()
}
}
@@ -316,15 +320,15 @@ impl Render for ContactListPanel {
.overflow_y_scrollbar()
.child(
v_flex()
.gap(px(theme::SPACE))
.gap(px(ui::SPACE))
.flex_1()
.w_full()
.text_style(TextStyle::Callout)
.child(
div()
.text_style(TextStyle::Caption)
.font_semibold()
.text_color(cx.theme().text_muted)
.font_weight(gpui::FontWeight::SEMIBOLD)
.text_color(cx.theme().muted_foreground)
.child("New contact:"),
)
.child(
@@ -371,10 +375,10 @@ impl Render for ContactListPanel {
})
.child(
Button::new("submit")
.icon(IconName::CheckCircle)
.icon(IconName::CircleCheck)
.label("Update")
.primary()
.font_semibold()
.font_weight(gpui::FontWeight::SEMIBOLD)
.loading(self.updating)
.disabled(self.updating)
.on_click(cx.listener(move |this, _ev, window, cx| {
+39 -38
View File
@@ -1,3 +1,12 @@
use crate::dock::add_panel_to;
use crate::nav::Nav;
use gpui_kit::assets::IconName;
use gpui_kit::component::avatar::Avatar;
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::dock::{DockArea, DockPlacement, Panel, PanelEvent, PanelHandle};
use gpui_kit::component::group_box::{GroupBox, GroupBoxVariants};
use gpui_kit::component::scroll::ScrollableElement;
use gpui_kit::component::{ActiveTheme, Icon, Sizable, h_flex, v_flex};
use std::time::Duration;
use anyhow::Error;
@@ -13,14 +22,7 @@ use nostr_sdk::prelude::*;
use person::PersonRegistry;
use smallvec::{SmallVec, smallvec};
use state::{NostrRegistry, StateEvent};
use theme::{ActiveTheme, TextStyle, Typeset as _, button_radius};
use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants};
use ui::dock::{DockArea, DockPlacement, Panel, PanelEvent, PanelHandle, add_panel_to};
use ui::group_box::{GroupBox, GroupBoxVariants};
use ui::nav::Nav;
use ui::scroll::ScrollableElement;
use ui::{Icon, IconName, Sizable, StyledExt, h_flex, v_flex};
use ui::{TextStyle, Typeset as _, button_radius};
use super::browse;
use crate::sidebar::open_room;
@@ -44,7 +46,6 @@ pub fn init(dock: WeakEntity<DockArea>, window: &mut Window, cx: &mut App) -> En
}
pub struct GreeterPanel {
name: SharedString,
focus_handle: FocusHandle,
dock: WeakEntity<DockArea>,
@@ -98,7 +99,6 @@ impl GreeterPanel {
});
Self {
name: "Onboarding".into(),
focus_handle: cx.focus_handle(),
dock,
requests: vec![],
@@ -207,7 +207,7 @@ impl GreeterPanel {
.border_color(cx.theme().border)
.rounded(px(button_radius()))
.text_style(TextStyle::Caption)
.text_color(cx.theme().text_faint)
.text_color(cx.theme().muted_foreground)
.child("No chat requests")
.into_any_element(),
]
@@ -220,6 +220,8 @@ impl GreeterPanel {
let dock = dock.clone();
let name = room.read(cx).display_name(cx);
let picture = room.read(cx).display_image(cx);
#[allow(unused_variables)]
#[allow(unused_variables)]
let seed = room.read(cx).display_image_seed(cx);
let created_at = room.read(cx).created_at.to_ago();
let preview = preview.clone();
@@ -233,7 +235,7 @@ impl GreeterPanel {
.gap_2()
.rounded(px(button_radius()))
.cursor_pointer()
.hover(|this| this.bg(cx.theme().element_hover))
.hover(|this| this.bg(cx.theme().primary))
.on_click({
let room = room.clone();
let dock = dock.clone();
@@ -241,12 +243,10 @@ impl GreeterPanel {
open_room(&room, &dock, window, cx);
}
})
.child(
Avatar::from_source(picture)
.seed(seed)
.small()
.flex_shrink_0(),
)
.child(match picture {
Some(picture) => Avatar::new().src(picture).small().into_any_element(),
None => Avatar::new().small().flex_shrink_0().into_any_element(),
})
.child(
div()
.min_w_0()
@@ -260,7 +260,7 @@ impl GreeterPanel {
.min_w_0()
.truncate()
.text_style(TextStyle::Callout)
.text_color(cx.theme().text_muted)
.text_color(cx.theme().muted_foreground)
.italic()
.child(preview),
)
@@ -284,7 +284,7 @@ impl GreeterPanel {
div()
.text_align(gpui::TextAlign::Right)
.text_style(TextStyle::Caption)
.text_color(cx.theme().text_faint)
.text_color(cx.theme().muted_foreground)
.child(created_at),
),
)
@@ -334,6 +334,7 @@ impl GreeterPanel {
let dock = dock.clone();
let name = room.read(cx).display_name(cx);
let picture = room.read(cx).display_image(cx);
#[allow(unused_variables)]
let seed = room.read(cx).display_image_seed(cx);
let created_at = room.read(cx).created_at.to_ago();
let preview = preview.clone();
@@ -347,7 +348,7 @@ impl GreeterPanel {
.gap_2()
.rounded(px(button_radius()))
.cursor_pointer()
.hover(|this| this.bg(cx.theme().element_hover))
.hover(|this| this.bg(cx.theme().primary))
.on_click({
let room = room.clone();
let dock = dock.clone();
@@ -355,12 +356,10 @@ impl GreeterPanel {
open_room(&room, &dock, window, cx);
}
})
.child(
Avatar::from_source(picture)
.seed(seed)
.small()
.flex_shrink_0(),
)
.child(match picture {
Some(picture) => Avatar::new().src(picture).small().into_any_element(),
None => Avatar::new().small().flex_shrink_0().into_any_element(),
})
.child(
div()
.min_w_0()
@@ -374,7 +373,7 @@ impl GreeterPanel {
.min_w_0()
.truncate()
.text_style(TextStyle::Callout)
.text_color(cx.theme().text_muted)
.text_color(cx.theme().muted_foreground)
.italic()
.child(preview),
)
@@ -398,7 +397,7 @@ impl GreeterPanel {
div()
.text_align(gpui::TextAlign::Right)
.text_style(TextStyle::Caption)
.text_color(cx.theme().text_faint)
.text_color(cx.theme().muted_foreground)
.child(created_at),
),
)
@@ -439,9 +438,9 @@ impl GreeterPanel {
.w_full()
.child(
div()
.text_color(cx.theme().text_muted)
.text_color(cx.theme().muted_foreground)
.text_style(TextStyle::Subheadline)
.font_semibold()
.font_weight(gpui::FontWeight::SEMIBOLD)
.child("Find new friends"),
)
.child(
@@ -452,7 +451,7 @@ impl GreeterPanel {
.prefix(
Icon::new(IconName::Compass)
.small()
.text_color(cx.theme().text_faint),
.text_color(cx.theme().muted_foreground),
)
.label("Browse Public Groups")
.text_style(TextStyle::Callout)
@@ -480,12 +479,14 @@ impl GreeterPanel {
}
}
impl Panel for GreeterPanel {
fn panel_id(&self) -> SharedString {
self.name.clone()
impl gpui_kit::component::dock::BasePanel for GreeterPanel {
fn panel_name(&self) -> &'static str {
"Welcome"
}
}
fn title(&self, cx: &App) -> AnyElement {
impl Panel for GreeterPanel {
fn title(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
h_flex()
.gap_1()
.text_style(TextStyle::Caption)
@@ -493,7 +494,7 @@ impl Panel for GreeterPanel {
svg()
.path("brand/coop.svg")
.size_4()
.text_color(cx.theme().text_muted),
.text_color(cx.theme().muted_foreground),
)
.child("Welcome")
.into_any_element()
@@ -520,7 +521,7 @@ impl Render for GreeterPanel {
svg()
.path("brand/coop.svg")
.size_12()
.text_color(cx.theme().text_faint),
.text_color(cx.theme().muted_foreground),
)
})
.when(logged_in, |this| {
+14 -11
View File
@@ -1,10 +1,11 @@
use gpui::{
AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
IntoElement, ParentElement, Render, SharedString, Styled, Window,
App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, IntoElement,
ParentElement, Render, SharedString, Styled, Window,
};
use theme::{ActiveTheme, TextStyle, Typeset as _};
use ui::dock::{Panel, PanelEvent};
use ui::{Icon, IconName, Sizable, h_flex};
use gpui_kit::assets::IconName;
use gpui_kit::component::dock::{Panel, PanelEvent};
use gpui_kit::component::{ActiveTheme, Icon, Sizable, h_flex};
use ui::{TextStyle, Typeset as _};
pub fn init(window: &mut Window, cx: &mut App) -> Entity<InboxPanel> {
cx.new(|cx| InboxPanel::new(window, cx))
@@ -24,18 +25,20 @@ impl InboxPanel {
}
}
impl Panel for InboxPanel {
fn panel_id(&self) -> SharedString {
self.name.clone()
impl gpui_kit::component::dock::BasePanel for InboxPanel {
fn panel_name(&self) -> &'static str {
"Inbox"
}
}
fn title(&self, cx: &App) -> AnyElement {
impl Panel for InboxPanel {
fn title(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
h_flex()
.gap_1p5()
.child(
Icon::new(IconName::Inbox)
.small()
.text_color(cx.theme().text_faint),
.text_color(cx.theme().muted_foreground),
)
.child(self.name.clone())
.into_any_element()
@@ -56,7 +59,7 @@ impl Render for InboxPanel {
.size_full()
.justify_center()
.text_style(TextStyle::Callout)
.text_color(cx.theme().text_muted)
.text_color(cx.theme().muted_foreground)
.child(self.name.clone())
}
}
+44 -31
View File
@@ -3,22 +3,23 @@ use std::rc::Rc;
use gpui::prelude::FluentBuilder;
use gpui::{
AnyElement, ClipboardItem, Context, Entity, IntoElement, ParentElement, SharedString, Styled,
WeakEntity, div, px, relative, uniform_list,
AnyElement, ClipboardItem, Context, Entity, InteractiveElement as _, IntoElement,
ParentElement, SharedString, Styled, WeakEntity, div, px, relative, uniform_list,
};
use gpui_kit::assets::IconName;
use gpui_kit::component::avatar::Avatar;
use gpui_kit::component::menu::{ContextMenuExt as _, PopupMenuItem};
use gpui_kit::component::scroll::Scrollbar;
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 theme::{ActiveTheme, TextStyle, Typeset as _};
use ui::avatar::Avatar;
use ui::menu::{DropdownMenu, PopupMenuItem};
use ui::nav::Nav;
use ui::scroll::Scrollbar;
use ui::{Icon, IconName, Sizable, StyledExt, h_flex, v_flex};
use ui::{TextStyle, Typeset as _};
use super::Sidebar;
use crate::dialogs::profile;
use crate::nav::Nav;
/// A collapsible section of the group view.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -86,7 +87,7 @@ impl Sidebar {
.current_user()
.is_some_and(|me| group.read(cx).admins().contains(&me));
let (name, seed, picture, total_members, rows) = {
let (name, _seed, picture, total_members, rows) = {
let group = group.read(cx);
let admins = group.admins();
@@ -143,7 +144,7 @@ impl Sidebar {
.flex_1()
.min_h_0()
.w_full()
.gap(px(theme::SPACE))
.gap(px(ui::SPACE))
.child(
h_flex()
.px_2()
@@ -154,7 +155,12 @@ impl Sidebar {
.pl_0p5()
.gap_1()
.flex_shrink_1()
.child(Avatar::from_source(picture).seed(seed).small())
.child(match picture {
Some(picture) => {
Avatar::new().src(picture).small().into_any_element()
}
None => Avatar::new().small().into_any_element(),
})
.child(
v_flex()
.flex_1()
@@ -164,13 +170,13 @@ impl Sidebar {
.text_style(TextStyle::Caption)
.truncate()
.line_height(relative(1.2))
.font_semibold()
.font_weight(gpui::FontWeight::SEMIBOLD)
.child(name),
)
.child(
div()
.text_style(TextStyle::Caption2)
.text_color(cx.theme().text_muted)
.text_color(cx.theme().muted_foreground)
.child(total_members),
),
),
@@ -234,18 +240,22 @@ 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().text_faint))
.suffix(
Icon::new(icon)
.small()
.text_color(cx.theme().muted_foreground),
)
.text_style(TextStyle::Caption)
.font_semibold()
.text_color(cx.theme().text_faint)
.font_weight(gpui::FontWeight::SEMIBOLD)
.text_color(cx.theme().muted_foreground)
.on_click(cx.listener(move |this, _event, _window, cx| {
match section {
GroupSection::Admins => this.admins_open = !this.admins_open,
@@ -277,29 +287,31 @@ impl Sidebar {
.current_user()
.is_some_and(|me| me == public_key);
Nav::new(public_key.to_hex())
let nav = Nav::new(public_key.to_hex())
.label(person.name())
.text_style(TextStyle::Callout)
.font_medium()
.font_weight(gpui::FontWeight::MEDIUM)
.clickable(true)
.when(!hide_avatar, |this| {
this.prefix(
Avatar::new(person.avatar())
.seed(person.avatar_seed())
.small()
.flex_shrink_0(),
)
this.prefix(match person.avatar() {
Some(picture) => Avatar::new().src(picture).small().into_any_element(),
None => Avatar::new().small().flex_shrink_0().into_any_element(),
})
})
.when(!roles.is_empty(), |this| {
this.suffix(
div()
.truncate()
.text_style(TextStyle::Caption)
.text_color(cx.theme().text_faint)
.text_color(cx.theme().muted_foreground)
.child(SharedString::from(roles.join(", "))),
)
})
.dropdown_menu(move |menu, _window, _cx| {
});
let row = div()
.id(public_key.to_hex())
.w_full()
.child(nav)
.context_menu(move |menu, _window, _cx| {
let menu = menu
.item(
PopupMenuItem::new("View profile").on_click(move |_, window, cx| {
@@ -338,7 +350,8 @@ impl Sidebar {
nip29_ui::remove_member(group.clone(), public_key, window, cx);
}
}))
})
.into_any_element()
});
row.into_any_element()
}
}
+27 -23
View File
@@ -4,12 +4,14 @@ use gpui::{
AnyElement, App, Context, Div, Entity, InteractiveElement, IntoElement, ParentElement,
Stateful, Styled, div, px,
};
use gpui_kit::assets::IconName;
use gpui_kit::component::avatar::Avatar;
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::menu::DropdownMenu;
use gpui_kit::component::{Disableable, Selectable, Sizable, h_flex};
use nostr_sdk::prelude::*;
use theme::TABBAR_HEIGHT;
use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants};
use ui::menu::DropdownMenu;
use ui::{Disableable, IconName, Selectable, Sizable, TRAFFIC_LIGHT_PADDING, h_flex};
use ui::TABBAR_HEIGHT;
use ui::TRAFFIC_LIGHT_PADDING;
use super::Sidebar;
use crate::Command;
@@ -23,7 +25,6 @@ impl Sidebar {
let updater = AutoUpdater::try_global(cx);
let profile = self.directory.read(cx).person(current_user, cx);
let avatar = profile.avatar();
let avatar_seed = profile.avatar_seed();
h_flex()
.id("sidebar-user")
@@ -31,29 +32,32 @@ impl Sidebar {
.h(TABBAR_HEIGHT)
.flex_shrink_0()
.items_center()
.gap(px(theme::SPACE))
.gap(px(ui::SPACE))
.px_2()
.when(cfg!(target_os = "macos"), |this| {
this.pl(px(TRAFFIC_LIGHT_PADDING))
})
.child(
Button::new("current-user")
.child(
Avatar::new(avatar.clone())
.seed(avatar_seed.clone())
.small(),
)
.child(match avatar.clone() {
Some(picture) => Avatar::new().src(picture).small().into_any_element(),
None => Avatar::new().small().into_any_element(),
})
.small()
.caret()
.dropdown_caret(true)
.compact()
.transparent()
.text()
.dropdown_menu(move |this, _window, _cx| {
this.menu_with_icon(
"Profile",
IconName::Profile,
IconName::UserRound,
Box::new(Command::ShowProfile),
)
.menu_with_icon("Relays", IconName::Relay, Box::new(Command::ShowRelayList))
.menu_with_icon(
"Relays",
IconName::RadioTower,
Box::new(Command::ShowRelayList),
)
.separator()
.menu_with_icon(
"Settings",
@@ -79,7 +83,7 @@ impl Sidebar {
)
.child(
Button::new("sidebar-new")
.icon(IconName::PlusChat)
.icon(IconName::MessageSquarePlus)
.tooltip("New")
.ghost()
.small()
@@ -87,18 +91,18 @@ impl Sidebar {
menu.label("Public Group")
.menu_with_icon(
"New Group",
IconName::Group,
IconName::Users,
Box::new(Command::NewGroup),
)
.menu_with_icon(
"Join Group",
IconName::Door,
IconName::DoorOpen,
Box::new(Command::JoinGroup),
)
.separator()
.menu_with_icon(
"New Chat",
IconName::Message,
IconName::MessageSquare,
Box::new(Command::NewChat),
)
}),
@@ -123,11 +127,11 @@ impl Sidebar {
let staged = updater.read(cx).staged();
h_flex()
.gap(px(theme::SPACE))
.gap(px(ui::SPACE))
.when(!up_to_date, |this| {
this.child(
Button::new("update")
.icon(IconName::ArrowDownCircle)
.icon(IconName::CircleArrowDown)
.tooltip(status)
.small()
.ghost()
@@ -137,7 +141,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()
+9 -9
View File
@@ -4,13 +4,13 @@ use gpui::{
AnyElement, App, AppContext, Context, IntoElement, ParentElement, SharedString, Styled, Task,
Window, div,
};
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::dock::{DockPlacement, PanelHandle};
use gpui_kit::component::notification::Notification;
use gpui_kit::component::{ActiveTheme, WindowExt, h_flex, v_flex};
use nostr_sdk::prelude::*;
use state::NostrRegistry;
use theme::{ActiveTheme, TextStyle, Typeset as _};
use ui::button::{Button, ButtonVariants};
use ui::dock::{DockPlacement, PanelHandle};
use ui::notification::Notification;
use ui::{StyledExt, WindowExtension, h_flex, v_flex};
use ui::{TextStyle, Typeset as _};
use super::Sidebar;
use super::rows::SidebarRow;
@@ -127,7 +127,7 @@ impl Sidebar {
})?;
cx.update(|window, cx| {
ui::dock::add_panel_to(
crate::dock::add_panel_to(
&dock,
PanelHandle::new(chat_ui::init(room.downgrade(), window, cx)),
DockPlacement::Center,
@@ -195,7 +195,7 @@ impl Sidebar {
.px_4()
.border_t_1()
.border_color(cx.theme().border)
.bg(cx.theme().bg)
.bg(cx.theme().background)
.child(
Button::new("create-chat")
.label(if self.selected_contacts.len() > 1 {
@@ -212,11 +212,11 @@ impl Sidebar {
h_flex()
.gap_1()
.text_style(TextStyle::Caption)
.text_color(cx.theme().text_muted)
.text_color(cx.theme().muted_foreground)
.child("Selected")
.child(
div()
.font_semibold()
.font_weight(gpui::FontWeight::SEMIBOLD)
.child(SharedString::from(self.selected_contacts.len().to_string())),
),
)
+25 -15
View File
@@ -1,3 +1,7 @@
use crate::nav::Nav;
use gpui_kit::assets::IconName;
use gpui_kit::component::scroll::Scrollbar;
use gpui_kit::component::{ActiveTheme, Icon, Sizable, h_flex, v_flex};
use std::rc::Rc;
use chat::{ChatRegistry, RoomKind};
@@ -8,10 +12,7 @@ use gpui::{
};
use nip29::GroupsRegistry;
use settings::AppSettings;
use theme::{ActiveTheme, TextStyle, Typeset as _, control_radius};
use ui::nav_item::NavItem;
use ui::scroll::Scrollbar;
use ui::{Icon, IconName, Sizable, StyledExt, h_flex, v_flex};
use ui::{TextStyle, Typeset as _, control_radius};
use super::Sidebar;
use super::rows::SidebarRow;
@@ -61,19 +62,23 @@ impl Sidebar {
.flex_1()
.min_h_0()
.relative()
.gap(px(theme::SPACE))
.gap(px(ui::SPACE))
.child(
v_flex()
.px_2()
.gap_1()
.child(
NavItem::new("browse", "Browse", Icon::new(IconName::Compass).small())
Nav::new("browse")
.prefix(Icon::new(IconName::Compass).small())
.label("Browse")
.on_click(|_, window, cx| {
window.dispatch_action(Box::new(Command::ShowBrowse), cx)
}),
)
.child(
NavItem::new("reqs", "Requests", Icon::new(IconName::Invite).small())
Nav::new("reqs")
.prefix(Icon::new(IconName::UserRoundPlus).small())
.label("Requests")
.when(request_count > 0, |this| {
this.suffix(
h_flex()
@@ -86,29 +91,34 @@ impl Sidebar {
.min_w_6()
.justify_center()
.text_style(TextStyle::Caption2)
.text_color(cx.theme().text_muted)
.text_color(cx.theme().muted_foreground)
.text_center()
.rounded(px(control_radius()))
.bg(cx.theme().surface_raised)
.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().accent),
div()
.size_1()
.rounded_full()
.bg(cx.theme().primary),
)
}),
)
})
.when(requests_open, |this| this.bg(cx.theme().element_active))
.when(requests_open, |this| this.bg(cx.theme().list_active))
.on_click(cx.listener(|this, _event, _window, cx| {
this.toggle_requests(cx);
})),
)
.child(
NavItem::new("contacts", "Contacts", Icon::new(IconName::Book).small())
.when(contacts_open, |this| this.bg(cx.theme().element_active))
Nav::new("contacts")
.prefix(Icon::new(IconName::Book).small())
.label("Contacts")
.when(contacts_open, |this| this.bg(cx.theme().list_active))
.on_click(cx.listener(|this, _event, window, cx| {
this.toggle_contacts(window, cx);
})),
@@ -119,8 +129,8 @@ impl Sidebar {
div()
.px_4()
.text_style(TextStyle::Caption)
.font_semibold()
.text_color(cx.theme().text_muted)
.font_weight(gpui::FontWeight::SEMIBOLD)
.text_color(cx.theme().muted_foreground)
.child(list_title),
)
})
+377 -376
View File
@@ -1,376 +1,377 @@
use std::collections::HashSet;
use anyhow::Error;
use chat::{ChatEvent, ChatRegistry};
use common::DebouncedDelay;
use gpui::prelude::FluentBuilder;
use gpui::{
AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
InteractiveElement, IntoElement, ObjectFit, ParentElement, Render, SharedString, Styled,
StyledImage, Subscription, Task, UniformListScrollHandle, WeakEntity, Window, div, img, px,
retain_all,
};
use instant::Duration;
use nip29::{Group, GroupKey, GroupsEvent, GroupsRegistry};
use nostr_sdk::prelude::*;
use person::{Directory, PersonRegistry};
use smallvec::{SmallVec, smallvec};
use state::{FIND_DELAY, NostrRegistry, StateEvent};
use theme::{ActiveTheme, TABBAR_HEIGHT, TextStyle, Typeset as _};
use ui::dock::{DockArea, Panel, PanelEvent};
use ui::indicator::Indicator;
use ui::input::{InputEvent, InputState};
use ui::notification::Notification;
use ui::{Sizable, StyledExt, WindowExtension, h_flex, title_bar_drag_handlers, v_flex};
mod group_view;
mod header;
mod inbox;
mod list;
mod onboarding;
mod rows;
mod search;
pub(crate) use rows::open_room;
use search::SearchResult;
pub struct Sidebar {
focus_handle: FocusHandle,
list_scroll: UniformListScrollHandle,
dock: WeakEntity<DockArea>,
/// Random background shown behind the signed-out screen.
banner: SharedString,
/// The group whose information the sidebar is browsing, if any.
group: Option<WeakEntity<Group>>,
/// Whether the sidebar is showing the group's information instead of its list.
group_open: bool,
/// Scroll state of the group's member list.
group_scroll: UniformListScrollHandle,
/// Whether the group view's admins section is expanded.
admins_open: bool,
/// Whether the group view's members section is expanded.
members_open: bool,
/// Whether the main list's pinned section is expanded.
pins_open: bool,
/// Whether the user expanded or collapsed the main list's groups section.
groups_open: Option<bool>,
/// Same as `groups_open` for the chats section.
chats_open: Option<bool>,
/// Whether the main list is showing contacts to start a chat with.
contacts_open: bool,
/// The signed-in user's contacts, loaded when the contact picker opens.
contacts: Option<Vec<PublicKey>>,
/// Person directory for contacts, search hits, and group members
directory: Entity<Directory>,
/// Contacts selected in the contact picker.
selected_contacts: HashSet<PublicKey>,
/// Scroll state of the contact picker's list.
contacts_scroll: UniformListScrollHandle,
/// Whether the main list is showing chat requests instead of chats and groups.
requests_open: bool,
new_requests: bool,
requests_scroll: UniformListScrollHandle,
/// Whether the search bar is shown below the header.
search_open: bool,
/// Whether searches also cover global (relay) profiles.
global_open: bool,
/// Debounced delay for the global search while typing.
find_debouncer: DebouncedDelay<Self>,
/// Search input state.
find_input: Entity<InputState>,
/// Search results: rooms and groups by name, then messages by content.
results: Vec<SearchResult>,
/// Whether a global (relay) search is in progress.
global_finding: bool,
/// Profiles found by the last global search, if one was triggered.
global_results: Option<Vec<PublicKey>>,
/// The async global profile search.
global_task: Option<Task<()>>,
/// Scroll state of the search results list.
search_scroll: UniformListScrollHandle,
tasks: SmallVec<[Task<Result<(), Error>>; 1]>,
_subscriptions: SmallVec<[Subscription; 4]>,
}
impl Sidebar {
pub fn new(window: &mut Window, dock: WeakEntity<DockArea>, cx: &mut Context<Self>) -> Self {
let chat = ChatRegistry::global(cx);
let groups = GroupsRegistry::global(cx);
let nostr = NostrRegistry::global(cx);
let wot = PersonRegistry::global(cx).read(cx).wot();
let find_input = cx.new(|cx| {
InputState::new(window, cx)
.placeholder("Type to search")
.clean_on_escape()
});
let mut subscriptions = smallvec![];
subscriptions.push(
cx.subscribe_in(&chat, window, move |this, _s, event, _window, cx| {
if event == &ChatEvent::Ping {
this.new_requests = true;
cx.notify();
};
}),
);
subscriptions.push(
cx.subscribe_in(&wot, window, |_this, _wot, _event, _window, cx| {
cx.notify();
}),
);
subscriptions.push(
cx.subscribe_in(&groups, window, |_this, _, event, window, cx| {
match event {
GroupsEvent::Updated => {
// TODO: find a better way?
cx.notify();
}
GroupsEvent::Error(error) => {
window.push_notification(Notification::error(error.clone()), cx);
}
GroupsEvent::Created {
warning: Some(warning),
..
} => {
window.push_notification(Notification::warning(warning.clone()), cx);
}
_ => {}
};
}),
);
subscriptions.push(
cx.subscribe_in(
&nostr,
window,
|this, state, event, window, cx| match event {
StateEvent::NoSigner => {
this.banner = onboarding::pick_banner();
if this.search_open {
this.toggle_search(window, cx);
}
cx.notify();
}
StateEvent::SignerChanged => {
if let Some(current_user) = state.read(cx).current_user() {
this.directory.update(cx, |directory, cx| {
directory.track([current_user], cx);
});
}
}
_ => {}
},
),
);
subscriptions.push(cx.subscribe_in(
&find_input,
window,
|this, state, event, window, cx| match event {
InputEvent::PressEnter { .. } => {
this.search(cx);
if this.global_open {
this.global_search(cx);
}
}
InputEvent::Change => {
if state.read(cx).value().is_empty() {
this.clear_results(cx);
} else {
this.search(cx);
if this.global_open {
this.find_debouncer.fire_new(
Duration::from_millis(FIND_DELAY),
window,
cx,
|this, window, cx| this.debounced_global(window, cx),
);
}
}
}
_ => {}
},
));
Self {
focus_handle: cx.focus_handle(),
list_scroll: UniformListScrollHandle::new(),
dock,
banner: onboarding::pick_banner(),
group: None,
group_open: false,
group_scroll: UniformListScrollHandle::new(),
admins_open: true,
members_open: true,
pins_open: true,
groups_open: None,
chats_open: None,
contacts_open: false,
contacts: None,
directory: Directory::new(cx),
selected_contacts: HashSet::new(),
contacts_scroll: UniformListScrollHandle::new(),
requests_open: false,
requests_scroll: UniformListScrollHandle::new(),
new_requests: false,
search_open: false,
global_open: false,
find_debouncer: DebouncedDelay::new(),
find_input,
results: Vec::new(),
global_finding: false,
global_results: None,
global_task: None,
search_scroll: UniformListScrollHandle::new(),
tasks: smallvec![],
_subscriptions: subscriptions,
}
}
/// Show the group view without toggling it closed.
pub(crate) fn show_group_meta(&mut self, key: &GroupKey, cx: &mut Context<Self>) {
let groups = GroupsRegistry::global(cx);
let Some(group) = groups.read(cx).group(key, cx) else {
return;
};
self.show_group(&group, cx);
}
/// Hide the group view if the sidebar is showing the given group.
pub(crate) fn hide_group_meta(&mut self, key: &GroupKey, cx: &mut Context<Self>) {
let showing = self
.group
.as_ref()
.and_then(|group| group.upgrade())
.is_some_and(|group| group.read(cx).key() == key);
if showing {
self.hide_group(cx);
}
}
}
impl Panel for Sidebar {
fn panel_id(&self) -> SharedString {
"Sidebar".into()
}
fn title(&self, _cx: &App) -> AnyElement {
SharedString::from("Sidebar").into_any_element()
}
fn closable(&self, _cx: &App) -> bool {
false
}
fn zoomable(&self, _cx: &App) -> bool {
false
}
}
impl EventEmitter<PanelEvent> for Sidebar {}
impl Focusable for Sidebar {
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
self.focus_handle.clone()
}
}
impl Render for Sidebar {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let nostr = NostrRegistry::global(cx);
let current_user = nostr.read(cx).current_user();
let logged_in = current_user.is_some();
let chat = ChatRegistry::global(cx);
let loading = chat.read(cx).loading();
let group = self.group.clone().and_then(|group| group.upgrade());
let searching = self.search_open && !self.find_input.read(cx).value().is_empty();
v_flex()
.image_cache(retain_all("sidebar"))
.size_full()
.relative()
.gap(px(theme::SPACE))
.bg(cx.theme().surface)
.when_some(current_user.as_ref(), |this, current_user| {
this.child(title_bar_drag_handlers(
self.render_user(current_user, cx),
window,
cx,
))
})
.when(self.search_open, |this| this.child(self.render_find(cx)))
.when(!logged_in, |this| {
this.relative()
.child(title_bar_drag_handlers(
div()
.id("onboarding-drag")
.absolute()
.top_0()
.left_0()
.h(TABBAR_HEIGHT)
.w_full(),
window,
cx,
))
.child(
div().absolute().inset_0().child(
img(self.banner.clone())
.size_full()
.object_fit(ObjectFit::Cover),
),
)
.child(onboarding::render_signed_out(window, cx))
})
.map(|this| match group {
Some(group) if self.group_open && !searching => {
this.child(self.render_group(group, cx))
}
_ if searching => this.child(self.render_results(cx)),
_ => this.child(self.render_list(cx)),
})
.when(loading && logged_in, |this| {
this.child(
div()
.absolute()
.bottom_4()
.left_0()
.h_9()
.w_full()
.px_8()
.child(
h_flex()
.gap(px(theme::SPACE))
.w_full()
.h_9()
.justify_center()
.bg(cx.theme().bg.opacity(0.85))
.when(cx.theme().shadow, |this| this.shadow_md())
.rounded_full()
.text_style(TextStyle::Caption)
.font_semibold()
.text_color(cx.theme().text_muted)
.child(Indicator::new().small().color(cx.theme().accent))
.child("Getting messages..."),
),
)
})
.into_any_element()
}
}
use std::collections::HashSet;
use anyhow::Error;
use chat::{ChatEvent, ChatRegistry};
use common::DebouncedDelay;
use gpui::prelude::FluentBuilder;
use gpui::{
App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement,
IntoElement, ObjectFit, ParentElement, Render, SharedString, Styled, StyledImage, Subscription,
Task, UniformListScrollHandle, WeakEntity, Window, div, img, px, retain_all,
};
use gpui_kit::component::dock::{DockArea, Panel, PanelEvent};
use gpui_kit::component::input::{InputEvent, InputState};
use gpui_kit::component::notification::Notification;
use gpui_kit::component::spinner::Spinner;
use gpui_kit::component::{ActiveTheme, Sizable, WindowExt, h_flex, v_flex};
use instant::Duration;
use nip29::{Group, GroupKey, GroupsEvent, GroupsRegistry};
use nostr_sdk::prelude::*;
use person::{Directory, PersonRegistry};
use smallvec::{SmallVec, smallvec};
use state::{FIND_DELAY, NostrRegistry, StateEvent};
use ui::{TABBAR_HEIGHT, TextStyle, Typeset as _, title_bar_drag_handlers};
mod group_view;
mod header;
mod inbox;
mod list;
mod onboarding;
mod rows;
mod search;
pub(crate) use rows::open_room;
use search::SearchResult;
pub struct Sidebar {
focus_handle: FocusHandle,
list_scroll: UniformListScrollHandle,
dock: WeakEntity<DockArea>,
/// Random background shown behind the signed-out screen.
banner: SharedString,
/// The group whose information the sidebar is browsing, if any.
group: Option<WeakEntity<Group>>,
/// Whether the sidebar is showing the group's information instead of its list.
group_open: bool,
/// Scroll state of the group's member list.
group_scroll: UniformListScrollHandle,
/// Whether the group view's admins section is expanded.
admins_open: bool,
/// Whether the group view's members section is expanded.
members_open: bool,
/// Whether the main list's pinned section is expanded.
pins_open: bool,
/// Whether the user expanded or collapsed the main list's groups section.
groups_open: Option<bool>,
/// Same as `groups_open` for the chats section.
chats_open: Option<bool>,
/// Whether the main list is showing contacts to start a chat with.
contacts_open: bool,
/// The signed-in user's contacts, loaded when the contact picker opens.
contacts: Option<Vec<PublicKey>>,
/// Person directory for contacts, search hits, and group members
directory: Entity<Directory>,
/// Contacts selected in the contact picker.
selected_contacts: HashSet<PublicKey>,
/// Scroll state of the contact picker's list.
contacts_scroll: UniformListScrollHandle,
/// Whether the main list is showing chat requests instead of chats and groups.
requests_open: bool,
new_requests: bool,
requests_scroll: UniformListScrollHandle,
/// Whether the search bar is shown below the header.
search_open: bool,
/// Whether searches also cover global (relay) profiles.
global_open: bool,
/// Debounced delay for the global search while typing.
find_debouncer: DebouncedDelay<Self>,
/// Search input state.
find_input: Entity<InputState>,
/// Search results: rooms and groups by name, then messages by content.
results: Vec<SearchResult>,
/// Whether a global (relay) search is in progress.
global_finding: bool,
/// Profiles found by the last global search, if one was triggered.
global_results: Option<Vec<PublicKey>>,
/// The async global profile search.
global_task: Option<Task<()>>,
/// Scroll state of the search results list.
search_scroll: UniformListScrollHandle,
tasks: SmallVec<[Task<Result<(), Error>>; 1]>,
_subscriptions: SmallVec<[Subscription; 4]>,
}
impl Sidebar {
pub fn new(window: &mut Window, dock: WeakEntity<DockArea>, cx: &mut Context<Self>) -> Self {
let chat = ChatRegistry::global(cx);
let groups = GroupsRegistry::global(cx);
let nostr = NostrRegistry::global(cx);
let wot = PersonRegistry::global(cx).read(cx).wot();
let find_input = cx.new(|cx| {
InputState::new(window, cx)
.placeholder("Type to search")
.clean_on_escape()
});
let mut subscriptions = smallvec![];
subscriptions.push(
cx.subscribe_in(&chat, window, move |this, _s, event, _window, cx| {
if event == &ChatEvent::Ping {
this.new_requests = true;
cx.notify();
};
}),
);
subscriptions.push(
cx.subscribe_in(&wot, window, |_this, _wot, _event, _window, cx| {
cx.notify();
}),
);
subscriptions.push(
cx.subscribe_in(&groups, window, |_this, _, event, window, cx| {
match event {
GroupsEvent::Updated => {
// TODO: find a better way?
cx.notify();
}
GroupsEvent::Error(error) => {
window.push_notification(Notification::error(error.clone()), cx);
}
GroupsEvent::Created {
warning: Some(warning),
..
} => {
window.push_notification(Notification::warning(warning.clone()), cx);
}
_ => {}
};
}),
);
subscriptions.push(
cx.subscribe_in(
&nostr,
window,
|this, state, event, window, cx| match event {
StateEvent::NoSigner => {
this.banner = onboarding::pick_banner();
if this.search_open {
this.toggle_search(window, cx);
}
cx.notify();
}
StateEvent::SignerChanged => {
if let Some(current_user) = state.read(cx).current_user() {
this.directory.update(cx, |directory, cx| {
directory.track([current_user], cx);
});
}
}
_ => {}
},
),
);
subscriptions.push(cx.subscribe_in(
&find_input,
window,
|this, state, event, window, cx| match event {
InputEvent::PressEnter { .. } => {
this.search(cx);
if this.global_open {
this.global_search(cx);
}
}
InputEvent::Change => {
if state.read(cx).value().is_empty() {
this.clear_results(cx);
} else {
this.search(cx);
if this.global_open {
this.find_debouncer.fire_new(
Duration::from_millis(FIND_DELAY),
window,
cx,
|this, window, cx| this.debounced_global(window, cx),
);
}
}
}
_ => {}
},
));
Self {
focus_handle: cx.focus_handle(),
list_scroll: UniformListScrollHandle::new(),
dock,
banner: onboarding::pick_banner(),
group: None,
group_open: false,
group_scroll: UniformListScrollHandle::new(),
admins_open: true,
members_open: true,
pins_open: true,
groups_open: None,
chats_open: None,
contacts_open: false,
contacts: None,
directory: Directory::new(cx),
selected_contacts: HashSet::new(),
contacts_scroll: UniformListScrollHandle::new(),
requests_open: false,
requests_scroll: UniformListScrollHandle::new(),
new_requests: false,
search_open: false,
global_open: false,
find_debouncer: DebouncedDelay::new(),
find_input,
results: Vec::new(),
global_finding: false,
global_results: None,
global_task: None,
search_scroll: UniformListScrollHandle::new(),
tasks: smallvec![],
_subscriptions: subscriptions,
}
}
/// Show the group view without toggling it closed.
pub(crate) fn show_group_meta(&mut self, key: &GroupKey, cx: &mut Context<Self>) {
let groups = GroupsRegistry::global(cx);
let Some(group) = groups.read(cx).group(key, cx) else {
return;
};
self.show_group(&group, cx);
}
/// Hide the group view if the sidebar is showing the given group.
pub(crate) fn hide_group_meta(&mut self, key: &GroupKey, cx: &mut Context<Self>) {
let showing = self
.group
.as_ref()
.and_then(|group| group.upgrade())
.is_some_and(|group| group.read(cx).key() == key);
if showing {
self.hide_group(cx);
}
}
}
impl gpui_kit::component::dock::BasePanel for Sidebar {
fn panel_name(&self) -> &'static str {
"Sidebar"
}
fn closable(&self, _cx: &App) -> bool {
false
}
fn zoomable(&self, _cx: &App) -> bool {
false
}
}
impl Panel for Sidebar {
fn title(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
SharedString::from("Sidebar")
}
}
impl EventEmitter<PanelEvent> for Sidebar {}
impl Focusable for Sidebar {
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
self.focus_handle.clone()
}
}
impl Render for Sidebar {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let nostr = NostrRegistry::global(cx);
let current_user = nostr.read(cx).current_user();
let logged_in = current_user.is_some();
let chat = ChatRegistry::global(cx);
let loading = chat.read(cx).loading();
let group = self.group.clone().and_then(|group| group.upgrade());
let searching = self.search_open && !self.find_input.read(cx).value().is_empty();
v_flex()
.image_cache(retain_all("sidebar"))
.size_full()
.relative()
.gap(px(ui::SPACE))
.bg(cx.theme().sidebar)
.when_some(current_user.as_ref(), |this, current_user| {
this.child(title_bar_drag_handlers(
self.render_user(current_user, cx),
window,
cx,
))
})
.when(self.search_open, |this| this.child(self.render_find(cx)))
.when(!logged_in, |this| {
this.relative()
.child(title_bar_drag_handlers(
div()
.id("onboarding-drag")
.absolute()
.top_0()
.left_0()
.h(TABBAR_HEIGHT)
.w_full(),
window,
cx,
))
.child(
div().absolute().inset_0().child(
img(self.banner.clone())
.size_full()
.object_fit(ObjectFit::Cover),
),
)
.child(onboarding::render_signed_out(window, cx))
})
.map(|this| match group {
Some(group) if self.group_open && !searching => {
this.child(self.render_group(group, cx))
}
_ if searching => this.child(self.render_results(cx)),
_ => this.child(self.render_list(cx)),
})
.when(loading && logged_in, |this| {
this.child(
div()
.absolute()
.bottom_4()
.left_0()
.h_9()
.w_full()
.px_8()
.child(
h_flex()
.gap(px(ui::SPACE))
.w_full()
.h_9()
.justify_center()
.bg(cx.theme().background.opacity(0.85))
.when(cx.theme().shadow, |this| this.shadow_md())
.rounded_full()
.text_style(TextStyle::Caption)
.font_weight(gpui::FontWeight::SEMIBOLD)
.text_color(cx.theme().muted_foreground)
.child(Spinner::new().small().color(cx.theme().primary))
.child("Getting messages..."),
),
)
})
.into_any_element()
}
}
+5 -5
View File
@@ -1,8 +1,8 @@
use gpui_kit::component::button::{Button, ButtonCustomVariant, ButtonVariants};
use gpui_kit::component::{Sizable, v_flex};
use std::time::{SystemTime, UNIX_EPOCH};
use gpui::{AnyElement, App, IntoElement, ParentElement, SharedString, Styled, Window, img};
use ui::button::{Button, ButtonCustomVariant, ButtonVariants};
use ui::{Sizable, StyledExt, v_flex};
use crate::dialogs::import;
@@ -20,7 +20,7 @@ pub(super) fn pick_banner() -> SharedString {
}
/// The headline and identity import shown while signed out.
pub(super) fn render_signed_out(window: &mut Window, cx: &mut App) -> AnyElement {
pub(super) fn render_signed_out(_window: &mut Window, cx: &mut App) -> AnyElement {
v_flex()
.size_full()
.justify_end()
@@ -31,14 +31,14 @@ pub(super) fn render_signed_out(window: &mut Window, cx: &mut App) -> AnyElement
Button::new("import")
.label("Import Identity")
.custom(
ButtonCustomVariant::new(window, cx)
ButtonCustomVariant::new(cx)
.color(gpui::white())
.foreground(gpui::black())
.hover(gpui::white().opacity(0.9))
.active(gpui::white().opacity(0.8)),
)
.large()
.font_semibold()
.font_weight(gpui::FontWeight::SEMIBOLD)
.on_click(|_, window, cx| {
import::open(window, cx);
}),
+89 -72
View File
@@ -4,24 +4,25 @@ use chat::{Room, RoomKind};
use common::TimestampExt;
use gpui::prelude::FluentBuilder;
use gpui::{
AnyElement, App, Context, Entity, IntoElement, ParentElement, SharedString, Styled, WeakEntity,
Window, div, px,
AnyElement, App, Context, Entity, InteractiveElement as _, IntoElement, ParentElement,
SharedString, Styled, WeakEntity, Window, div, px,
};
use gpui_kit::assets::IconName;
use gpui_kit::component::avatar::Avatar;
use gpui_kit::component::dialog::DialogButtonProps;
use gpui_kit::component::dock::{ClosePanel, DockArea, DockPlacement, PanelHandle};
use gpui_kit::component::menu::{ContextMenuExt as _, PopupMenuItem};
use gpui_kit::component::{ActiveTheme, Icon, Selectable, Sizable, WindowExt, h_flex, v_flex};
use nip29::Group;
use nostr_sdk::prelude::*;
use settings::AppSettings;
use theme::{ActiveTheme, TextStyle, Typeset as _, button_radius};
use ui::avatar::Avatar;
use ui::dialog::DialogButtonProps;
use ui::dock::{ClosePanel, DockArea, DockPlacement, PanelHandle};
use ui::menu::{ContextMenu, PopupMenuItem};
use ui::nav::Nav;
use ui::{Icon, IconName, Selectable, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
use ui::{TextStyle, Typeset as _, button_radius};
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(
@@ -30,7 +31,7 @@ pub(crate) fn open_room(
window: &mut Window,
cx: &mut App,
) {
ui::dock::add_panel_to(
crate::dock::add_panel_to(
dock,
PanelHandle::new(chat_ui::init(room.downgrade(), window, cx)),
DockPlacement::Center,
@@ -42,8 +43,9 @@ pub(crate) fn open_room(
let peer = room.read(cx).display_member(cx).public_key();
let view = screening::init(peer, window, cx);
window.open_dialog(cx, move |this, _window, _cx| {
this.confirm()
window.open_alert_dialog(cx, move |alert, _window, _cx| {
alert
.confirm()
.child(view.clone())
.button_props(
DialogButtonProps::default()
@@ -86,17 +88,21 @@ 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().text_muted))
.suffix(
Icon::new(icon)
.small()
.text_color(cx.theme().muted_foreground),
)
.text_style(TextStyle::Caption)
.font_semibold()
.text_color(cx.theme().text_muted)
.font_weight(gpui::FontWeight::SEMIBOLD)
.text_color(cx.theme().muted_foreground)
.on_click(cx.listener(move |this, _event, _window, cx| {
match section {
ListSection::Pins => this.pins_open = !this.pins_open,
@@ -117,8 +123,8 @@ impl Sidebar {
Nav::new(label)
.label(label)
.text_style(TextStyle::Caption)
.font_semibold()
.text_color(cx.theme().text_muted)
.font_weight(gpui::FontWeight::SEMIBOLD)
.text_color(cx.theme().muted_foreground)
.into_any_element()
}
SidebarRow::Empty(text) => v_flex()
@@ -136,7 +142,7 @@ impl Sidebar {
.border_color(cx.theme().border)
.rounded(px(button_radius()))
.text_style(TextStyle::Caption)
.text_color(cx.theme().text_muted)
.text_color(cx.theme().muted_foreground)
.child(*text),
)
.into_any_element(),
@@ -144,6 +150,7 @@ impl Sidebar {
let id = room.read(cx).id;
let name = room.read(cx).display_name(cx);
let picture = room.read(cx).display_image(cx);
#[allow(unused_variables)]
let seed = room.read(cx).display_image_seed(cx);
let created_at = room.read(cx).created_at.to_ago();
let member = room.read(cx).members().first().copied();
@@ -158,12 +165,14 @@ impl Sidebar {
.label(name)
.text_style(TextStyle::Callout)
.when(!hide_avatar, |this| {
this.prefix(
Avatar::from_source(picture)
.seed(seed)
.small()
.flex_shrink_0(),
)
this.prefix(match picture {
Some(picture) => {
Avatar::new().src(picture).small().into_any_element()
}
None => {
Avatar::new().small().flex_shrink_0().into_any_element()
}
})
})
.suffix(
h_flex()
@@ -182,9 +191,9 @@ impl Sidebar {
.when(!spam, |this| {
this.child(
div()
.font_normal()
.font_weight(gpui::FontWeight::NORMAL)
.text_style(TextStyle::Caption)
.text_color(cx.theme().text_faint)
.text_color(cx.theme().muted_foreground)
.child(created_at),
)
}),
@@ -193,10 +202,11 @@ impl Sidebar {
open_room(&room, &dock, window, cx);
});
ContextMenu::new(
format!("room-menu-{index}"),
nav,
move |menu, _window, _cx| {
div()
.id(index)
.w_full()
.child(nav)
.context_menu(move |menu, _window, _cx| {
let menu = menu.item(
PopupMenuItem::new(if pinned { "Unpin" } else { "Pin" })
.on_click(move |_, _window, cx| {
@@ -215,9 +225,8 @@ impl Sidebar {
} else {
menu
}
},
)
.into_any_element()
})
.into_any_element()
}
SidebarRow::Contact { public_key } => {
let person = self.directory.read(cx).person(public_key, cx);
@@ -229,12 +238,14 @@ impl Sidebar {
.label(person.name())
.text_style(TextStyle::Callout)
.when(!hide_avatar, |this| {
this.prefix(
Avatar::from_source(person.avatar())
.seed(person.avatar_seed())
.small()
.flex_shrink_0(),
)
this.prefix(match person.avatar() {
Some(picture) => {
Avatar::new().src(picture).small().into_any_element()
}
None => {
Avatar::new().small().flex_shrink_0().into_any_element()
}
})
})
.selected(selected)
.on_click(move |_event, _window, cx| {
@@ -254,12 +265,14 @@ impl Sidebar {
.label(person.name())
.text_style(TextStyle::Callout)
.when(!hide_avatar, |this| {
this.prefix(
Avatar::from_source(person.avatar())
.seed(person.avatar_seed())
.small()
.flex_shrink_0(),
)
this.prefix(match person.avatar() {
Some(picture) => {
Avatar::new().src(picture).small().into_any_element()
}
None => {
Avatar::new().small().flex_shrink_0().into_any_element()
}
})
})
.on_click(move |_event, window, cx| {
profile::open(public_key, window, cx);
@@ -289,24 +302,26 @@ impl Sidebar {
.label(name)
.text_style(TextStyle::Callout)
.when(!hide_avatar, |this| {
this.prefix(
Avatar::from_source(picture)
.seed(seed)
.small()
.flex_shrink_0(),
)
this.prefix(match picture {
Some(picture) => {
Avatar::new().src(picture).small().into_any_element()
}
None => {
Avatar::new().small().flex_shrink_0().into_any_element()
}
})
})
.when_some(suffix, |this, suffix| {
this.suffix(
div()
.font_normal()
.font_weight(gpui::FontWeight::NORMAL)
.text_style(TextStyle::Caption)
.text_color(cx.theme().text_faint)
.text_color(cx.theme().muted_foreground)
.child(suffix),
)
})
.on_click(move |_event, window, cx| {
ui::dock::add_panel_to(
crate::dock::add_panel_to(
&dock,
PanelHandle::new(nip29_ui::init(group.clone(), window, cx)),
DockPlacement::Center,
@@ -321,10 +336,11 @@ impl Sidebar {
}
});
ContextMenu::new(
format!("group-menu-{index}"),
nav,
move |menu, _window, _cx| {
div()
.id(index)
.w_full()
.child(nav)
.context_menu(move |menu, _window, _cx| {
menu.item(
PopupMenuItem::new(if pinned { "Unpin" } else { "Pin" })
.on_click({
@@ -347,9 +363,8 @@ impl Sidebar {
}
}),
)
},
)
.into_any_element()
})
.into_any_element()
}
SidebarRow::Message { hit } => {
let person = self.directory.read(cx).person(&hit.author, cx);
@@ -362,20 +377,22 @@ impl Sidebar {
Nav::new(index)
.label(content)
.text_style(TextStyle::Callout)
.font_medium()
.font_weight(gpui::FontWeight::MEDIUM)
.when(!hide_avatar, |this| {
this.prefix(
Avatar::from_source(person.avatar())
.seed(person.avatar_seed())
.small()
.flex_shrink_0(),
)
this.prefix(match person.avatar() {
Some(picture) => {
Avatar::new().src(picture).small().into_any_element()
}
None => {
Avatar::new().small().flex_shrink_0().into_any_element()
}
})
})
.suffix(
div()
.font_normal()
.font_weight(gpui::FontWeight::NORMAL)
.text_style(TextStyle::Caption)
.text_color(cx.theme().text_faint)
.text_color(cx.theme().muted_foreground)
.child(detail),
)
.on_click(move |_event, window, cx| {
+7 -6
View File
@@ -1,3 +1,8 @@
use gpui_kit::assets::IconName;
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::input::Input;
use gpui_kit::component::scroll::Scrollbar;
use gpui_kit::component::{ActiveTheme, Selectable, Sizable, h_flex, v_flex};
use std::collections::HashSet;
use std::rc::Rc;
@@ -9,11 +14,7 @@ use gpui::{
use nip29::{Group, GroupsRegistry};
use nostr_sdk::prelude::*;
use state::{FIND_LIMIT, NostrRegistry};
use theme::{ActiveTheme, button_radius};
use ui::button::{Button, ButtonVariants};
use ui::input::Input;
use ui::scroll::Scrollbar;
use ui::{IconName, Selectable, Sizable, h_flex, v_flex};
use ui::button_radius;
use super::Sidebar;
use super::rows::SidebarRow;
@@ -215,7 +216,7 @@ impl Sidebar {
.gap_1()
.w_full()
.rounded(px(button_radius()))
.bg(cx.theme().surface_raised)
.bg(cx.theme().secondary)
.child(Input::new(&self.find_input).appearance(false).small())
.child(
Button::new("sidebar-global")