migrate ui components to gpui-kit
This commit is contained in:
@@ -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" }
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
})),
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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")),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
})),
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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())),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}),
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user