redesign the sidebar
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 2m0s
Rust / build (ubuntu-latest, stable) (pull_request) Failing after 1m27s
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 2m0s
Rust / build (ubuntu-latest, stable) (pull_request) Failing after 1m27s
This commit is contained in:
@@ -12,7 +12,6 @@ use ui::Root;
|
||||
use crate::actions::Quit;
|
||||
|
||||
mod actions;
|
||||
mod command_bar;
|
||||
mod dialogs;
|
||||
mod panels;
|
||||
mod sidebar;
|
||||
|
||||
@@ -154,7 +154,7 @@ impl Render for GreeterPanel {
|
||||
.label("Set up relay list")
|
||||
.ghost()
|
||||
.small()
|
||||
.no_center()
|
||||
.justify_start()
|
||||
.on_click(move |_ev, window, cx| {
|
||||
Workspace::add_panel(
|
||||
relay_list::init(window, cx),
|
||||
@@ -172,7 +172,7 @@ impl Render for GreeterPanel {
|
||||
.label("Set up messaging relays")
|
||||
.ghost()
|
||||
.small()
|
||||
.no_center()
|
||||
.justify_start()
|
||||
.on_click(move |_ev, window, cx| {
|
||||
Workspace::add_panel(
|
||||
messaging_relays::init(window, cx),
|
||||
@@ -211,7 +211,7 @@ impl Render for GreeterPanel {
|
||||
.label("Connect account via Nostr Connect")
|
||||
.ghost()
|
||||
.small()
|
||||
.no_center()
|
||||
.justify_start()
|
||||
.on_click(move |_ev, window, cx| {
|
||||
Workspace::add_panel(
|
||||
connect::init(window, cx),
|
||||
@@ -227,7 +227,7 @@ impl Render for GreeterPanel {
|
||||
.label("Import a secret key or bunker")
|
||||
.ghost()
|
||||
.small()
|
||||
.no_center()
|
||||
.justify_start()
|
||||
.on_click(move |_ev, window, cx| {
|
||||
Workspace::add_panel(
|
||||
import::init(window, cx),
|
||||
@@ -264,7 +264,7 @@ impl Render for GreeterPanel {
|
||||
.label("Backup account")
|
||||
.ghost()
|
||||
.small()
|
||||
.no_center(),
|
||||
.justify_start(),
|
||||
)
|
||||
.child(
|
||||
Button::new("profile")
|
||||
@@ -272,7 +272,7 @@ impl Render for GreeterPanel {
|
||||
.label("Update profile")
|
||||
.ghost()
|
||||
.small()
|
||||
.no_center()
|
||||
.justify_start()
|
||||
.on_click(cx.listener(move |this, _ev, window, cx| {
|
||||
this.add_profile_panel(window, cx)
|
||||
})),
|
||||
@@ -283,7 +283,7 @@ impl Render for GreeterPanel {
|
||||
.label("Invite friends")
|
||||
.ghost()
|
||||
.small()
|
||||
.no_center(),
|
||||
.justify_start(),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -438,9 +438,9 @@ impl Render for CommandBar {
|
||||
.w_full()
|
||||
.child(
|
||||
TextInput::new(&self.find_input)
|
||||
.appearance(true)
|
||||
.appearance(false)
|
||||
.bordered(false)
|
||||
.xsmall()
|
||||
.small()
|
||||
.text_xs()
|
||||
.when(!self.find_input.read(cx).loading, |this| {
|
||||
this.suffix(
|
||||
@@ -1,23 +1,38 @@
|
||||
use std::collections::HashSet;
|
||||
use std::ops::Range;
|
||||
use std::time::Duration;
|
||||
|
||||
use chat::{ChatEvent, ChatRegistry, RoomKind};
|
||||
use common::RenderedTimestamp;
|
||||
use anyhow::Error;
|
||||
use chat::{ChatEvent, ChatRegistry, Room, RoomKind};
|
||||
use common::{DebouncedDelay, RenderedTimestamp};
|
||||
use dock::panel::{Panel, PanelEvent};
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
deferred, div, uniform_list, App, AppContext, Context, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, IntoElement, ParentElement, Render, RetainAllImageCache, SharedString, Styled,
|
||||
Subscription, Window,
|
||||
div, rems, uniform_list, App, AppContext, Context, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, InteractiveElement, IntoElement, ParentElement, Render, RetainAllImageCache,
|
||||
SharedString, StatefulInteractiveElement, Styled, Subscription, Task, Window,
|
||||
};
|
||||
use list_item::RoomListItem;
|
||||
use nostr_sdk::prelude::*;
|
||||
use person::PersonRegistry;
|
||||
use settings::AppSettings;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use state::{NostrRegistry, FIND_DELAY};
|
||||
use theme::{ActiveTheme, TABBAR_HEIGHT};
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::divider::Divider;
|
||||
use ui::indicator::Indicator;
|
||||
use ui::{h_flex, v_flex, IconName, Selectable, Sizable, StyledExt};
|
||||
use ui::input::{InputEvent, InputState, TextInput};
|
||||
use ui::notification::Notification;
|
||||
use ui::{
|
||||
h_flex, v_flex, Disableable, Icon, IconName, Selectable, Sizable, StyledExt, WindowExtension,
|
||||
};
|
||||
|
||||
mod list_item;
|
||||
|
||||
const INPUT_PLACEHOLDER: &str = "Find or start a conversation";
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Sidebar> {
|
||||
cx.new(|cx| Sidebar::new(window, cx))
|
||||
}
|
||||
@@ -30,12 +45,42 @@ pub struct Sidebar {
|
||||
/// Image cache
|
||||
image_cache: Entity<RetainAllImageCache>,
|
||||
|
||||
/// Find input state
|
||||
find_input: Entity<InputState>,
|
||||
|
||||
/// Debounced delay for find input
|
||||
find_debouncer: DebouncedDelay<Self>,
|
||||
|
||||
/// Whether a search is in progress
|
||||
finding: bool,
|
||||
|
||||
/// Whether the find input is focused
|
||||
find_focused: bool,
|
||||
|
||||
/// Find results
|
||||
find_results: Entity<Option<Vec<PublicKey>>>,
|
||||
|
||||
/// Async find operation
|
||||
find_task: Option<Task<Result<(), Error>>>,
|
||||
|
||||
/// Whether there are search results
|
||||
has_search: bool,
|
||||
|
||||
/// Whether there are new chat requests
|
||||
new_requests: bool,
|
||||
|
||||
/// Selected public keys
|
||||
selected_pkeys: Entity<HashSet<PublicKey>>,
|
||||
|
||||
/// Chatroom filter
|
||||
filter: Entity<RoomKind>,
|
||||
|
||||
/// User's contacts
|
||||
contact_list: Entity<Option<Vec<PublicKey>>>,
|
||||
|
||||
/// Async tasks
|
||||
tasks: SmallVec<[Task<()>; 1]>,
|
||||
|
||||
/// Event subscriptions
|
||||
_subscriptions: SmallVec<[Subscription; 1]>,
|
||||
}
|
||||
@@ -44,9 +89,49 @@ impl Sidebar {
|
||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let chat = ChatRegistry::global(cx);
|
||||
let filter = cx.new(|_| RoomKind::Ongoing);
|
||||
let contact_list = cx.new(|_| None);
|
||||
let selected_pkeys = cx.new(|_| HashSet::new());
|
||||
let find_results = cx.new(|_| None);
|
||||
let find_input = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.placeholder(INPUT_PLACEHOLDER)
|
||||
.clean_on_escape()
|
||||
});
|
||||
|
||||
let mut subscriptions = smallvec![];
|
||||
|
||||
subscriptions.push(
|
||||
// Subscribe to find input events
|
||||
cx.subscribe_in(&find_input, window, |this, state, event, window, cx| {
|
||||
let delay = Duration::from_millis(FIND_DELAY);
|
||||
|
||||
match event {
|
||||
InputEvent::PressEnter { .. } => {
|
||||
this.search(window, cx);
|
||||
}
|
||||
InputEvent::Change => {
|
||||
if state.read(cx).value().is_empty() {
|
||||
// Clear results when input is empty
|
||||
this.reset(window, cx);
|
||||
} else {
|
||||
// Run debounced search
|
||||
this.find_debouncer
|
||||
.fire_new(delay, window, cx, |this, window, cx| {
|
||||
this.debounced_search(window, cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
InputEvent::Focus => {
|
||||
this.set_input_focus(cx);
|
||||
this.get_contact_list(window, cx);
|
||||
}
|
||||
InputEvent::Blur => {
|
||||
this.set_input_focus(cx);
|
||||
}
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
subscriptions.push(
|
||||
// Subscribe for registry new events
|
||||
cx.subscribe_in(&chat, window, move |this, _s, event, _window, cx| {
|
||||
@@ -61,12 +146,193 @@ impl Sidebar {
|
||||
name: "Sidebar".into(),
|
||||
focus_handle: cx.focus_handle(),
|
||||
image_cache: RetainAllImageCache::new(cx),
|
||||
find_input,
|
||||
find_debouncer: DebouncedDelay::new(),
|
||||
find_results,
|
||||
find_task: None,
|
||||
find_focused: false,
|
||||
finding: false,
|
||||
has_search: false,
|
||||
new_requests: false,
|
||||
contact_list,
|
||||
selected_pkeys,
|
||||
filter,
|
||||
tasks: smallvec![],
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the contact list.
|
||||
fn get_contact_list(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let task = nostr.read(cx).get_contact_list(cx);
|
||||
|
||||
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||
match task.await {
|
||||
Ok(contacts) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_contact_list(contacts, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Err(e) => {
|
||||
cx.update(|window, cx| {
|
||||
window.push_notification(Notification::error(e.to_string()), cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
/// Set the contact list with new contacts.
|
||||
fn set_contact_list<I>(&mut self, contacts: I, cx: &mut Context<Self>)
|
||||
where
|
||||
I: IntoIterator<Item = PublicKey>,
|
||||
{
|
||||
self.contact_list.update(cx, |this, cx| {
|
||||
*this = Some(contacts.into_iter().collect());
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
|
||||
/// Trigger the debounced search
|
||||
fn debounced_search(&self, window: &mut Window, cx: &mut Context<Self>) -> Task<()> {
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.search(window, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
|
||||
/// Search
|
||||
fn search(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
// Return if a search is already in progress
|
||||
if self.finding {
|
||||
if self.find_task.is_none() {
|
||||
window.push_notification("There is another search in progress", cx);
|
||||
return;
|
||||
} else {
|
||||
// Cancel the ongoing search request
|
||||
self.find_task = None;
|
||||
}
|
||||
}
|
||||
|
||||
// Get query
|
||||
let query = self.find_input.read(cx).value();
|
||||
|
||||
// Return if the query is empty
|
||||
if query.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Block the input until the search completes
|
||||
self.set_finding(true, window, cx);
|
||||
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let find_users = nostr.read(cx).search(&query, cx);
|
||||
|
||||
// Run task in the main thread
|
||||
self.find_task = Some(cx.spawn_in(window, async move |this, cx| {
|
||||
let rooms = find_users.await?;
|
||||
// Update the UI with the search results
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.set_results(rooms, cx);
|
||||
this.set_finding(false, window, cx);
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
|
||||
fn set_results(&mut self, results: Vec<PublicKey>, cx: &mut Context<Self>) {
|
||||
self.find_results.update(cx, |this, cx| {
|
||||
*this = Some(results);
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
|
||||
fn set_finding(&mut self, status: bool, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
// Disable the input to prevent duplicate requests
|
||||
self.find_input.update(cx, |this, cx| {
|
||||
this.set_disabled(status, cx);
|
||||
this.set_loading(status, cx);
|
||||
});
|
||||
// Set the search status
|
||||
self.finding = status;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn set_input_focus(&mut self, cx: &mut Context<Self>) {
|
||||
self.find_focused = !self.find_focused;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn reset(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
// Clear all search results
|
||||
self.find_results.update(cx, |this, cx| {
|
||||
*this = None;
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
// Reset the search status
|
||||
self.set_finding(false, window, cx);
|
||||
|
||||
// Cancel the current search task
|
||||
self.find_task = None;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Select a public key in the sidebar.
|
||||
fn select(&mut self, pkey: PublicKey, cx: &mut Context<Self>) {
|
||||
self.selected_pkeys.update(cx, |this, cx| {
|
||||
if this.contains(&pkey) {
|
||||
this.remove(&pkey);
|
||||
} else {
|
||||
this.insert(pkey);
|
||||
}
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
|
||||
/// Check if a public key is selected in the sidebar.
|
||||
fn is_selected(&self, pkey: PublicKey, cx: &App) -> bool {
|
||||
self.selected_pkeys.read(cx).contains(&pkey)
|
||||
}
|
||||
|
||||
/// Get all selected public keys in the sidebar.
|
||||
fn selected(&self, cx: &Context<Self>) -> HashSet<PublicKey> {
|
||||
self.selected_pkeys.read(cx).clone()
|
||||
}
|
||||
|
||||
/// Create a new room
|
||||
fn create_room(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let chat = ChatRegistry::global(cx);
|
||||
let async_chat = chat.downgrade();
|
||||
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let signer_pkey = nostr.read(cx).signer_pkey(cx);
|
||||
|
||||
// Get all selected public keys
|
||||
let receivers = self.selected(cx);
|
||||
|
||||
let task: Task<Result<(), Error>> = cx.spawn_in(window, async move |_this, cx| {
|
||||
let public_key = signer_pkey.await?;
|
||||
|
||||
async_chat.update_in(cx, |this, window, cx| {
|
||||
let room = cx.new(|_| Room::new(public_key, receivers));
|
||||
this.emit_room(room.downgrade(), cx);
|
||||
|
||||
window.close_modal(cx);
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
task.detach();
|
||||
}
|
||||
|
||||
/// Get the active filter.
|
||||
fn current_filter(&self, kind: &RoomKind, cx: &Context<Self>) -> bool {
|
||||
self.filter.read(cx) == kind
|
||||
@@ -111,6 +377,67 @@ impl Sidebar {
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn render_contacts(&self, range: Range<usize>, cx: &Context<Self>) -> Vec<impl IntoElement> {
|
||||
let persons = PersonRegistry::global(cx);
|
||||
let hide_avatar = AppSettings::get_hide_avatar(cx);
|
||||
|
||||
let Some(contacts) = self.contact_list.read(cx) else {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
contacts
|
||||
.get(range.clone())
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.enumerate()
|
||||
.map(|(ix, item)| {
|
||||
let profile = persons.read(cx).get(item, cx);
|
||||
let pkey = item.to_owned();
|
||||
let id = range.start + ix;
|
||||
|
||||
h_flex()
|
||||
.id(id)
|
||||
.h_8()
|
||||
.w_full()
|
||||
.px_1()
|
||||
.gap_2()
|
||||
.rounded(cx.theme().radius)
|
||||
.when(!hide_avatar, |this| {
|
||||
this.child(
|
||||
div()
|
||||
.flex_shrink_0()
|
||||
.size_6()
|
||||
.rounded_full()
|
||||
.overflow_hidden()
|
||||
.child(Avatar::new(profile.avatar()).size(rems(1.5))),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_1()
|
||||
.justify_between()
|
||||
.line_clamp(1)
|
||||
.text_ellipsis()
|
||||
.truncate()
|
||||
.text_sm()
|
||||
.child(profile.name())
|
||||
.when(self.is_selected(pkey, cx), |this| {
|
||||
this.child(
|
||||
Icon::new(IconName::CheckCircle)
|
||||
.small()
|
||||
.text_color(cx.theme().icon_accent),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.hover(|this| this.bg(cx.theme().elevated_surface_background))
|
||||
.on_click(cx.listener(move |this, _ev, _window, cx| {
|
||||
this.select(pkey, cx);
|
||||
}))
|
||||
.into_any_element()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Panel for Sidebar {
|
||||
@@ -133,89 +460,124 @@ impl Render for Sidebar {
|
||||
let loading = chat.read(cx).loading();
|
||||
let total_rooms = chat.read(cx).count(self.filter.read(cx), cx);
|
||||
|
||||
// Whether the find panel should be shown
|
||||
let show_find_panel = self.has_search || self.find_focused;
|
||||
|
||||
// Set button label based on total selected users
|
||||
let button_label = if self.selected_pkeys.read(cx).len() > 1 {
|
||||
"Create Group DM"
|
||||
} else {
|
||||
"Create DM"
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.image_cache(self.image_cache.clone())
|
||||
.size_full()
|
||||
.relative()
|
||||
.gap_2()
|
||||
.child(
|
||||
h_flex()
|
||||
.h(TABBAR_HEIGHT)
|
||||
.w_full()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().border)
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_1()
|
||||
.h_full()
|
||||
.gap_2()
|
||||
.p_2()
|
||||
.justify_center()
|
||||
.child(
|
||||
Button::new("all")
|
||||
.map(|this| {
|
||||
if self.current_filter(&RoomKind::Ongoing, cx) {
|
||||
this.icon(IconName::InboxFill)
|
||||
} else {
|
||||
this.icon(IconName::Inbox)
|
||||
}
|
||||
})
|
||||
.label("Inbox")
|
||||
.tooltip("All ongoing conversations")
|
||||
.xsmall()
|
||||
.bold()
|
||||
.ghost()
|
||||
.flex_1()
|
||||
.rounded_none()
|
||||
.selected(self.current_filter(&RoomKind::Ongoing, cx))
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.set_filter(RoomKind::Ongoing, cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("requests")
|
||||
.map(|this| {
|
||||
if self.current_filter(&RoomKind::Request, cx) {
|
||||
this.icon(IconName::FistbumpFill)
|
||||
} else {
|
||||
this.icon(IconName::Fistbump)
|
||||
}
|
||||
})
|
||||
.label("Requests")
|
||||
.tooltip("Incoming new conversations")
|
||||
.xsmall()
|
||||
.bold()
|
||||
.ghost()
|
||||
.flex_1()
|
||||
.rounded_none()
|
||||
.selected(!self.current_filter(&RoomKind::Ongoing, cx))
|
||||
.when(self.new_requests, |this| {
|
||||
this.child(
|
||||
div().size_1().rounded_full().bg(cx.theme().cursor),
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.set_filter(RoomKind::default(), cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.h_full()
|
||||
.px_2()
|
||||
.border_l_1()
|
||||
.border_color(cx.theme().border)
|
||||
.child(
|
||||
Button::new("option")
|
||||
.icon(IconName::Ellipsis)
|
||||
.small()
|
||||
.ghost(),
|
||||
),
|
||||
TextInput::new(&self.find_input)
|
||||
.appearance(false)
|
||||
.bordered(false)
|
||||
.small()
|
||||
.text_xs()
|
||||
.when(!self.find_input.read(cx).loading, |this| {
|
||||
this.suffix(
|
||||
Button::new("find-icon")
|
||||
.icon(IconName::Search)
|
||||
.tooltip("Press Enter to search")
|
||||
.transparent()
|
||||
.small(),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.when(!loading && total_rooms == 0, |this| {
|
||||
.child(
|
||||
h_flex()
|
||||
.h(TABBAR_HEIGHT)
|
||||
.justify_center()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().border)
|
||||
.when(show_find_panel, |this| {
|
||||
this.child(
|
||||
Button::new("search-results")
|
||||
.icon(IconName::Search)
|
||||
.label("Search")
|
||||
.tooltip("All search results")
|
||||
.small()
|
||||
.underline()
|
||||
.ghost()
|
||||
.font_semibold()
|
||||
.rounded_none()
|
||||
.h_full()
|
||||
.flex_1()
|
||||
.selected(true),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
Button::new("all")
|
||||
.map(|this| {
|
||||
if self.current_filter(&RoomKind::Ongoing, cx) {
|
||||
this.icon(IconName::InboxFill)
|
||||
} else {
|
||||
this.icon(IconName::Inbox)
|
||||
}
|
||||
})
|
||||
.when(!show_find_panel, |this| this.label("Inbox"))
|
||||
.tooltip("All ongoing conversations")
|
||||
.small()
|
||||
.underline()
|
||||
.ghost()
|
||||
.font_semibold()
|
||||
.rounded_none()
|
||||
.h_full()
|
||||
.flex_1()
|
||||
.disabled(show_find_panel)
|
||||
.selected(
|
||||
!show_find_panel && self.current_filter(&RoomKind::Ongoing, cx),
|
||||
)
|
||||
.on_click(cx.listener(|this, _ev, _window, cx| {
|
||||
this.set_filter(RoomKind::Ongoing, cx);
|
||||
})),
|
||||
)
|
||||
.child(Divider::vertical())
|
||||
.child(
|
||||
Button::new("requests")
|
||||
.map(|this| {
|
||||
if self.current_filter(&RoomKind::Request, cx) {
|
||||
this.icon(IconName::FistbumpFill)
|
||||
} else {
|
||||
this.icon(IconName::Fistbump)
|
||||
}
|
||||
})
|
||||
.when(!show_find_panel, |this| this.label("Requests"))
|
||||
.tooltip("Incoming new conversations")
|
||||
.small()
|
||||
.ghost()
|
||||
.underline()
|
||||
.font_semibold()
|
||||
.rounded_none()
|
||||
.h_full()
|
||||
.flex_1()
|
||||
.disabled(show_find_panel)
|
||||
.selected(
|
||||
!show_find_panel && !self.current_filter(&RoomKind::Ongoing, cx),
|
||||
)
|
||||
.when(self.new_requests, |this| {
|
||||
this.child(div().size_1().rounded_full().bg(cx.theme().cursor))
|
||||
})
|
||||
.on_click(cx.listener(|this, _ev, _window, cx| {
|
||||
this.set_filter(RoomKind::default(), cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
.when(!show_find_panel && !loading && total_rooms == 0, |this| {
|
||||
this.child(
|
||||
div().px_2p5().child(deferred(
|
||||
div().mt_2().px_2().child(
|
||||
v_flex()
|
||||
.p_3()
|
||||
.h_24()
|
||||
@@ -238,47 +600,131 @@ impl Render for Sidebar {
|
||||
"Start a conversation with someone to get started.",
|
||||
),
|
||||
)),
|
||||
)),
|
||||
),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
v_flex()
|
||||
.h_full()
|
||||
.px_1p5()
|
||||
.w_full()
|
||||
.mt_2()
|
||||
.flex_1()
|
||||
.gap_1()
|
||||
.overflow_y_hidden()
|
||||
.child(
|
||||
uniform_list(
|
||||
"rooms",
|
||||
total_rooms,
|
||||
cx.processor(|this, range, _window, cx| {
|
||||
this.render_list_items(range, cx)
|
||||
}),
|
||||
)
|
||||
.h_full(),
|
||||
)
|
||||
.when(loading, |this| {
|
||||
.when(show_find_panel, |this| {
|
||||
this.gap_2()
|
||||
.when_some(self.find_results.read(cx).as_ref(), |this, results| {
|
||||
this.child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.flex_1()
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(SharedString::from("Results")),
|
||||
)
|
||||
.child(
|
||||
uniform_list(
|
||||
"rooms",
|
||||
results.len(),
|
||||
cx.processor(|this, range, _window, cx| {
|
||||
this.render_list_items(range, cx)
|
||||
}),
|
||||
)
|
||||
.flex_1()
|
||||
.h_full(),
|
||||
),
|
||||
)
|
||||
})
|
||||
.when_some(self.contact_list.read(cx).as_ref(), |this, contacts| {
|
||||
this.child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.flex_1()
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(SharedString::from("Suggestions")),
|
||||
)
|
||||
.child(
|
||||
uniform_list(
|
||||
"contacts",
|
||||
contacts.len(),
|
||||
cx.processor(move |this, range, _window, cx| {
|
||||
this.render_contacts(range, cx)
|
||||
}),
|
||||
)
|
||||
.flex_1()
|
||||
.h_full(),
|
||||
),
|
||||
)
|
||||
})
|
||||
})
|
||||
.when(!show_find_panel, |this| {
|
||||
this.child(
|
||||
div().absolute().top_2().left_0().w_full().px_8().child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.w_full()
|
||||
.h_9()
|
||||
.justify_center()
|
||||
.bg(cx.theme().background.opacity(0.85))
|
||||
.border_color(cx.theme().border_disabled)
|
||||
.border_1()
|
||||
.when(cx.theme().shadow, |this| this.shadow_sm())
|
||||
.rounded_full()
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(Indicator::new().small().color(cx.theme().icon_accent))
|
||||
.child(SharedString::from("Getting messages...")),
|
||||
),
|
||||
uniform_list(
|
||||
"rooms",
|
||||
total_rooms,
|
||||
cx.processor(|this, range, _window, cx| {
|
||||
this.render_list_items(range, cx)
|
||||
}),
|
||||
)
|
||||
.flex_1()
|
||||
.h_full(),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.when(!self.selected_pkeys.read(cx).is_empty(), |this| {
|
||||
this.child(
|
||||
div()
|
||||
.absolute()
|
||||
.bottom_0()
|
||||
.left_0()
|
||||
.h_9()
|
||||
.w_full()
|
||||
.px_2()
|
||||
.child(
|
||||
Button::new("create")
|
||||
.label(button_label)
|
||||
.primary()
|
||||
.small()
|
||||
.on_click(cx.listener(move |this, _ev, window, cx| {
|
||||
this.create_room(window, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
})
|
||||
.when(loading, |this| {
|
||||
this.child(
|
||||
div()
|
||||
.absolute()
|
||||
.bottom_2()
|
||||
.left_0()
|
||||
.h_9()
|
||||
.w_full()
|
||||
.px_8()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.w_full()
|
||||
.h_9()
|
||||
.justify_center()
|
||||
.bg(cx.theme().background.opacity(0.85))
|
||||
.border_color(cx.theme().border_disabled)
|
||||
.border_1()
|
||||
.when(cx.theme().shadow, |this| this.shadow_sm())
|
||||
.rounded_full()
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(Indicator::new().small().color(cx.theme().icon_accent))
|
||||
.child(SharedString::from("Getting messages...")),
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,12 +13,13 @@ use nostr_sdk::prelude::*;
|
||||
use person::PersonRegistry;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use state::NostrRegistry;
|
||||
use theme::{ActiveTheme, SIDEBAR_WIDTH, TITLEBAR_HEIGHT};
|
||||
use theme::{SIDEBAR_WIDTH, TITLEBAR_HEIGHT};
|
||||
use titlebar::TitleBar;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::{h_flex, v_flex, Icon, IconName, Root, Sizable, WindowExtension};
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::popup_menu::PopupMenuExt;
|
||||
use ui::{h_flex, v_flex, Root, Sizable, WindowExtension};
|
||||
|
||||
use crate::command_bar::CommandBar;
|
||||
use crate::panels::greeter;
|
||||
use crate::sidebar;
|
||||
|
||||
@@ -33,9 +34,6 @@ pub struct Workspace {
|
||||
/// App's Dock Area
|
||||
dock: Entity<DockArea>,
|
||||
|
||||
/// App's Command Bar
|
||||
command_bar: Entity<CommandBar>,
|
||||
|
||||
/// Current User
|
||||
current_user: Entity<Option<PublicKey>>,
|
||||
|
||||
@@ -45,22 +43,16 @@ pub struct Workspace {
|
||||
|
||||
impl Workspace {
|
||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let titlebar = cx.new(|_| TitleBar::new());
|
||||
let dock =
|
||||
cx.new(|cx| DockArea::new(window, cx).panel_style(dock::panel::PanelStyle::TabBar));
|
||||
|
||||
let chat = ChatRegistry::global(cx);
|
||||
let current_user = cx.new(|_| None);
|
||||
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let nip65_state = nostr.read(cx).nip65_state();
|
||||
|
||||
// Titlebar
|
||||
let titlebar = cx.new(|_| TitleBar::new());
|
||||
|
||||
// Command bar
|
||||
let command_bar = cx.new(|cx| CommandBar::new(window, cx));
|
||||
|
||||
// Dock
|
||||
let dock =
|
||||
cx.new(|cx| DockArea::new(window, cx).panel_style(dock::panel::PanelStyle::TabBar));
|
||||
|
||||
let mut subscriptions = smallvec![];
|
||||
|
||||
subscriptions.push(
|
||||
@@ -124,7 +116,6 @@ impl Workspace {
|
||||
Self {
|
||||
titlebar,
|
||||
dock,
|
||||
command_bar,
|
||||
current_user,
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
@@ -213,7 +204,8 @@ impl Workspace {
|
||||
fn titlebar_left(&mut self, _window: &mut Window, cx: &Context<Self>) -> impl IntoElement {
|
||||
h_flex()
|
||||
.h(TITLEBAR_HEIGHT)
|
||||
.flex_1()
|
||||
.w(SIDEBAR_WIDTH)
|
||||
.flex_shrink_0()
|
||||
.justify_between()
|
||||
.gap_2()
|
||||
.when_some(self.current_user.read(cx).as_ref(), |this, public_key| {
|
||||
@@ -221,24 +213,30 @@ impl Workspace {
|
||||
let profile = persons.read(cx).get(public_key, cx);
|
||||
|
||||
this.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
Button::new("current-user")
|
||||
.child(Avatar::new(profile.avatar()).size(rems(1.25)))
|
||||
.child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
.small()
|
||||
.text_color(cx.theme().text_muted),
|
||||
),
|
||||
.small()
|
||||
.caret()
|
||||
.compact()
|
||||
.transparent()
|
||||
.popup_menu(move |this, _window, _cx| {
|
||||
this.label(profile.name())
|
||||
.separator()
|
||||
.menu("Profile", Box::new(ClosePanel))
|
||||
.menu("Backup", Box::new(ClosePanel))
|
||||
.menu("Themes", Box::new(ClosePanel))
|
||||
.menu("Settings", Box::new(ClosePanel))
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn titlebar_center(&mut self, _window: &mut Window, _cx: &Context<Self>) -> impl IntoElement {
|
||||
h_flex().flex_1().w_full().child(self.command_bar.clone())
|
||||
h_flex().h(TITLEBAR_HEIGHT).flex_1()
|
||||
}
|
||||
|
||||
fn titlebar_right(&mut self, _window: &mut Window, _cx: &Context<Self>) -> impl IntoElement {
|
||||
h_flex().flex_1()
|
||||
h_flex().h(TITLEBAR_HEIGHT).w(SIDEBAR_WIDTH).flex_shrink_0()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user