refactor ui (#17)
* wip: redesign sidebar * wip: adjust dpi * update * update * refactor modal * fix modal
This commit is contained in:
@@ -1,31 +1,47 @@
|
||||
use account::Account;
|
||||
use common::profile::SharedProfile;
|
||||
use global::get_client;
|
||||
use gpui::{
|
||||
actions, div, img, impl_internal_actions, prelude::FluentBuilder, px, App, AppContext, Axis,
|
||||
Context, Entity, InteractiveElement, IntoElement, ParentElement, Render, Styled, Subscription,
|
||||
Task, Window,
|
||||
div, impl_internal_actions, prelude::FluentBuilder, px, App, AppContext, Axis, Context, Entity,
|
||||
InteractiveElement, IntoElement, ParentElement, Render, Styled, Subscription, Window,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use std::sync::Arc;
|
||||
use ui::{
|
||||
button::{Button, ButtonRounded, ButtonVariants},
|
||||
button::{Button, ButtonVariants},
|
||||
dock_area::{dock::DockPlacement, panel::PanelView, DockArea, DockItem},
|
||||
popup_menu::PopupMenuExt,
|
||||
theme::{scale::ColorScaleStep, ActiveTheme, Appearance, Theme},
|
||||
ContextModal, Icon, IconName, Root, Sizable, TitleBar,
|
||||
theme::{ActiveTheme, Appearance, Theme},
|
||||
ContextModal, IconName, Root, Sizable, TitleBar,
|
||||
};
|
||||
|
||||
use crate::views::{chat, contacts, profile, relays, settings, welcome};
|
||||
use crate::views::{chat, compose, contacts, profile, relays, welcome};
|
||||
use crate::views::{onboarding, sidebar};
|
||||
|
||||
const MODAL_WIDTH: f32 = 420.;
|
||||
const SIDEBAR_WIDTH: f32 = 280.;
|
||||
|
||||
impl_internal_actions!(dock, [AddPanel, ToggleModal]);
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<ChatSpace> {
|
||||
ChatSpace::new(window, cx)
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Deserialize)]
|
||||
pub enum PanelKind {
|
||||
Room(u64),
|
||||
// More kind will be added here
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Deserialize)]
|
||||
pub enum ModalKind {
|
||||
Profile,
|
||||
Contacts,
|
||||
Settings,
|
||||
Compose,
|
||||
Contact,
|
||||
Relay,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Deserialize)]
|
||||
pub struct ToggleModal {
|
||||
pub modal: ModalKind,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Deserialize)]
|
||||
@@ -40,16 +56,6 @@ impl AddPanel {
|
||||
}
|
||||
}
|
||||
|
||||
// Dock actions
|
||||
impl_internal_actions!(dock, [AddPanel]);
|
||||
|
||||
// Account actions
|
||||
actions!(account, [Logout]);
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<ChatSpace> {
|
||||
ChatSpace::new(window, cx)
|
||||
}
|
||||
|
||||
pub struct ChatSpace {
|
||||
titlebar: bool,
|
||||
dock: Entity<DockArea>,
|
||||
@@ -61,23 +67,26 @@ impl ChatSpace {
|
||||
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||
let account = Account::global(cx);
|
||||
let dock = cx.new(|cx| DockArea::new(window, cx));
|
||||
let titlebar = false;
|
||||
|
||||
cx.new(|cx| {
|
||||
let mut subscriptions = smallvec![];
|
||||
|
||||
subscriptions.push(cx.observe_in(
|
||||
&account,
|
||||
window,
|
||||
|this: &mut ChatSpace, account, window, cx| {
|
||||
if account.read(cx).profile.is_some() {
|
||||
this.open_chats(window, cx);
|
||||
} else {
|
||||
this.open_onboarding(window, cx);
|
||||
}
|
||||
},
|
||||
));
|
||||
|
||||
let mut this = Self {
|
||||
dock,
|
||||
titlebar,
|
||||
subscriptions: smallvec![cx.observe_in(
|
||||
&account,
|
||||
window,
|
||||
|this: &mut ChatSpace, account, window, cx| {
|
||||
if account.read(cx).profile.is_some() {
|
||||
this.open_chats(window, cx);
|
||||
} else {
|
||||
this.open_onboarding(window, cx);
|
||||
}
|
||||
},
|
||||
)],
|
||||
subscriptions,
|
||||
titlebar: false,
|
||||
};
|
||||
|
||||
if Account::global(cx).read(cx).profile.is_some() {
|
||||
@@ -135,7 +144,7 @@ impl ChatSpace {
|
||||
);
|
||||
|
||||
self.dock.update(cx, |this, cx| {
|
||||
this.set_left_dock(left, Some(px(260.)), true, window, cx);
|
||||
this.set_left_dock(left, Some(px(SIDEBAR_WIDTH)), true, window, cx);
|
||||
this.set_center(center, window, cx);
|
||||
});
|
||||
}
|
||||
@@ -165,74 +174,6 @@ impl ChatSpace {
|
||||
}))
|
||||
}
|
||||
|
||||
fn render_account_btn(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
Button::new("account")
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.reverse()
|
||||
.icon(Icon::new(IconName::ChevronDownSmall))
|
||||
.when_some(
|
||||
Account::global(cx).read(cx).profile.as_ref(),
|
||||
|this, profile| this.child(img(profile.shared_avatar()).size_5()),
|
||||
)
|
||||
.popup_menu(move |this, _, _cx| {
|
||||
this.menu(
|
||||
"Profile",
|
||||
Box::new(AddPanel::new(PanelKind::Profile, DockPlacement::Right)),
|
||||
)
|
||||
.menu(
|
||||
"Contacts",
|
||||
Box::new(AddPanel::new(PanelKind::Contacts, DockPlacement::Right)),
|
||||
)
|
||||
.menu(
|
||||
"Settings",
|
||||
Box::new(AddPanel::new(PanelKind::Settings, DockPlacement::Center)),
|
||||
)
|
||||
.separator()
|
||||
.menu("Change account", Box::new(Logout))
|
||||
})
|
||||
}
|
||||
|
||||
fn render_relays_btn(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
Button::new("relays")
|
||||
.xsmall()
|
||||
.ghost()
|
||||
.icon(IconName::Relays)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.render_edit_relays(window, cx);
|
||||
}))
|
||||
}
|
||||
|
||||
fn render_edit_relays(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let relays = relays::init(window, cx);
|
||||
|
||||
window.open_modal(cx, move |this, window, cx| {
|
||||
let is_loading = relays.read(cx).loading();
|
||||
|
||||
this.width(px(420.))
|
||||
.title("Edit your Messaging Relays")
|
||||
.child(relays.clone())
|
||||
.footer(
|
||||
div()
|
||||
.p_2()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().base.step(cx, ColorScaleStep::FIVE))
|
||||
.child(
|
||||
Button::new("update_inbox_relays_btn")
|
||||
.label("Update")
|
||||
.primary()
|
||||
.bold()
|
||||
.rounded(ButtonRounded::Large)
|
||||
.w_full()
|
||||
.loading(is_loading)
|
||||
.on_click(window.listener_for(&relays, |this, _, window, cx| {
|
||||
this.update(window, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
fn on_panel_action(&mut self, action: &AddPanel, window: &mut Window, cx: &mut Context<Self>) {
|
||||
match &action.panel {
|
||||
PanelKind::Room(id) => {
|
||||
@@ -246,49 +187,55 @@ impl ChatSpace {
|
||||
Err(e) => window.push_notification(e.to_string(), cx),
|
||||
}
|
||||
}
|
||||
PanelKind::Profile => {
|
||||
let panel = profile::init(window, cx);
|
||||
|
||||
self.dock.update(cx, |dock_area, cx| {
|
||||
dock_area.add_panel(panel, action.position, window, cx);
|
||||
});
|
||||
}
|
||||
PanelKind::Contacts => {
|
||||
let panel = Arc::new(contacts::init(window, cx));
|
||||
|
||||
self.dock.update(cx, |dock_area, cx| {
|
||||
dock_area.add_panel(panel, action.position, window, cx);
|
||||
});
|
||||
}
|
||||
PanelKind::Settings => {
|
||||
let panel = Arc::new(settings::init(window, cx));
|
||||
|
||||
self.dock.update(cx, |dock_area, cx| {
|
||||
dock_area.add_panel(panel, action.position, window, cx);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn on_logout_action(&mut self, _action: &Logout, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let client = get_client();
|
||||
let reset: Task<Result<(), anyhow::Error>> = cx.background_spawn(async move {
|
||||
client.reset().await;
|
||||
Ok(())
|
||||
});
|
||||
fn on_modal_action(
|
||||
&mut self,
|
||||
action: &ToggleModal,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match action.modal {
|
||||
ModalKind::Profile => {
|
||||
let profile = profile::init(window, cx);
|
||||
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
if reset.await.is_ok() {
|
||||
cx.update(|_, cx| {
|
||||
Account::global(cx).update(cx, |this, cx| {
|
||||
this.profile = None;
|
||||
cx.notify();
|
||||
});
|
||||
window.open_modal(cx, move |modal, _, _| {
|
||||
modal
|
||||
.title("Profile")
|
||||
.width(px(MODAL_WIDTH))
|
||||
.child(profile.clone())
|
||||
})
|
||||
.ok();
|
||||
};
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
ModalKind::Compose => {
|
||||
let compose = compose::init(window, cx);
|
||||
|
||||
window.open_modal(cx, move |modal, _, _| {
|
||||
modal
|
||||
.title("Direct Messages")
|
||||
.width(px(MODAL_WIDTH))
|
||||
.child(compose.clone())
|
||||
})
|
||||
}
|
||||
ModalKind::Contact => {
|
||||
let contacts = contacts::init(window, cx);
|
||||
|
||||
window.open_modal(cx, move |this, _window, _cx| {
|
||||
this.width(px(MODAL_WIDTH))
|
||||
.title("Contacts")
|
||||
.child(contacts.clone())
|
||||
});
|
||||
}
|
||||
ModalKind::Relay => {
|
||||
let relays = relays::init(window, cx);
|
||||
|
||||
window.open_modal(cx, move |this, _, _| {
|
||||
this.width(px(MODAL_WIDTH))
|
||||
.title("Edit your Messaging Relays")
|
||||
.child(relays.clone())
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,9 +266,7 @@ impl Render for ChatSpace {
|
||||
.justify_end()
|
||||
.gap_2()
|
||||
.px_2()
|
||||
.child(self.render_appearance_btn(cx))
|
||||
.child(self.render_relays_btn(cx))
|
||||
.child(self.render_account_btn(cx)),
|
||||
.child(self.render_appearance_btn(cx)),
|
||||
),
|
||||
)
|
||||
})
|
||||
@@ -334,6 +279,6 @@ impl Render for ChatSpace {
|
||||
.children(modal_layer)
|
||||
// Actions
|
||||
.on_action(cx.listener(Self::on_panel_action))
|
||||
.on_action(cx.listener(Self::on_logout_action))
|
||||
.on_action(cx.listener(Self::on_modal_action))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,7 +261,7 @@ fn main() {
|
||||
}),
|
||||
window_bounds: Some(WindowBounds::Windowed(Bounds::centered(
|
||||
None,
|
||||
size(px(900.0), px(680.0)),
|
||||
size(px(920.0), px(700.0)),
|
||||
cx,
|
||||
))),
|
||||
#[cfg(target_os = "linux")]
|
||||
|
||||
@@ -14,14 +14,14 @@ use smallvec::{smallvec, SmallVec};
|
||||
use smol::fs;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use ui::{
|
||||
button::{Button, ButtonRounded, ButtonVariants},
|
||||
button::{Button, ButtonVariants},
|
||||
dock_area::panel::{Panel, PanelEvent},
|
||||
input::{InputEvent, TextInput},
|
||||
notification::Notification,
|
||||
popup_menu::PopupMenu,
|
||||
text::RichText,
|
||||
theme::{scale::ColorScaleStep, ActiveTheme},
|
||||
v_flex, ContextModal, Icon, IconName, Sizable, StyledExt,
|
||||
v_flex, ContextModal, Disableable, Icon, IconName, Size, StyledExt,
|
||||
};
|
||||
|
||||
const ALERT: &str = "has not set up Messaging (DM) Relays, so they will NOT receive your messages.";
|
||||
@@ -58,8 +58,7 @@ impl Chat {
|
||||
let attaches = cx.new(|_| None);
|
||||
let input = cx.new(|cx| {
|
||||
TextInput::new(window, cx)
|
||||
.appearance(false)
|
||||
.text_size(ui::Size::Small)
|
||||
.text_size(Size::Small)
|
||||
.placeholder("Message...")
|
||||
});
|
||||
|
||||
@@ -343,11 +342,12 @@ impl Chat {
|
||||
|
||||
div()
|
||||
.group("")
|
||||
.w_full()
|
||||
.relative()
|
||||
.flex()
|
||||
.gap_3()
|
||||
.w_full()
|
||||
.p_2()
|
||||
.px_3()
|
||||
.py_2()
|
||||
.map(|this| match message {
|
||||
RoomMessage::User(item) => {
|
||||
let text = text_data
|
||||
@@ -355,6 +355,7 @@ impl Chat {
|
||||
.or_insert_with(|| RichText::new(item.content.to_owned(), &item.mentions));
|
||||
|
||||
this.hover(|this| this.bg(cx.theme().accent.step(cx, ColorScaleStep::ONE)))
|
||||
.text_sm()
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
@@ -379,19 +380,18 @@ impl Chat {
|
||||
.flex()
|
||||
.items_baseline()
|
||||
.gap_2()
|
||||
.text_xs()
|
||||
.child(
|
||||
div().font_semibold().child(item.author.shared_name()),
|
||||
)
|
||||
.child(div().child(item.ago()).text_color(
|
||||
cx.theme().base.step(cx, ColorScaleStep::ELEVEN),
|
||||
)),
|
||||
.child(
|
||||
div()
|
||||
.text_color(
|
||||
cx.theme().base.step(cx, ColorScaleStep::NINE),
|
||||
)
|
||||
.child(item.ago()),
|
||||
),
|
||||
)
|
||||
.child(div().text_sm().child(text.element(
|
||||
"body".into(),
|
||||
window,
|
||||
cx,
|
||||
))),
|
||||
.child(text.element("body".into(), window, cx)),
|
||||
)
|
||||
}
|
||||
RoomMessage::System(content) => this
|
||||
@@ -407,7 +407,7 @@ impl Chat {
|
||||
.group_hover("", |this| this.bg(cx.theme().danger)),
|
||||
)
|
||||
.child(img("brand/avatar.png").size_8().flex_shrink_0())
|
||||
.text_xs()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().danger)
|
||||
.child(content.clone()),
|
||||
RoomMessage::Announcement => this
|
||||
@@ -419,12 +419,12 @@ impl Chat {
|
||||
.justify_center()
|
||||
.text_center()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
|
||||
.text_color(cx.theme().base.step(cx, ColorScaleStep::NINE))
|
||||
.line_height(relative(1.3))
|
||||
.child(
|
||||
svg()
|
||||
.path("brand/coop.svg")
|
||||
.size_8()
|
||||
.size_10()
|
||||
.text_color(cx.theme().base.step(cx, ColorScaleStep::THREE)),
|
||||
)
|
||||
.child(ROOM_DESCRIPTION),
|
||||
@@ -444,7 +444,7 @@ impl Panel for Chat {
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
@@ -459,7 +459,7 @@ impl Panel for Chat {
|
||||
.map(|(ix, facepill)| {
|
||||
div()
|
||||
.when(ix > 0, |div| div.ml_neg_1())
|
||||
.child(img(facepill).size_4())
|
||||
.child(img(facepill).size_5())
|
||||
}),
|
||||
),
|
||||
)
|
||||
@@ -491,7 +491,7 @@ impl Render for Chat {
|
||||
.size_full()
|
||||
.child(list(self.list_state.clone()).flex_1())
|
||||
.child(
|
||||
div().flex_shrink_0().p_2().child(
|
||||
div().flex_shrink_0().px_3().py_2().child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
@@ -539,7 +539,6 @@ impl Render for Chat {
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.h_9()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
@@ -550,30 +549,10 @@ impl Render for Chat {
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.upload_media(window, cx);
|
||||
}))
|
||||
.disabled(self.is_uploading)
|
||||
.loading(self.is_uploading),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.flex()
|
||||
.items_center()
|
||||
.bg(cx.theme().base.step(cx, ColorScaleStep::THREE))
|
||||
.rounded(px(cx.theme().radius))
|
||||
.pl_2()
|
||||
.pr_1()
|
||||
.child(self.input.clone())
|
||||
.child(
|
||||
Button::new("send")
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.bold()
|
||||
.rounded(ButtonRounded::Medium)
|
||||
.label("SEND")
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.send_message(window, cx)
|
||||
})),
|
||||
),
|
||||
),
|
||||
.child(self.input.clone()),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -7,9 +7,9 @@ use common::{profile::SharedProfile, random_name};
|
||||
use global::get_client;
|
||||
use gpui::{
|
||||
div, img, impl_internal_actions, prelude::FluentBuilder, px, relative, uniform_list, App,
|
||||
AppContext, ClickEvent, Context, Div, Entity, FocusHandle, InteractiveElement, IntoElement,
|
||||
ParentElement, Render, RenderOnce, SharedString, StatefulInteractiveElement, Styled,
|
||||
Subscription, Task, TextAlign, Window,
|
||||
AppContext, Context, Entity, FocusHandle, InteractiveElement, IntoElement, ParentElement,
|
||||
Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, TextAlign,
|
||||
Window,
|
||||
};
|
||||
use nostr_sdk::prelude::*;
|
||||
use serde::Deserialize;
|
||||
@@ -17,18 +17,18 @@ use smallvec::{smallvec, SmallVec};
|
||||
use smol::Timer;
|
||||
use std::{
|
||||
collections::{BTreeSet, HashSet},
|
||||
rc::Rc,
|
||||
time::Duration,
|
||||
};
|
||||
use ui::{
|
||||
button::{Button, ButtonRounded},
|
||||
button::{Button, ButtonVariants},
|
||||
input::{InputEvent, TextInput},
|
||||
theme::{scale::ColorScaleStep, ActiveTheme},
|
||||
ContextModal, Icon, IconName, Sizable, Size, StyledExt,
|
||||
ContextModal, Disableable, Icon, IconName, Sizable, Size, StyledExt,
|
||||
};
|
||||
|
||||
const DESCRIPTION: &str =
|
||||
"Start a conversation with someone using their npub or NIP-05 (like foo@bar.com).";
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Compose> {
|
||||
cx.new(|cx| Compose::new(window, cx))
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Deserialize)]
|
||||
struct SelectContact(PublicKey);
|
||||
@@ -58,7 +58,7 @@ impl Compose {
|
||||
let name = random_name(2);
|
||||
let mut input = TextInput::new(window, cx)
|
||||
.appearance(false)
|
||||
.text_size(Size::XSmall);
|
||||
.text_size(Size::Small);
|
||||
|
||||
input.set_placeholder("Family... . (Optional)");
|
||||
input.set_text(name, window, cx);
|
||||
@@ -193,18 +193,6 @@ impl Compose {
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn label(&self, _window: &Window, cx: &App) -> SharedString {
|
||||
if self.selected.read(cx).len() > 1 {
|
||||
"Create Group DM".into()
|
||||
} else {
|
||||
"Create DM".into()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_submitting(&self) -> bool {
|
||||
self.is_submitting
|
||||
}
|
||||
|
||||
fn add(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let client = get_client();
|
||||
let content = self.user_input.read(cx).text().to_string();
|
||||
@@ -336,6 +324,15 @@ impl Compose {
|
||||
|
||||
impl Render for Compose {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
const DESCRIPTION: &str =
|
||||
"Start a conversation with someone using their npub or NIP-05 (like foo@bar.com).";
|
||||
|
||||
let label: SharedString = if self.selected.read(cx).len() > 1 {
|
||||
"Create Group DM".into()
|
||||
} else {
|
||||
"Create DM".into()
|
||||
};
|
||||
|
||||
div()
|
||||
.track_focus(&self.focus_handle)
|
||||
.on_action(cx.listener(Self::on_action_select))
|
||||
@@ -344,15 +341,13 @@ impl Render for Compose {
|
||||
.gap_1()
|
||||
.child(
|
||||
div()
|
||||
.px_2()
|
||||
.text_xs()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
|
||||
.child(DESCRIPTION),
|
||||
)
|
||||
.when_some(self.error_message.read(cx).as_ref(), |this, msg| {
|
||||
this.child(
|
||||
div()
|
||||
.px_2()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().danger)
|
||||
.child(msg.clone()),
|
||||
@@ -362,13 +357,12 @@ impl Render for Compose {
|
||||
div().flex().flex_col().child(
|
||||
div()
|
||||
.h_10()
|
||||
.px_2()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().base.step(cx, ColorScaleStep::FIVE))
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.child(div().text_xs().font_semibold().child("Title:"))
|
||||
.child(div().pb_0p5().text_sm().font_semibold().child("Title:"))
|
||||
.child(self.title_input.clone()),
|
||||
),
|
||||
)
|
||||
@@ -377,25 +371,9 @@ impl Render for Compose {
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child(div().px_2().text_xs().font_semibold().child("To:"))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.px_2()
|
||||
.child(
|
||||
Button::new("add_user_to_compose_btn")
|
||||
.icon(IconName::Plus)
|
||||
.small()
|
||||
.rounded(ButtonRounded::Size(px(9999.)))
|
||||
.loading(self.is_loading)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.add(window, cx);
|
||||
})),
|
||||
)
|
||||
.child(self.user_input.clone()),
|
||||
)
|
||||
.mt_1()
|
||||
.child(div().text_sm().font_semibold().child("To:"))
|
||||
.child(self.user_input.clone())
|
||||
.map(|this| {
|
||||
let contacts = self.contacts.read(cx).clone();
|
||||
let view = cx.entity();
|
||||
@@ -444,30 +422,35 @@ impl Render for Compose {
|
||||
div()
|
||||
.id(ix)
|
||||
.w_full()
|
||||
.h_9()
|
||||
.h_10()
|
||||
.px_2()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.rounded(px(cx.theme().radius))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.text_xs()
|
||||
.child(div().flex_shrink_0().child(
|
||||
img(item.shared_avatar()).size_6(),
|
||||
))
|
||||
.gap_3()
|
||||
.text_sm()
|
||||
.child(
|
||||
img(item.shared_avatar())
|
||||
.size_7()
|
||||
.flex_shrink_0(),
|
||||
)
|
||||
.child(item.shared_name()),
|
||||
)
|
||||
.when(is_select, |this| {
|
||||
this.child(
|
||||
Icon::new(IconName::CircleCheck)
|
||||
.size_3()
|
||||
.text_color(cx.theme().base.step(
|
||||
cx,
|
||||
ColorScaleStep::TWELVE,
|
||||
)),
|
||||
Icon::new(IconName::CheckCircleFill)
|
||||
.small()
|
||||
.text_color(
|
||||
cx.theme().accent.step(
|
||||
cx,
|
||||
ColorScaleStep::NINE,
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
.hover(|this| {
|
||||
@@ -490,71 +473,22 @@ impl Render for Compose {
|
||||
items
|
||||
},
|
||||
)
|
||||
.min_h(px(250.)),
|
||||
.pb_4()
|
||||
.min_h(px(280.)),
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type Handler = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ComposeButton {
|
||||
base: Div,
|
||||
label: SharedString,
|
||||
handler: Handler,
|
||||
}
|
||||
|
||||
impl ComposeButton {
|
||||
pub fn new(label: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
base: div(),
|
||||
label: label.into(),
|
||||
handler: Rc::new(|_, _, _| {}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_click(
|
||||
mut self,
|
||||
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.handler = Rc::new(handler);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ComposeButton {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let handler = self.handler.clone();
|
||||
|
||||
self.base
|
||||
.id("compose")
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.px_1()
|
||||
.h_7()
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.rounded(px(cx.theme().radius))
|
||||
.hover(|this| this.bg(cx.theme().base.step(cx, ColorScaleStep::THREE)))
|
||||
.child(
|
||||
div()
|
||||
.size_6()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.rounded_full()
|
||||
.bg(cx.theme().accent.step(cx, ColorScaleStep::NINE))
|
||||
.child(
|
||||
Icon::new(IconName::ComposeFill)
|
||||
.small()
|
||||
.text_color(cx.theme().base.darken(cx)),
|
||||
),
|
||||
div().mt_2().child(
|
||||
Button::new("create_dm_btn")
|
||||
.label(label)
|
||||
.primary()
|
||||
.w_full()
|
||||
.loading(self.is_submitting)
|
||||
.disabled(self.is_submitting)
|
||||
.on_click(cx.listener(|this, _, window, cx| this.compose(window, cx))),
|
||||
),
|
||||
)
|
||||
.child(self.label.clone())
|
||||
.on_click(move |ev, window, cx| handler(ev, window, cx))
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,8 @@ use ui::{
|
||||
Sizable,
|
||||
};
|
||||
|
||||
const MIN_HEIGHT: f32 = 280.;
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Contacts> {
|
||||
Contacts::new(window, cx)
|
||||
}
|
||||
@@ -108,51 +110,56 @@ impl Focusable for Contacts {
|
||||
|
||||
impl Render for Contacts {
|
||||
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div().size_full().pt_2().px_2().map(|this| {
|
||||
let entity = cx.entity().clone();
|
||||
|
||||
div().map(|this| {
|
||||
if let Some(contacts) = self.contacts.clone() {
|
||||
this.child(
|
||||
uniform_list(
|
||||
cx.entity().clone(),
|
||||
entity,
|
||||
"contacts",
|
||||
contacts.len(),
|
||||
move |_, range, _window, cx| {
|
||||
let mut items = Vec::new();
|
||||
let mut items = Vec::with_capacity(contacts.len());
|
||||
|
||||
for ix in range {
|
||||
let item = contacts.get(ix).unwrap().clone();
|
||||
|
||||
items.push(
|
||||
div()
|
||||
.w_full()
|
||||
.h_9()
|
||||
.px_2()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.rounded(px(cx.theme().radius))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.text_xs()
|
||||
.child(
|
||||
div()
|
||||
.flex_shrink_0()
|
||||
.child(img(item.shared_avatar()).size_6()),
|
||||
)
|
||||
.child(item.shared_name()),
|
||||
)
|
||||
.hover(|this| {
|
||||
this.bg(cx.theme().base.step(cx, ColorScaleStep::THREE))
|
||||
}),
|
||||
);
|
||||
if let Some(item) = contacts.get(ix) {
|
||||
items.push(
|
||||
div()
|
||||
.w_full()
|
||||
.h_9()
|
||||
.px_2()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.rounded(px(cx.theme().radius))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.text_xs()
|
||||
.child(
|
||||
div().flex_shrink_0().child(
|
||||
img(item.shared_avatar()).size_6(),
|
||||
),
|
||||
)
|
||||
.child(item.shared_name()),
|
||||
)
|
||||
.hover(|this| {
|
||||
this.bg(cx
|
||||
.theme()
|
||||
.base
|
||||
.step(cx, ColorScaleStep::THREE))
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
items
|
||||
},
|
||||
)
|
||||
.h_full(),
|
||||
.min_h(px(MIN_HEIGHT)),
|
||||
)
|
||||
} else {
|
||||
this.flex()
|
||||
|
||||
@@ -17,13 +17,9 @@ use ui::{
|
||||
notification::Notification,
|
||||
popup_menu::PopupMenu,
|
||||
theme::{scale::ColorScaleStep, ActiveTheme},
|
||||
ContextModal, Disableable, Icon, IconName, Sizable, Size, StyledExt,
|
||||
ContextModal, Disableable, Sizable, Size, StyledExt,
|
||||
};
|
||||
|
||||
use crate::chat_space::ChatSpace;
|
||||
|
||||
use super::onboarding;
|
||||
|
||||
const INPUT_INVALID: &str = "You must provide a valid Private Key or Bunker.";
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Login> {
|
||||
@@ -60,12 +56,12 @@ impl Login {
|
||||
|
||||
let key_input = cx.new(|cx| {
|
||||
TextInput::new(window, cx)
|
||||
.text_size(Size::XSmall)
|
||||
.text_size(Size::Small)
|
||||
.placeholder("nsec... or bunker://...")
|
||||
});
|
||||
|
||||
let connect_relay = cx.new(|cx| {
|
||||
let mut input = TextInput::new(window, cx).text_size(Size::XSmall).small();
|
||||
let mut input = TextInput::new(window, cx).text_size(Size::Small).small();
|
||||
input.set_text("wss://relay.nsec.app", window, cx);
|
||||
input
|
||||
});
|
||||
@@ -233,11 +229,6 @@ impl Login {
|
||||
self.is_logging_in = status;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn back(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let panel = onboarding::init(window, cx);
|
||||
ChatSpace::set_center_panel(panel, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
impl Panel for Login {
|
||||
@@ -299,14 +290,13 @@ impl Render for Login {
|
||||
.child(
|
||||
div()
|
||||
.text_center()
|
||||
.text_lg()
|
||||
.text_xl()
|
||||
.font_semibold()
|
||||
.line_height(relative(1.2))
|
||||
.line_height(relative(1.3))
|
||||
.child("Welcome Back!"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(
|
||||
cx.theme().base.step(cx, ColorScaleStep::ELEVEN),
|
||||
)
|
||||
@@ -365,7 +355,6 @@ impl Render for Login {
|
||||
.text_center()
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.font_semibold()
|
||||
.line_height(relative(1.2))
|
||||
.text_color(
|
||||
@@ -375,7 +364,7 @@ impl Render for Login {
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_sm()
|
||||
.text_color(
|
||||
cx.theme().base.step(cx, ColorScaleStep::ELEVEN),
|
||||
)
|
||||
@@ -424,17 +413,5 @@ impl Render for Login {
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div().absolute().left_2().top_10().w_16().child(
|
||||
Button::new("back")
|
||||
.label("Back")
|
||||
.icon(Icon::new(IconName::ArrowLeft))
|
||||
.ghost()
|
||||
.small()
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.back(window, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
pub mod chat;
|
||||
pub mod compose;
|
||||
pub mod contacts;
|
||||
pub mod login;
|
||||
pub mod new_account;
|
||||
pub mod onboarding;
|
||||
pub mod profile;
|
||||
pub mod relays;
|
||||
pub mod settings;
|
||||
pub mod sidebar;
|
||||
pub mod welcome;
|
||||
|
||||
@@ -19,10 +19,6 @@ use ui::{
|
||||
Disableable, Icon, IconName, Sizable, Size, StyledExt,
|
||||
};
|
||||
|
||||
use crate::chat_space::ChatSpace;
|
||||
|
||||
use super::onboarding;
|
||||
|
||||
const STEAM_ID_DESCRIPTION: &str =
|
||||
"Steam ID is used to get your currently playing game and update your status.";
|
||||
|
||||
@@ -52,26 +48,26 @@ impl NewAccount {
|
||||
fn view(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let name_input = cx.new(|cx| {
|
||||
TextInput::new(window, cx)
|
||||
.text_size(Size::XSmall)
|
||||
.text_size(Size::Small)
|
||||
.placeholder("Alice")
|
||||
});
|
||||
|
||||
let avatar_input = cx.new(|cx| {
|
||||
TextInput::new(window, cx)
|
||||
.text_size(Size::XSmall)
|
||||
.text_size(Size::Small)
|
||||
.small()
|
||||
.placeholder("https://example.com/avatar.jpg")
|
||||
});
|
||||
|
||||
let steam_input = cx.new(|cx| {
|
||||
TextInput::new(window, cx)
|
||||
.text_size(Size::XSmall)
|
||||
.text_size(Size::Small)
|
||||
.placeholder("76561199810385277")
|
||||
});
|
||||
|
||||
let bio_input = cx.new(|cx| {
|
||||
TextInput::new(window, cx)
|
||||
.text_size(Size::XSmall)
|
||||
.text_size(Size::Small)
|
||||
.multi_line()
|
||||
.placeholder("A short introduce about you.")
|
||||
});
|
||||
@@ -188,11 +184,6 @@ impl NewAccount {
|
||||
self.is_uploading = status;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn back(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let panel = onboarding::init(window, cx);
|
||||
ChatSpace::set_center_panel(panel, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
impl Panel for NewAccount {
|
||||
@@ -238,13 +229,13 @@ impl Render for NewAccount {
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.gap_8()
|
||||
.gap_10()
|
||||
.child(
|
||||
div()
|
||||
.text_center()
|
||||
.text_lg()
|
||||
.font_semibold()
|
||||
.line_height(relative(1.2))
|
||||
.line_height(relative(1.3))
|
||||
.child("Create New Account"),
|
||||
)
|
||||
.child(
|
||||
@@ -295,7 +286,7 @@ impl Render for NewAccount {
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.text_xs()
|
||||
.text_sm()
|
||||
.child("Name *:")
|
||||
.child(self.name_input.clone()),
|
||||
)
|
||||
@@ -304,7 +295,7 @@ impl Render for NewAccount {
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.text_xs()
|
||||
.text_sm()
|
||||
.child("Bio:")
|
||||
.child(self.bio_input.clone()),
|
||||
)
|
||||
@@ -313,7 +304,7 @@ impl Render for NewAccount {
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.text_xs()
|
||||
.text_sm()
|
||||
.child("Steam ID:")
|
||||
.child(self.steam_input.clone())
|
||||
.child(
|
||||
@@ -341,17 +332,5 @@ impl Render for NewAccount {
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div().absolute().left_2().top_10().w_16().child(
|
||||
Button::new("back")
|
||||
.label("Back")
|
||||
.icon(Icon::new(IconName::ArrowLeft))
|
||||
.ghost()
|
||||
.small()
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.back(window, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ use super::{login, new_account};
|
||||
|
||||
const LOGO_URL: &str = "brand/coop.svg";
|
||||
const TITLE: &str = "Welcome to Coop!";
|
||||
const SUBTITLE: &str = "a Nostr client for secure communication.";
|
||||
const SUBTITLE: &str = "Secure Communication on Nostr.";
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Onboarding> {
|
||||
Onboarding::new(window, cx)
|
||||
@@ -97,7 +97,7 @@ impl Render for Onboarding {
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.gap_8()
|
||||
.gap_10()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
@@ -107,7 +107,7 @@ impl Render for Onboarding {
|
||||
.child(
|
||||
svg()
|
||||
.path(LOGO_URL)
|
||||
.size_12()
|
||||
.size_16()
|
||||
.text_color(cx.theme().base.step(cx, ColorScaleStep::THREE)),
|
||||
)
|
||||
.child(
|
||||
@@ -115,14 +115,13 @@ impl Render for Onboarding {
|
||||
.text_center()
|
||||
.child(
|
||||
div()
|
||||
.text_lg()
|
||||
.text_xl()
|
||||
.font_semibold()
|
||||
.line_height(relative(1.2))
|
||||
.line_height(relative(1.3))
|
||||
.child(TITLE),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
|
||||
.child(SUBTITLE),
|
||||
),
|
||||
|
||||
@@ -2,46 +2,38 @@ use async_utility::task::spawn;
|
||||
use common::nip96_upload;
|
||||
use global::{constants::IMAGE_SERVICE, get_client};
|
||||
use gpui::{
|
||||
div, img, prelude::FluentBuilder, AnyElement, App, AppContext, Context, Entity, EventEmitter,
|
||||
Flatten, FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions, Render,
|
||||
SharedString, Styled, Task, Window,
|
||||
div, img, prelude::FluentBuilder, px, App, AppContext, Context, Entity, Flatten, IntoElement,
|
||||
ParentElement, PathPromptOptions, Render, Styled, Task, Window,
|
||||
};
|
||||
use nostr_sdk::prelude::*;
|
||||
use smol::fs;
|
||||
use std::{str::FromStr, sync::Arc, time::Duration};
|
||||
use std::{str::FromStr, time::Duration};
|
||||
use ui::{
|
||||
button::{Button, ButtonVariants},
|
||||
dock_area::panel::{Panel, PanelEvent},
|
||||
input::TextInput,
|
||||
popup_menu::PopupMenu,
|
||||
ContextModal, Disableable, Sizable, Size,
|
||||
theme::{scale::ColorScaleStep, ActiveTheme},
|
||||
ContextModal, Disableable, IconName, Sizable, Size,
|
||||
};
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Arc<Entity<Profile>> {
|
||||
Arc::new(Profile::new(window, cx))
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Profile> {
|
||||
Profile::new(window, cx)
|
||||
}
|
||||
|
||||
pub struct Profile {
|
||||
profile: Option<Metadata>,
|
||||
// Form
|
||||
name_input: Entity<TextInput>,
|
||||
avatar_input: Entity<TextInput>,
|
||||
bio_input: Entity<TextInput>,
|
||||
website_input: Entity<TextInput>,
|
||||
is_loading: bool,
|
||||
is_submitting: bool,
|
||||
// Panel
|
||||
name: SharedString,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
impl Profile {
|
||||
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||
let window_handle = window.window_handle();
|
||||
|
||||
let name_input = cx.new(|cx| {
|
||||
TextInput::new(window, cx)
|
||||
.text_size(Size::XSmall)
|
||||
.text_size(Size::Small)
|
||||
.placeholder("Alice")
|
||||
});
|
||||
|
||||
@@ -54,13 +46,13 @@ impl Profile {
|
||||
|
||||
let website_input = cx.new(|cx| {
|
||||
TextInput::new(window, cx)
|
||||
.text_size(Size::XSmall)
|
||||
.text_size(Size::Small)
|
||||
.placeholder("https://your-website.com")
|
||||
});
|
||||
|
||||
let bio_input = cx.new(|cx| {
|
||||
TextInput::new(window, cx)
|
||||
.text_size(Size::XSmall)
|
||||
.text_size(Size::Small)
|
||||
.multi_line()
|
||||
.placeholder("A short introduce about you.")
|
||||
});
|
||||
@@ -74,8 +66,6 @@ impl Profile {
|
||||
profile: None,
|
||||
is_loading: false,
|
||||
is_submitting: false,
|
||||
name: "Profile".into(),
|
||||
focus_handle: cx.focus_handle(),
|
||||
};
|
||||
|
||||
let task: Task<Result<Option<Metadata>, Error>> = cx.background_spawn(async move {
|
||||
@@ -89,10 +79,10 @@ impl Profile {
|
||||
Ok(metadata)
|
||||
});
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
if let Ok(Some(metadata)) = task.await {
|
||||
_ = cx.update_window(window_handle, |_, window, cx| {
|
||||
_ = this.update(cx, |this: &mut Profile, cx| {
|
||||
cx.update(|window, cx| {
|
||||
this.update(cx, |this: &mut Profile, cx| {
|
||||
this.avatar_input.update(cx, |this, cx| {
|
||||
if let Some(avatar) = metadata.picture.as_ref() {
|
||||
this.set_text(avatar, window, cx);
|
||||
@@ -114,9 +104,12 @@ impl Profile {
|
||||
}
|
||||
});
|
||||
this.profile = Some(metadata);
|
||||
|
||||
cx.notify();
|
||||
});
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
@@ -127,7 +120,6 @@ impl Profile {
|
||||
|
||||
fn upload(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let avatar_input = self.avatar_input.downgrade();
|
||||
let window_handle = window.window_handle();
|
||||
let paths = cx.prompt_for_paths(PathPromptOptions {
|
||||
files: true,
|
||||
directories: false,
|
||||
@@ -137,7 +129,7 @@ impl Profile {
|
||||
// Show loading spinner
|
||||
self.set_loading(true, cx);
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
match Flatten::flatten(paths.await.map_err(|e| e.into())) {
|
||||
Ok(Some(mut paths)) => {
|
||||
let path = paths.pop().unwrap();
|
||||
@@ -153,32 +145,33 @@ impl Profile {
|
||||
});
|
||||
|
||||
if let Ok(url) = rx.await {
|
||||
cx.update_window(window_handle, |_, window, cx| {
|
||||
cx.update(|window, cx| {
|
||||
// Stop loading spinner
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_loading(false, cx);
|
||||
})
|
||||
.unwrap();
|
||||
.ok();
|
||||
|
||||
// Set avatar input
|
||||
avatar_input
|
||||
.update(cx, |this, cx| {
|
||||
this.set_text(url.to_string(), window, cx);
|
||||
})
|
||||
.unwrap();
|
||||
.ok();
|
||||
})
|
||||
.unwrap();
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
// Stop loading spinner
|
||||
if let Some(view) = this.upgrade() {
|
||||
cx.update_entity(&view, |this, cx| {
|
||||
cx.update(|_, cx| {
|
||||
// Stop loading spinner
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_loading(false, cx);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
.ok();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
@@ -186,11 +179,6 @@ impl Profile {
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||
self.is_loading = status;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn submit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
// Show loading spinner
|
||||
self.set_submitting(true, cx);
|
||||
@@ -216,82 +204,57 @@ impl Profile {
|
||||
new_metadata = new_metadata.website(url);
|
||||
}
|
||||
|
||||
let window_handle = window.window_handle();
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||
let client = get_client();
|
||||
let (tx, rx) = oneshot::channel::<EventId>();
|
||||
_ = client.set_metadata(&new_metadata).await?;
|
||||
|
||||
cx.background_spawn(async move {
|
||||
if let Ok(output) = client.set_metadata(&new_metadata).await {
|
||||
_ = tx.send(output.val);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
Ok(())
|
||||
});
|
||||
|
||||
if rx.await.is_ok() {
|
||||
cx.update_window(window_handle, |_, window, cx| {
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
if task.await.is_ok() {
|
||||
cx.update(|window, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_submitting(false, cx);
|
||||
window.push_notification("Your profile has been updated successfully", cx);
|
||||
})
|
||||
.unwrap()
|
||||
.ok();
|
||||
})
|
||||
.unwrap();
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||
self.is_loading = status;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn set_submitting(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||
self.is_submitting = status;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl Panel for Profile {
|
||||
fn panel_id(&self) -> SharedString {
|
||||
"ProfilePanel".into()
|
||||
}
|
||||
|
||||
fn title(&self, _cx: &App) -> AnyElement {
|
||||
self.name.clone().into_any_element()
|
||||
}
|
||||
|
||||
fn popup_menu(&self, menu: PopupMenu, _cx: &App) -> PopupMenu {
|
||||
menu.track_focus(&self.focus_handle)
|
||||
}
|
||||
|
||||
fn toolbar_buttons(&self, _window: &Window, _cx: &App) -> Vec<Button> {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<PanelEvent> for Profile {}
|
||||
|
||||
impl Focusable for Profile {
|
||||
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Profile {
|
||||
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.size_full()
|
||||
.px_2()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.h_32()
|
||||
.bg(cx.theme().base.step(cx, ColorScaleStep::TWO))
|
||||
.rounded(px(cx.theme().radius))
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.justify_end()
|
||||
.justify_center()
|
||||
.gap_2()
|
||||
.w_full()
|
||||
.h_24()
|
||||
.map(|this| {
|
||||
let picture = self.avatar_input.read(cx).text();
|
||||
|
||||
@@ -310,23 +273,16 @@ impl Render for Profile {
|
||||
}
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_1()
|
||||
.items_center()
|
||||
.w_full()
|
||||
.child(self.avatar_input.clone())
|
||||
.child(
|
||||
Button::new("upload")
|
||||
.label("Upload")
|
||||
.ghost()
|
||||
.small()
|
||||
.disabled(self.is_submitting)
|
||||
.loading(self.is_loading)
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.upload(window, cx);
|
||||
})),
|
||||
),
|
||||
Button::new("upload")
|
||||
.icon(IconName::Upload)
|
||||
.label("Change")
|
||||
.ghost()
|
||||
.small()
|
||||
.disabled(self.is_loading || self.is_submitting)
|
||||
.loading(self.is_loading)
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.upload(window, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
@@ -334,7 +290,7 @@ impl Render for Profile {
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.text_xs()
|
||||
.text_sm()
|
||||
.child("Name:")
|
||||
.child(self.name_input.clone()),
|
||||
)
|
||||
@@ -343,26 +299,25 @@ impl Render for Profile {
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.text_xs()
|
||||
.child("Bio:")
|
||||
.child(self.bio_input.clone()),
|
||||
.text_sm()
|
||||
.child("Website:")
|
||||
.child(self.website_input.clone()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.text_xs()
|
||||
.child("Website:")
|
||||
.child(self.website_input.clone()),
|
||||
.text_sm()
|
||||
.child("Bio:")
|
||||
.child(self.bio_input.clone()),
|
||||
)
|
||||
.child(
|
||||
div().flex().items_center().justify_end().child(
|
||||
div().mt_2().w_full().child(
|
||||
Button::new("submit")
|
||||
.label("Update")
|
||||
.primary()
|
||||
.small()
|
||||
.disabled(self.is_loading)
|
||||
.disabled(self.is_loading || self.is_submitting)
|
||||
.loading(self.is_submitting)
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.submit(window, cx);
|
||||
|
||||
@@ -3,7 +3,7 @@ use global::{constants::NEW_MESSAGE_SUB_ID, get_client};
|
||||
use gpui::{
|
||||
div, prelude::FluentBuilder, px, uniform_list, App, AppContext, Context, Entity, FocusHandle,
|
||||
InteractiveElement, IntoElement, ParentElement, Render, Styled, Subscription, Task, TextAlign,
|
||||
Window,
|
||||
UniformList, Window,
|
||||
};
|
||||
use nostr_sdk::prelude::*;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
@@ -11,10 +11,11 @@ use ui::{
|
||||
button::{Button, ButtonVariants},
|
||||
input::{InputEvent, TextInput},
|
||||
theme::{scale::ColorScaleStep, ActiveTheme},
|
||||
ContextModal, IconName, Sizable,
|
||||
ContextModal, Disableable, IconName, Sizable,
|
||||
};
|
||||
|
||||
const MESSAGE: &str = "In order to receive messages from others, you need to setup Messaging Relays. You can use the recommend relays or add more.";
|
||||
const MIN_HEIGHT: f32 = 200.0;
|
||||
const MESSAGE: &str = "In order to receive messages from others, you need to setup at least one Messaging Relay. You can use the recommend relays or add more.";
|
||||
const HELP_TEXT: &str = "Please add some relays.";
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Relays> {
|
||||
@@ -32,7 +33,12 @@ pub struct Relays {
|
||||
|
||||
impl Relays {
|
||||
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||
let client = get_client();
|
||||
let input = cx.new(|cx| {
|
||||
TextInput::new(window, cx)
|
||||
.text_size(ui::Size::XSmall)
|
||||
.small()
|
||||
.placeholder("wss://example.com")
|
||||
});
|
||||
|
||||
let relays = cx.new(|cx| {
|
||||
let relays = vec![
|
||||
@@ -41,6 +47,7 @@ impl Relays {
|
||||
];
|
||||
|
||||
let task: Task<Result<Vec<RelayUrl>, Error>> = cx.background_spawn(async move {
|
||||
let client = get_client();
|
||||
let signer = client.signer().await?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
|
||||
@@ -82,13 +89,6 @@ impl Relays {
|
||||
relays
|
||||
});
|
||||
|
||||
let input = cx.new(|cx| {
|
||||
TextInput::new(window, cx)
|
||||
.text_size(ui::Size::XSmall)
|
||||
.small()
|
||||
.placeholder("wss://example.com")
|
||||
});
|
||||
|
||||
cx.new(|cx| {
|
||||
let mut subscriptions = smallvec![];
|
||||
|
||||
@@ -113,11 +113,9 @@ impl Relays {
|
||||
}
|
||||
|
||||
pub fn update(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
// Show loading spinner
|
||||
self.set_loading(true, cx);
|
||||
|
||||
let relays = self.relays.read(cx).clone();
|
||||
|
||||
let task: Task<Result<EventId, Error>> = cx.background_spawn(async move {
|
||||
let client = get_client();
|
||||
let signer = client.signer().await?;
|
||||
@@ -189,10 +187,6 @@ impl Relays {
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn loading(&self) -> bool {
|
||||
self.is_loading
|
||||
}
|
||||
|
||||
fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||
self.is_loading = status;
|
||||
cx.notify();
|
||||
@@ -225,116 +219,136 @@ impl Relays {
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
|
||||
fn render_list(
|
||||
&mut self,
|
||||
relays: Vec<RelayUrl>,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> UniformList {
|
||||
let view = cx.entity();
|
||||
let total = relays.len();
|
||||
|
||||
uniform_list(view, "relays", total, move |_, range, _window, cx| {
|
||||
let mut items = Vec::new();
|
||||
|
||||
for ix in range {
|
||||
let item = relays.get(ix).unwrap().clone().to_string();
|
||||
|
||||
items.push(
|
||||
div().group("").w_full().h_9().py_0p5().child(
|
||||
div()
|
||||
.px_2()
|
||||
.h_full()
|
||||
.w_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.rounded(px(cx.theme().radius))
|
||||
.bg(cx.theme().base.step(cx, ColorScaleStep::THREE))
|
||||
.text_xs()
|
||||
.child(item)
|
||||
.child(
|
||||
Button::new("remove_{ix}")
|
||||
.icon(IconName::Close)
|
||||
.xsmall()
|
||||
.ghost()
|
||||
.invisible()
|
||||
.group_hover("", |this| this.visible())
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.remove(ix, window, cx)
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
items
|
||||
})
|
||||
.w_full()
|
||||
.min_h(px(MIN_HEIGHT))
|
||||
}
|
||||
|
||||
fn render_empty(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.h_20()
|
||||
.mb_2()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.text_sm()
|
||||
.text_align(TextAlign::Center)
|
||||
.child(HELP_TEXT)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Relays {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.track_focus(&self.focus_handle)
|
||||
.w_full()
|
||||
.h_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(
|
||||
div()
|
||||
.px_2()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
|
||||
.child(MESSAGE),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.px_2()
|
||||
.w_full()
|
||||
.flex_1()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.child(self.input.clone())
|
||||
.child(
|
||||
Button::new("add_relay_btn")
|
||||
.icon(IconName::Plus)
|
||||
.small()
|
||||
.rounded(px(cx.theme().radius))
|
||||
.on_click(
|
||||
cx.listener(|this, _, window, cx| this.add(window, cx)),
|
||||
),
|
||||
),
|
||||
.text_sm()
|
||||
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
|
||||
.child(MESSAGE),
|
||||
)
|
||||
.map(|this| {
|
||||
let view = cx.entity();
|
||||
let relays = self.relays.read(cx).clone();
|
||||
let total = relays.len();
|
||||
|
||||
if !relays.is_empty() {
|
||||
this.child(
|
||||
uniform_list(
|
||||
view,
|
||||
"relays",
|
||||
total,
|
||||
move |_, range, _window, cx| {
|
||||
let mut items = Vec::new();
|
||||
|
||||
for ix in range {
|
||||
let item = relays.get(ix).unwrap().clone().to_string();
|
||||
|
||||
items.push(
|
||||
div().group("").w_full().h_9().py_0p5().child(
|
||||
div()
|
||||
.px_2()
|
||||
.h_full()
|
||||
.w_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.rounded(px(cx.theme().radius))
|
||||
.bg(cx
|
||||
.theme()
|
||||
.base
|
||||
.step(cx, ColorScaleStep::THREE))
|
||||
.text_xs()
|
||||
.child(item)
|
||||
.child(
|
||||
Button::new("remove_{ix}")
|
||||
.icon(IconName::Close)
|
||||
.xsmall()
|
||||
.ghost()
|
||||
.invisible()
|
||||
.group_hover("", |this| {
|
||||
this.visible()
|
||||
})
|
||||
.on_click(cx.listener(
|
||||
move |this, _, window, cx| {
|
||||
this.remove(ix, window, cx)
|
||||
},
|
||||
)),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
items
|
||||
},
|
||||
)
|
||||
.w_full()
|
||||
.min_h(px(120.)),
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.child(self.input.clone())
|
||||
.child(
|
||||
Button::new("add_relay_btn")
|
||||
.icon(IconName::Plus)
|
||||
.label("Add")
|
||||
.small()
|
||||
.ghost()
|
||||
.rounded(px(cx.theme().radius))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.add(window, cx)
|
||||
})),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
this.h_20()
|
||||
.mb_2()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.text_xs()
|
||||
.text_align(TextAlign::Center)
|
||||
.child(HELP_TEXT)
|
||||
}
|
||||
}),
|
||||
.map(|this| {
|
||||
let relays = self.relays.read(cx).clone();
|
||||
|
||||
if !relays.is_empty() {
|
||||
this.child(self.render_list(relays, window, cx))
|
||||
} else {
|
||||
this.child(self.render_empty(window, cx))
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Button::new("submti")
|
||||
.label("Update")
|
||||
.primary()
|
||||
.w_full()
|
||||
.loading(self.is_loading)
|
||||
.disabled(self.is_loading)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.update(window, cx);
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
use gpui::{
|
||||
div, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
IntoElement, ParentElement, Render, SharedString, Styled, Window,
|
||||
};
|
||||
use ui::{
|
||||
button::Button,
|
||||
dock_area::panel::{Panel, PanelEvent},
|
||||
popup_menu::PopupMenu,
|
||||
};
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Settings> {
|
||||
Settings::new(window, cx)
|
||||
}
|
||||
|
||||
pub struct Settings {
|
||||
name: SharedString,
|
||||
closable: bool,
|
||||
zoomable: bool,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
pub fn new(_window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||
cx.new(|cx| Self {
|
||||
name: "Settings".into(),
|
||||
closable: true,
|
||||
zoomable: true,
|
||||
focus_handle: cx.focus_handle(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Panel for Settings {
|
||||
fn panel_id(&self) -> SharedString {
|
||||
"SettingsPanel".into()
|
||||
}
|
||||
|
||||
fn title(&self, _cx: &App) -> AnyElement {
|
||||
self.name.clone().into_any_element()
|
||||
}
|
||||
|
||||
fn closable(&self, _cx: &App) -> bool {
|
||||
self.closable
|
||||
}
|
||||
|
||||
fn zoomable(&self, _cx: &App) -> bool {
|
||||
self.zoomable
|
||||
}
|
||||
|
||||
fn popup_menu(&self, menu: PopupMenu, _cx: &App) -> PopupMenu {
|
||||
menu.track_focus(&self.focus_handle)
|
||||
}
|
||||
|
||||
fn toolbar_buttons(&self, _window: &Window, _cx: &App) -> Vec<Button> {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<PanelEvent> for Settings {}
|
||||
|
||||
impl Focusable for Settings {
|
||||
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Settings {
|
||||
fn render(&mut self, _window: &mut gpui::Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.size_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.child("Settings")
|
||||
}
|
||||
}
|
||||
58
crates/coop/src/views/sidebar/button.rs
Normal file
58
crates/coop/src/views/sidebar/button.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
div, prelude::FluentBuilder, px, App, ClickEvent, Div, InteractiveElement, IntoElement,
|
||||
ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window,
|
||||
};
|
||||
use ui::{
|
||||
theme::{scale::ColorScaleStep, ActiveTheme},
|
||||
Icon,
|
||||
};
|
||||
|
||||
type Handler = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct SidebarButton {
|
||||
base: Div,
|
||||
label: SharedString,
|
||||
icon: Option<Icon>,
|
||||
handler: Handler,
|
||||
}
|
||||
|
||||
impl SidebarButton {
|
||||
pub fn new(label: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
base: div().flex().items_center().gap_3().px_3().h_8(),
|
||||
label: label.into(),
|
||||
icon: None,
|
||||
handler: Rc::new(|_, _, _| {}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
|
||||
self.icon = Some(icon.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_click(
|
||||
mut self,
|
||||
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.handler = Rc::new(handler);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for SidebarButton {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let handler = self.handler.clone();
|
||||
|
||||
self.base
|
||||
.id(self.label.clone())
|
||||
.rounded(px(cx.theme().radius))
|
||||
.when_some(self.icon, |this, icon| this.child(icon))
|
||||
.child(self.label.clone())
|
||||
.hover(|this| this.bg(cx.theme().base.step(cx, ColorScaleStep::THREE)))
|
||||
.on_click(move |ev, window, cx| handler(ev, window, cx))
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,21 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
div, prelude::FluentBuilder, px, App, ClickEvent, Img, InteractiveElement, IntoElement,
|
||||
ParentElement as _, RenderOnce, SharedString, StatefulInteractiveElement, Styled as _, Window,
|
||||
div, percentage, prelude::FluentBuilder, px, App, ClickEvent, Div, Img, InteractiveElement,
|
||||
IntoElement, ParentElement as _, RenderOnce, SharedString, StatefulInteractiveElement, Styled,
|
||||
Window,
|
||||
};
|
||||
use ui::{
|
||||
theme::{scale::ColorScaleStep, ActiveTheme},
|
||||
Collapsible, Icon, IconName, StyledExt,
|
||||
Collapsible, Icon, IconName, Sizable, StyledExt,
|
||||
};
|
||||
|
||||
type Handler = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct Parent {
|
||||
base: Div,
|
||||
icon: Option<Icon>,
|
||||
active_icon: Option<Icon>,
|
||||
label: SharedString,
|
||||
items: Vec<Folder>,
|
||||
collapsed: bool,
|
||||
@@ -24,9 +25,9 @@ pub struct Parent {
|
||||
impl Parent {
|
||||
pub fn new(label: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
base: div().flex().flex_col().gap_2(),
|
||||
label: label.into(),
|
||||
icon: None,
|
||||
active_icon: None,
|
||||
items: Vec::new(),
|
||||
collapsed: false,
|
||||
handler: Rc::new(|_, _, _| {}),
|
||||
@@ -38,11 +39,6 @@ impl Parent {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn active_icon(mut self, icon: impl Into<Icon>) -> Self {
|
||||
self.active_icon = Some(icon.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn collapsed(mut self, collapsed: bool) -> Self {
|
||||
self.collapsed = collapsed;
|
||||
self
|
||||
@@ -83,10 +79,7 @@ impl RenderOnce for Parent {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let handler = self.handler.clone();
|
||||
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
self.base
|
||||
.child(
|
||||
div()
|
||||
.id(self.label.clone())
|
||||
@@ -94,36 +87,37 @@ impl RenderOnce for Parent {
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.px_2()
|
||||
.h_6()
|
||||
.h_8()
|
||||
.rounded(px(cx.theme().radius))
|
||||
.text_xs()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
|
||||
.font_semibold()
|
||||
.when_some(self.icon, |this, icon| {
|
||||
this.map(|this| {
|
||||
if self.collapsed {
|
||||
this.child(icon.size_4())
|
||||
} else {
|
||||
this.when_some(self.active_icon, |this, icon| {
|
||||
this.child(icon.size_4())
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
.child(self.label.clone())
|
||||
.font_medium()
|
||||
.child(
|
||||
Icon::new(IconName::CaretDown)
|
||||
.xsmall()
|
||||
.when(self.collapsed, |this| this.rotate(percentage(270. / 360.))),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.when_some(self.icon, |this, icon| this.child(icon.small()))
|
||||
.child(self.label.clone()),
|
||||
)
|
||||
.hover(|this| this.bg(cx.theme().base.step(cx, ColorScaleStep::THREE)))
|
||||
.on_click(move |ev, window, cx| handler(ev, window, cx)),
|
||||
)
|
||||
.when(!self.collapsed, |this| {
|
||||
this.child(div().flex().flex_col().gap_1().pl_3().children(self.items))
|
||||
this.child(div().flex().flex_col().gap_2().pl_3().children(self.items))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct Folder {
|
||||
base: Div,
|
||||
icon: Option<Icon>,
|
||||
active_icon: Option<Icon>,
|
||||
label: SharedString,
|
||||
items: Vec<FolderItem>,
|
||||
collapsed: bool,
|
||||
@@ -133,9 +127,9 @@ pub struct Folder {
|
||||
impl Folder {
|
||||
pub fn new(label: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
base: div().flex().flex_col().gap_2(),
|
||||
label: label.into(),
|
||||
icon: None,
|
||||
active_icon: None,
|
||||
items: Vec::new(),
|
||||
collapsed: false,
|
||||
handler: Rc::new(|_, _, _| {}),
|
||||
@@ -147,11 +141,6 @@ impl Folder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn active_icon(mut self, icon: impl Into<Icon>) -> Self {
|
||||
self.active_icon = Some(icon.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn collapsed(mut self, collapsed: bool) -> Self {
|
||||
self.collapsed = collapsed;
|
||||
self
|
||||
@@ -186,10 +175,7 @@ impl RenderOnce for Folder {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let handler = self.handler.clone();
|
||||
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
self.base
|
||||
.child(
|
||||
div()
|
||||
.id(self.label.clone())
|
||||
@@ -197,23 +183,24 @@ impl RenderOnce for Folder {
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.px_2()
|
||||
.h_6()
|
||||
.h_8()
|
||||
.rounded(px(cx.theme().radius))
|
||||
.text_xs()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
|
||||
.font_semibold()
|
||||
.when_some(self.icon, |this, icon| {
|
||||
this.map(|this| {
|
||||
if self.collapsed {
|
||||
this.child(icon.size_4())
|
||||
} else {
|
||||
this.when_some(self.active_icon, |this, icon| {
|
||||
this.child(icon.size_4())
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
.child(self.label.clone())
|
||||
.font_medium()
|
||||
.child(
|
||||
Icon::new(IconName::CaretDown)
|
||||
.xsmall()
|
||||
.when(self.collapsed, |this| this.rotate(percentage(270. / 360.))),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.when_some(self.icon, |this, icon| this.child(icon.small()))
|
||||
.child(self.label.clone()),
|
||||
)
|
||||
.hover(|this| this.bg(cx.theme().base.step(cx, ColorScaleStep::THREE)))
|
||||
.on_click(move |ev, window, cx| handler(ev, window, cx)),
|
||||
)
|
||||
@@ -226,6 +213,7 @@ impl RenderOnce for Folder {
|
||||
#[derive(IntoElement)]
|
||||
pub struct FolderItem {
|
||||
ix: usize,
|
||||
base: Div,
|
||||
img: Option<Img>,
|
||||
label: Option<SharedString>,
|
||||
description: Option<SharedString>,
|
||||
@@ -236,6 +224,7 @@ impl FolderItem {
|
||||
pub fn new(ix: usize) -> Self {
|
||||
Self {
|
||||
ix,
|
||||
base: div().h_8().w_full().px_2(),
|
||||
img: None,
|
||||
label: None,
|
||||
description: None,
|
||||
@@ -271,15 +260,12 @@ impl RenderOnce for FolderItem {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let handler = self.handler.clone();
|
||||
|
||||
div()
|
||||
self.base
|
||||
.id(self.ix)
|
||||
.h_6()
|
||||
.px_2()
|
||||
.w_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.text_xs()
|
||||
.text_sm()
|
||||
.rounded(px(cx.theme().radius))
|
||||
.child(
|
||||
div()
|
||||
@@ -291,19 +277,21 @@ impl RenderOnce for FolderItem {
|
||||
.font_medium()
|
||||
.map(|this| {
|
||||
if let Some(img) = self.img {
|
||||
this.child(img.size_4().flex_shrink_0())
|
||||
this.child(img.size_5().flex_shrink_0())
|
||||
} else {
|
||||
this.child(
|
||||
div()
|
||||
.flex()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.size_4()
|
||||
.size_5()
|
||||
.rounded_full()
|
||||
.bg(cx.theme().accent.step(cx, ColorScaleStep::THREE))
|
||||
.child(Icon::new(IconName::GroupFill).size_2().text_color(
|
||||
cx.theme().accent.step(cx, ColorScaleStep::TWELVE),
|
||||
)),
|
||||
.child(
|
||||
Icon::new(IconName::UsersThreeFill).xsmall().text_color(
|
||||
cx.theme().accent.step(cx, ColorScaleStep::TWELVE),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -313,11 +301,12 @@ impl RenderOnce for FolderItem {
|
||||
this.child(
|
||||
div()
|
||||
.flex_shrink_0()
|
||||
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
|
||||
.text_xs()
|
||||
.text_color(cx.theme().base.step(cx, ColorScaleStep::TEN))
|
||||
.child(description),
|
||||
)
|
||||
})
|
||||
.hover(|this| this.bg(cx.theme().base.step(cx, ColorScaleStep::FOUR)))
|
||||
.hover(|this| this.bg(cx.theme().base.step(cx, ColorScaleStep::THREE)))
|
||||
.on_click(move |ev, window, cx| handler(ev, window, cx))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,37 @@
|
||||
use account::Account;
|
||||
use button::SidebarButton;
|
||||
use chats::{
|
||||
room::{Room, RoomKind},
|
||||
ChatRegistry,
|
||||
};
|
||||
use compose::{Compose, ComposeButton};
|
||||
use common::profile::SharedProfile;
|
||||
use folder::{Folder, FolderItem, Parent};
|
||||
use global::get_client;
|
||||
use gpui::{
|
||||
div, img, prelude::FluentBuilder, px, AnyElement, App, AppContext, Context, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, Render, SharedString, Styled,
|
||||
Window,
|
||||
actions, div, img, prelude::FluentBuilder, AnyElement, App, AppContext, Context, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Render,
|
||||
SharedString, Styled, Task, Window,
|
||||
};
|
||||
use ui::{
|
||||
button::{Button, ButtonRounded, ButtonVariants},
|
||||
dock_area::panel::{Panel, PanelEvent},
|
||||
popup_menu::PopupMenu,
|
||||
button::{Button, ButtonVariants},
|
||||
dock_area::{
|
||||
dock::DockPlacement,
|
||||
panel::{Panel, PanelEvent},
|
||||
},
|
||||
popup_menu::{PopupMenu, PopupMenuExt},
|
||||
scroll::ScrollbarAxis,
|
||||
skeleton::Skeleton,
|
||||
theme::{scale::ColorScaleStep, ActiveTheme},
|
||||
ContextModal, Disableable, IconName, StyledExt,
|
||||
IconName, Sizable, StyledExt,
|
||||
};
|
||||
|
||||
use crate::chat_space::{AddPanel, PanelKind};
|
||||
use crate::chat_space::{AddPanel, ModalKind, PanelKind, ToggleModal};
|
||||
|
||||
mod compose;
|
||||
mod button;
|
||||
mod folder;
|
||||
|
||||
actions!(profile, [Logout]);
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Sidebar> {
|
||||
Sidebar::new(window, cx)
|
||||
}
|
||||
@@ -55,49 +63,6 @@ impl Sidebar {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_compose(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let compose = cx.new(|cx| Compose::new(window, cx));
|
||||
|
||||
window.open_modal(cx, move |modal, window, cx| {
|
||||
let label = compose.read(cx).label(window, cx);
|
||||
let is_submitting = compose.read(cx).is_submitting();
|
||||
|
||||
modal
|
||||
.title("Direct Messages")
|
||||
.width(px(420.))
|
||||
.child(compose.clone())
|
||||
.footer(
|
||||
div()
|
||||
.p_2()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().base.step(cx, ColorScaleStep::FIVE))
|
||||
.child(
|
||||
Button::new("create_dm_btn")
|
||||
.label(label)
|
||||
.primary()
|
||||
.bold()
|
||||
.rounded(ButtonRounded::Large)
|
||||
.w_full()
|
||||
.loading(is_submitting)
|
||||
.disabled(is_submitting)
|
||||
.on_click(window.listener_for(&compose, |this, _, window, cx| {
|
||||
this.compose(window, cx)
|
||||
})),
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn open_room(&self, id: u64, window: &mut Window, cx: &mut Context<Self>) {
|
||||
window.dispatch_action(
|
||||
Box::new(AddPanel::new(
|
||||
PanelKind::Room(id),
|
||||
ui::dock_area::dock::DockPlacement::Center,
|
||||
)),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
fn ongoing(&mut self, cx: &mut Context<Self>) {
|
||||
self.ongoing = !self.ongoing;
|
||||
cx.notify();
|
||||
@@ -118,7 +83,6 @@ impl Sidebar {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn render_skeleton(&self, total: i32) -> impl IntoIterator<Item = impl IntoElement> {
|
||||
(0..total).map(|_| {
|
||||
div()
|
||||
@@ -148,8 +112,11 @@ impl Sidebar {
|
||||
.description(ago)
|
||||
.img(img)
|
||||
.on_click({
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
this.open_room(id, window, cx);
|
||||
cx.listener(move |_, _, window, cx| {
|
||||
window.dispatch_action(
|
||||
Box::new(AddPanel::new(PanelKind::Room(id), DockPlacement::Center)),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
});
|
||||
|
||||
@@ -158,6 +125,28 @@ impl Sidebar {
|
||||
|
||||
items
|
||||
}
|
||||
|
||||
fn on_logout(&mut self, _: &Logout, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let task: Task<Result<(), anyhow::Error>> = cx.background_spawn(async move {
|
||||
let client = get_client();
|
||||
_ = client.reset().await;
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
if task.await.is_ok() {
|
||||
cx.update(|_, cx| {
|
||||
Account::global(cx).update(cx, |this, cx| {
|
||||
this.profile = None;
|
||||
cx.notify();
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
};
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
impl Panel for Sidebar {
|
||||
@@ -188,7 +177,9 @@ impl Focusable for Sidebar {
|
||||
|
||||
impl Render for Sidebar {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let account = Account::global(cx).read(cx).profile.as_ref();
|
||||
let registry = ChatRegistry::global(cx).read(cx);
|
||||
|
||||
let rooms = registry.rooms(cx);
|
||||
let loading = registry.loading();
|
||||
|
||||
@@ -198,67 +189,149 @@ impl Render for Sidebar {
|
||||
|
||||
div()
|
||||
.scrollable(cx.entity_id(), ScrollbarAxis::Vertical)
|
||||
.on_action(cx.listener(Self::on_logout))
|
||||
.size_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.pt_1()
|
||||
.px_2()
|
||||
.py_3()
|
||||
.child(ComposeButton::new("New Message").on_click(cx.listener(
|
||||
|this, _, window, cx| {
|
||||
this.render_compose(window, cx);
|
||||
},
|
||||
)))
|
||||
.map(|this| {
|
||||
if loading {
|
||||
this.children(self.render_skeleton(6))
|
||||
} else {
|
||||
this.when_some(ongoing, |this, rooms| {
|
||||
this.child(
|
||||
Folder::new("Ongoing")
|
||||
.icon(IconName::FolderFill)
|
||||
.active_icon(IconName::FolderOpenFill)
|
||||
.collapsed(self.ongoing)
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
this.ongoing(cx);
|
||||
}))
|
||||
.children(Self::render_items(rooms, cx)),
|
||||
.pb_2()
|
||||
.when_some(account, |this, profile| {
|
||||
this.child(
|
||||
div()
|
||||
.h_7()
|
||||
.px_1p5()
|
||||
.flex()
|
||||
.justify_between()
|
||||
.items_center()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.text_sm()
|
||||
.font_semibold()
|
||||
.child(img(profile.shared_avatar()).size_7())
|
||||
.child(profile.shared_name()),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
Button::new("user_dropdown")
|
||||
.icon(IconName::Ellipsis)
|
||||
.small()
|
||||
.ghost()
|
||||
.popup_menu(|this, _window, _cx| {
|
||||
this.menu(
|
||||
"Profile",
|
||||
Box::new(ToggleModal {
|
||||
modal: ModalKind::Profile,
|
||||
}),
|
||||
)
|
||||
.menu(
|
||||
"Relays",
|
||||
Box::new(ToggleModal {
|
||||
modal: ModalKind::Relay,
|
||||
}),
|
||||
)
|
||||
.separator()
|
||||
.menu("Logout", Box::new(Logout))
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.text_sm()
|
||||
.font_medium()
|
||||
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
|
||||
.child(
|
||||
Parent::new("Incoming")
|
||||
.icon(IconName::FolderFill)
|
||||
.active_icon(IconName::FolderOpenFill)
|
||||
.collapsed(self.incoming)
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
this.incoming(cx);
|
||||
}))
|
||||
.when_some(trusted, |this, rooms| {
|
||||
SidebarButton::new("New Message")
|
||||
.icon(IconName::PlusCircleFill)
|
||||
.on_click(cx.listener(|_, _, window, cx| {
|
||||
window.dispatch_action(
|
||||
Box::new(ToggleModal {
|
||||
modal: ModalKind::Compose,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
SidebarButton::new("Contacts")
|
||||
.icon(IconName::AddressBook)
|
||||
.on_click(cx.listener(|_, _, window, cx| {
|
||||
window.dispatch_action(
|
||||
Box::new(ToggleModal {
|
||||
modal: ModalKind::Contact,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.px_2()
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().base.step(cx, ColorScaleStep::NINE))
|
||||
.child("Messages"),
|
||||
)
|
||||
.map(|this| {
|
||||
if loading {
|
||||
this.children(self.render_skeleton(6))
|
||||
} else {
|
||||
this.when_some(ongoing, |this, rooms| {
|
||||
this.child(
|
||||
Folder::new("Trusted")
|
||||
.icon(IconName::FolderFill)
|
||||
.active_icon(IconName::FolderOpenFill)
|
||||
.collapsed(self.trusted)
|
||||
Folder::new("Ongoing")
|
||||
.icon(IconName::Folder)
|
||||
.collapsed(self.ongoing)
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
this.trusted(cx);
|
||||
this.ongoing(cx);
|
||||
}))
|
||||
.children(Self::render_items(rooms, cx)),
|
||||
)
|
||||
})
|
||||
.when_some(unknown, |this, rooms| {
|
||||
this.child(
|
||||
Folder::new("Unknown")
|
||||
.icon(IconName::FolderFill)
|
||||
.active_icon(IconName::FolderOpenFill)
|
||||
.collapsed(self.unknown)
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
this.unknown(cx);
|
||||
}))
|
||||
.children(Self::render_items(rooms, cx)),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
.child(
|
||||
Parent::new("Incoming")
|
||||
.icon(IconName::Folder)
|
||||
.collapsed(self.incoming)
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
this.incoming(cx);
|
||||
}))
|
||||
.when_some(trusted, |this, rooms| {
|
||||
this.child(
|
||||
Folder::new("Trusted")
|
||||
.icon(IconName::Folder)
|
||||
.collapsed(self.trusted)
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
this.trusted(cx);
|
||||
}))
|
||||
.children(Self::render_items(rooms, cx)),
|
||||
)
|
||||
})
|
||||
.when_some(unknown, |this, rooms| {
|
||||
this.child(
|
||||
Folder::new("Unknown")
|
||||
.icon(IconName::Folder)
|
||||
.collapsed(self.unknown)
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
this.unknown(cx);
|
||||
}))
|
||||
.children(Self::render_items(rooms, cx)),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ impl Panel for Welcome {
|
||||
}
|
||||
|
||||
fn title(&self, _cx: &App) -> AnyElement {
|
||||
self.name.clone().into_any_element()
|
||||
"👋".into_any_element()
|
||||
}
|
||||
|
||||
fn closable(&self, _cx: &App) -> bool {
|
||||
@@ -92,8 +92,8 @@ impl Render for Welcome {
|
||||
.child(
|
||||
div()
|
||||
.child("coop on nostr.")
|
||||
.text_color(cx.theme().base.step(cx, ColorScaleStep::FOUR))
|
||||
.font_black()
|
||||
.text_color(cx.theme().base.step(cx, ColorScaleStep::NINE))
|
||||
.font_semibold()
|
||||
.text_sm(),
|
||||
),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user