feat: redesign the sidebar and workspace (#53)
Reviewed-on: #53
This commit was merged in pull request #53.
This commit is contained in:
@@ -20,7 +20,7 @@ pub fn open(window: &mut Window, cx: &mut App) {
|
||||
.show_close(false)
|
||||
.overlay_closable(false)
|
||||
.keyboard(false)
|
||||
.title("Onboarding")
|
||||
.title("Get Started")
|
||||
.child(import.clone())
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,17 +2,18 @@ use chat::{ChatRegistry, Room, RoomKind};
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
App, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled,
|
||||
Subscription, Window, div, px,
|
||||
Subscription, WeakEntity, Window, div, px,
|
||||
};
|
||||
use nostr_sdk::prelude::*;
|
||||
use state::NostrRegistry;
|
||||
use theme::ActiveTheme;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock::{DockArea, DockPlacement, PanelHandle};
|
||||
use ui::input::{Input, InputEvent, InputState};
|
||||
use ui::{StyledExt, WindowExtension, v_flex};
|
||||
|
||||
pub fn open(window: &mut Window, cx: &mut App) {
|
||||
let view = cx.new(|cx| NewChat::new(window, cx));
|
||||
pub fn open(dock: WeakEntity<DockArea>, window: &mut Window, cx: &mut App) {
|
||||
let view = cx.new(|cx| NewChat::new(dock, window, cx));
|
||||
|
||||
window.open_modal(cx, move |this, _window, _cx| {
|
||||
this.width(px(420.)).title("New chat").child(view.clone())
|
||||
@@ -20,6 +21,9 @@ pub fn open(window: &mut Window, cx: &mut App) {
|
||||
}
|
||||
|
||||
pub struct NewChat {
|
||||
/// The dock a started chat opens in
|
||||
dock: WeakEntity<DockArea>,
|
||||
|
||||
/// Public key input
|
||||
input: Entity<InputState>,
|
||||
|
||||
@@ -31,7 +35,7 @@ pub struct NewChat {
|
||||
}
|
||||
|
||||
impl NewChat {
|
||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
fn new(dock: WeakEntity<DockArea>, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let input = cx.new(|cx| InputState::new(window, cx).placeholder("npub"));
|
||||
|
||||
let subscription = cx.subscribe_in(&input, window, |this, _input, event, window, cx| {
|
||||
@@ -41,6 +45,7 @@ impl NewChat {
|
||||
});
|
||||
|
||||
Self {
|
||||
dock,
|
||||
input,
|
||||
error: None,
|
||||
_subscription: Some(subscription),
|
||||
@@ -56,6 +61,8 @@ impl NewChat {
|
||||
};
|
||||
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let chat = ChatRegistry::global(cx);
|
||||
|
||||
let Some(current_user) = nostr.read(cx).current_user() else {
|
||||
self.set_error("You are not signed in", cx);
|
||||
return;
|
||||
@@ -70,12 +77,20 @@ impl NewChat {
|
||||
.organize(¤t_user)
|
||||
.kind(RoomKind::Ongoing);
|
||||
|
||||
let chat = ChatRegistry::global(cx);
|
||||
chat.update(cx, |chat, cx| {
|
||||
let room = chat.update(cx, |chat, cx| {
|
||||
let room = cx.new(|_| room);
|
||||
chat.emit_room(&room, window, cx);
|
||||
chat.track_room(&room, cx);
|
||||
room
|
||||
});
|
||||
|
||||
ui::dock::add_panel_to(
|
||||
&self.dock,
|
||||
PanelHandle::new(chat_ui::init(room.downgrade(), window, cx)),
|
||||
DockPlacement::Center,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
window.close_modal(cx);
|
||||
}
|
||||
|
||||
|
||||
+18
-268
@@ -1,4 +1,3 @@
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use ::settings::AppSettings;
|
||||
@@ -6,25 +5,20 @@ use anyhow::Error;
|
||||
use auto_update::AutoUpdater;
|
||||
use chat::{ChatEvent, ChatRegistry};
|
||||
use common::download_dir;
|
||||
use community::{CommunityEvent, CommunityRegistry};
|
||||
use community_ui::CommunityPanel;
|
||||
use device::{DeviceEvent, DeviceRegistry};
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
Action, AnyElement, App, AppContext, Context, Entity, InteractiveElement, IntoElement,
|
||||
ParentElement, Render, SharedString, Styled, Subscription, Task, WeakEntity, Window, div, px,
|
||||
Action, App, AppContext, Context, Entity, InteractiveElement, IntoElement, ParentElement,
|
||||
Render, SharedString, Styled, Subscription, Task, Window, div, px,
|
||||
};
|
||||
use nostr_sdk::prelude::*;
|
||||
use person::{PersonRegistry, shorten_pubkey};
|
||||
use serde::Deserialize;
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use state::{NostrRegistry, StateEvent};
|
||||
use theme::{ActiveTheme, SIDEBAR_WIDTH, Theme, ThemeRegistry};
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock::{self, ClosePanel, DockArea, DockLayout, DockPlacement, Panel, PanelHandle};
|
||||
use ui::menu::{DropdownMenu, PopupMenuItem};
|
||||
use ui::dock::{self, DockArea, DockLayout, DockPlacement, Panel, PanelHandle};
|
||||
use ui::notification::{Notification, NotificationKind};
|
||||
use ui::{Icon, IconName, Root, Sizable, WindowExtension, h_flex, v_flex};
|
||||
use ui::{IconName, Root, Sizable, WindowExtension, h_flex, v_flex};
|
||||
|
||||
use crate::dialogs::restore::RestoreEncryption;
|
||||
use crate::dialogs::{new_chat, new_community, settings};
|
||||
@@ -71,9 +65,6 @@ enum Command {
|
||||
|
||||
pub struct Workspace {
|
||||
dock: Entity<DockArea>,
|
||||
title_bar_chrome: Rc<dock::TitleBarChrome>,
|
||||
/// The community panel currently docked, if any
|
||||
community_panel: Option<WeakEntity<CommunityPanel>>,
|
||||
/// Async tasks
|
||||
tasks: Vec<Task<Result<(), Error>>>,
|
||||
/// Event subscriptions
|
||||
@@ -83,12 +74,11 @@ pub struct Workspace {
|
||||
impl Workspace {
|
||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let chat = ChatRegistry::global(cx);
|
||||
let communities = CommunityRegistry::global(cx);
|
||||
let device = DeviceRegistry::global(cx);
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
|
||||
let sidebar = cx.new(|cx| Sidebar::new(window, cx));
|
||||
let (dock, title_bar_chrome) = dock::dock_area("coop", window, cx);
|
||||
let (dock, _) = dock::dock_area("coop", window, cx);
|
||||
let sidebar = cx.new(|cx| Sidebar::new(window, dock.downgrade(), cx));
|
||||
|
||||
let mut subscriptions = smallvec![];
|
||||
|
||||
@@ -164,7 +154,7 @@ impl Workspace {
|
||||
|
||||
subscriptions.push(
|
||||
// Observe all events emitted by the chat registry
|
||||
cx.subscribe_in(&chat, window, move |this, chat, ev, window, cx| {
|
||||
cx.subscribe_in(&chat, window, move |_this, _chat, ev, window, cx| {
|
||||
match ev {
|
||||
ChatEvent::InboxRelayNotFound => {
|
||||
const MSG: &str = "Messaging Relays not found. Cannot receive messages.";
|
||||
@@ -187,28 +177,6 @@ impl Workspace {
|
||||
cx,
|
||||
);
|
||||
}
|
||||
ChatEvent::OpenRoom(id) => {
|
||||
if let Some(room) = chat.read(cx).room(id, cx) {
|
||||
this.add_panel_to_dock(
|
||||
chat_ui::init(room, window, cx),
|
||||
DockPlacement::Center,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
ChatEvent::CloseRoom(..) => {
|
||||
this.dock.update(cx, |area, cx| {
|
||||
// Force focus to the tab panel
|
||||
ui::dock::focus_tab_panel(area, window, cx);
|
||||
|
||||
// Dispatch the close panel action
|
||||
cx.defer_in(window, |_, window, cx| {
|
||||
window.dispatch_action(Box::new(ClosePanel), cx);
|
||||
window.close_all_modals(cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
ChatEvent::Error(error) => {
|
||||
window.push_notification(Notification::error(error).autohide(false), cx);
|
||||
}
|
||||
@@ -217,71 +185,23 @@ impl Workspace {
|
||||
}),
|
||||
);
|
||||
|
||||
subscriptions.push(
|
||||
// Observe all events emitted by the community registry
|
||||
cx.subscribe_in(
|
||||
&communities,
|
||||
window,
|
||||
move |this, communities, event, window, cx| match event {
|
||||
CommunityEvent::Open(id) => {
|
||||
if let Some(community) = communities.read(cx).community(id) {
|
||||
let panel = community_ui::init(community, window, cx);
|
||||
|
||||
this.community_panel = Some(panel.downgrade());
|
||||
this.add_panel_to_dock(panel, DockPlacement::Center, window, cx);
|
||||
}
|
||||
}
|
||||
CommunityEvent::Close(_) => {
|
||||
let Some(panel) = this
|
||||
.community_panel
|
||||
.take()
|
||||
.and_then(|panel| panel.upgrade())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
this.dock.update(cx, |area, cx| {
|
||||
ui::dock::add_panel(
|
||||
area,
|
||||
PanelHandle::new(panel),
|
||||
DockPlacement::Center,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
ui::dock::focus_tab_panel(area, window, cx);
|
||||
|
||||
cx.defer_in(window, |_, window, cx| {
|
||||
window.dispatch_action(Box::new(ClosePanel), cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
cx.defer_in(window, move |this, window, cx| {
|
||||
let sidebar = PanelHandle::new(sidebar);
|
||||
|
||||
this.dock.update(cx, |area, cx| {
|
||||
let left = DockLayout::tabs().panel_view(Arc::new(sidebar), cx);
|
||||
area.set_dock(DockPlacement::Left, left, window, cx);
|
||||
area.set_dock_size(DockPlacement::Left, SIDEBAR_WIDTH, window, cx);
|
||||
});
|
||||
|
||||
let greeter = PanelHandle::new(greeter::init(window, cx));
|
||||
let center = DockLayout::v_split()
|
||||
.child(DockLayout::tabs().panel_view(Arc::new(greeter), cx), None);
|
||||
|
||||
this.dock.update(cx, |area, cx| {
|
||||
area.set_center(center, window, cx);
|
||||
this.dock.update(cx, |this, cx| {
|
||||
let left = DockLayout::tabs().panel_view(Arc::new(sidebar), cx);
|
||||
let center = DockLayout::v_split()
|
||||
.child(DockLayout::tabs().panel_view(Arc::new(greeter), cx), None);
|
||||
|
||||
this.set_dock(DockPlacement::Left, left, window, cx);
|
||||
this.set_dock_size(DockPlacement::Left, SIDEBAR_WIDTH, window, cx);
|
||||
this.set_center(center, window, cx);
|
||||
});
|
||||
});
|
||||
|
||||
Self {
|
||||
dock,
|
||||
title_bar_chrome,
|
||||
community_panel: None,
|
||||
tasks: vec![],
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
@@ -365,10 +285,11 @@ impl Workspace {
|
||||
self.add_panel_to_dock(browse::init(window, cx), DockPlacement::Center, window, cx);
|
||||
}
|
||||
Command::ShowSearch => {
|
||||
self.add_panel_to_dock(search::init(window, cx), DockPlacement::Center, window, cx);
|
||||
let panel = search::init(self.dock.downgrade(), window, cx);
|
||||
self.add_panel_to_dock(panel, DockPlacement::Center, window, cx);
|
||||
}
|
||||
Command::NewChat => {
|
||||
new_chat::open(window, cx);
|
||||
new_chat::open(self.dock.downgrade(), window, cx);
|
||||
}
|
||||
Command::NewCommunity => {
|
||||
new_community::open(window, cx);
|
||||
@@ -583,174 +504,6 @@ impl Workspace {
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
fn titlebar_right(_window: &mut Window, cx: &mut App) -> AnyElement {
|
||||
let auto_updater = AutoUpdater::try_global(cx);
|
||||
let chat = ChatRegistry::global(cx);
|
||||
let nip4e_enabled = AppSettings::get_nip4e(cx);
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
|
||||
let Some(public_key) = nostr.read(cx).current_user() else {
|
||||
return div().into_any_element();
|
||||
};
|
||||
|
||||
let persons = PersonRegistry::global(cx);
|
||||
let profile = persons.read(cx).get(&public_key, cx);
|
||||
let announcement = profile.announcement();
|
||||
|
||||
let updater_status = auto_updater.as_ref().and_then(|updater| {
|
||||
let updater = updater.read(cx);
|
||||
(!updater.idle()).then(|| updater.status())
|
||||
});
|
||||
|
||||
let staged_update = auto_updater
|
||||
.as_ref()
|
||||
.is_some_and(|updater| updater.read(cx).staged());
|
||||
|
||||
h_flex()
|
||||
.when(!cx.theme().platform.is_mac(), |this| this.pr_2())
|
||||
.gap_2()
|
||||
.when_some(updater_status, |this, status| {
|
||||
this.child(div().text_xs().italic().child(status))
|
||||
})
|
||||
.when(staged_update, |this| {
|
||||
this.child(
|
||||
Button::new("restart-to-update")
|
||||
.label("Restart to Update")
|
||||
.tooltip("Quit and relaunch into the installed update")
|
||||
.small()
|
||||
.ghost()
|
||||
.on_click(|_event, _window, cx| {
|
||||
if let Some(auto_updater) = AutoUpdater::try_global(cx) {
|
||||
auto_updater.update(cx, |this, cx| this.restart(cx));
|
||||
}
|
||||
}),
|
||||
)
|
||||
})
|
||||
.when(nip4e_enabled, |this| {
|
||||
this.child(
|
||||
Button::new("key")
|
||||
.icon(IconName::UserKey)
|
||||
.tooltip("Decoupled encryption key")
|
||||
.small()
|
||||
.ghost()
|
||||
.dropdown_menu(move |this, _window, _cx| {
|
||||
this.min_w(px(260.))
|
||||
.label("Encryption Key")
|
||||
.when_some(announcement.as_ref(), |this, announcement| {
|
||||
let name = announcement.client_name();
|
||||
let pkey = shorten_pubkey(announcement.public_key(), 8);
|
||||
|
||||
this.item(PopupMenuItem::element(move |_window, cx| {
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.text_sm()
|
||||
.child(
|
||||
Icon::new(IconName::Device)
|
||||
.small()
|
||||
.text_color(cx.theme().icon_muted),
|
||||
)
|
||||
.child(name.clone())
|
||||
}))
|
||||
.item(
|
||||
PopupMenuItem::element(move |_window, cx| {
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.text_sm()
|
||||
.child(
|
||||
Icon::new(IconName::UserKey)
|
||||
.small()
|
||||
.text_color(cx.theme().icon_muted),
|
||||
)
|
||||
.child(SharedString::from(pkey.clone()))
|
||||
}),
|
||||
)
|
||||
})
|
||||
.separator()
|
||||
.menu_with_icon(
|
||||
"Backup",
|
||||
IconName::Shield,
|
||||
Box::new(Command::BackupEncryption),
|
||||
)
|
||||
.menu_with_icon(
|
||||
"Restore from secret key",
|
||||
IconName::Usb,
|
||||
Box::new(Command::ImportEncryption),
|
||||
)
|
||||
.separator()
|
||||
.menu_with_icon(
|
||||
"Reload",
|
||||
IconName::Refresh,
|
||||
Box::new(Command::RefreshEncryption),
|
||||
)
|
||||
.menu_with_icon(
|
||||
"Reset",
|
||||
IconName::Warning,
|
||||
Box::new(Command::ResetEncryption),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
Button::new("inbox")
|
||||
.icon(IconName::Inbox)
|
||||
.small()
|
||||
.ghost()
|
||||
.dropdown_menu(move |this, _window, cx| {
|
||||
let urls: Vec<(SharedString, SharedString)> = profile
|
||||
.messaging_relays()
|
||||
.iter()
|
||||
.map(|url| {
|
||||
(
|
||||
SharedString::from(url.to_string()),
|
||||
chat.read(cx).count_messages(url).to_string().into(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Header
|
||||
let menu = this.min_w(px(260.)).label("Messaging Relays");
|
||||
|
||||
// Content
|
||||
let menu = urls.into_iter().fold(menu, |this, (url, count)| {
|
||||
this.item(PopupMenuItem::element(move |_window, cx| {
|
||||
h_flex()
|
||||
.px_1()
|
||||
.w_full()
|
||||
.text_sm()
|
||||
.justify_between()
|
||||
.child(url.clone())
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(count.clone()),
|
||||
)
|
||||
}))
|
||||
});
|
||||
|
||||
// Footer
|
||||
menu.separator()
|
||||
.menu_with_icon(
|
||||
"Manage gossip relays",
|
||||
IconName::Relay,
|
||||
Box::new(Command::ShowRelayList),
|
||||
)
|
||||
.menu_with_icon(
|
||||
"Manage messaging relays",
|
||||
IconName::Relay,
|
||||
Box::new(Command::ShowMessaging),
|
||||
)
|
||||
.separator()
|
||||
.menu_with_icon(
|
||||
"Reload",
|
||||
IconName::Refresh,
|
||||
Box::new(Command::RefreshMessagingRelays),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Workspace {
|
||||
@@ -758,9 +511,6 @@ impl Render for Workspace {
|
||||
let modal_layer = Root::render_modal_layer(window, cx);
|
||||
let notification_layer = Root::render_notification_layer(window, cx);
|
||||
|
||||
// Render the title bar chrome
|
||||
self.title_bar_chrome.set_trailing(Self::titlebar_right);
|
||||
|
||||
div()
|
||||
.id("workspace")
|
||||
.on_action(cx.listener(Self::on_command))
|
||||
|
||||
@@ -58,13 +58,16 @@ impl Panel for GreeterPanel {
|
||||
}
|
||||
|
||||
fn title(&self, cx: &App) -> AnyElement {
|
||||
div()
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.text_xs()
|
||||
.child(
|
||||
svg()
|
||||
.path("brand/coop.svg")
|
||||
.size_4()
|
||||
.text_color(cx.theme().text_muted),
|
||||
)
|
||||
.child("Welcome")
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ use common::DebouncedDelay;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
AnyElement, App, AppContext, Context, ElementId, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Task, Window, div,
|
||||
uniform_list,
|
||||
IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Task, WeakEntity,
|
||||
Window, div, uniform_list,
|
||||
};
|
||||
use instant::Duration;
|
||||
use nostr_sdk::prelude::*;
|
||||
@@ -17,22 +17,25 @@ use smallvec::{SmallVec, smallvec};
|
||||
use state::{FIND_DELAY, NostrRegistry};
|
||||
use theme::ActiveTheme;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock::{Panel, PanelEvent};
|
||||
use ui::dock::{DockArea, DockPlacement, Panel, PanelEvent, PanelHandle};
|
||||
use ui::input::{Input, InputEvent, InputState};
|
||||
use ui::nav::Nav;
|
||||
use ui::notification::Notification;
|
||||
use ui::{Icon, IconName, Selectable, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
|
||||
|
||||
use crate::sidebar::{TreeRow, TreeRowKind};
|
||||
use crate::sidebar::nav_avatar;
|
||||
|
||||
const INPUT_PLACEHOLDER: &str = "Find or start a conversation";
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<SearchPanel> {
|
||||
cx.new(|cx| SearchPanel::new(window, cx))
|
||||
pub fn init(dock: WeakEntity<DockArea>, window: &mut Window, cx: &mut App) -> Entity<SearchPanel> {
|
||||
cx.new(|cx| SearchPanel::new(dock, window, cx))
|
||||
}
|
||||
|
||||
pub struct SearchPanel {
|
||||
name: SharedString,
|
||||
focus_handle: FocusHandle,
|
||||
/// The dock a started chat opens in
|
||||
dock: WeakEntity<DockArea>,
|
||||
|
||||
/// Find input state
|
||||
find_input: Entity<InputState>,
|
||||
@@ -63,7 +66,7 @@ pub struct SearchPanel {
|
||||
}
|
||||
|
||||
impl SearchPanel {
|
||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
fn new(dock: WeakEntity<DockArea>, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let contact_list = cx.new(|_| None);
|
||||
let selected_pkeys = cx.new(|_| HashSet::new());
|
||||
let find_results = cx.new(|_| None);
|
||||
@@ -107,6 +110,7 @@ impl SearchPanel {
|
||||
Self {
|
||||
name: "Search".into(),
|
||||
focus_handle: cx.focus_handle(),
|
||||
dock,
|
||||
find_input,
|
||||
find_debouncer: DebouncedDelay::new(),
|
||||
find_results,
|
||||
@@ -285,6 +289,7 @@ impl SearchPanel {
|
||||
fn create_room(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let chat = ChatRegistry::global(cx);
|
||||
let async_chat = chat.downgrade();
|
||||
let dock = self.dock.clone();
|
||||
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let Some(public_key) = nostr.read(cx).current_user() else {
|
||||
@@ -295,14 +300,26 @@ impl SearchPanel {
|
||||
let receivers = self.get_selected(cx);
|
||||
|
||||
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||
// Create a new room and emit it
|
||||
async_chat.update_in(cx, |this, _window, cx| {
|
||||
// Create a new room and register it
|
||||
let room = async_chat.update_in(cx, |chat, _window, cx| {
|
||||
let room = cx.new(|_| {
|
||||
Room::new(public_key, receivers)
|
||||
.organize(&public_key)
|
||||
.kind(RoomKind::Ongoing)
|
||||
});
|
||||
this.emit_room(&room, _window, cx);
|
||||
chat.track_room(&room, cx);
|
||||
room
|
||||
})?;
|
||||
|
||||
// Open it in the dock
|
||||
cx.update(|window, cx| {
|
||||
ui::dock::add_panel_to(
|
||||
&dock,
|
||||
PanelHandle::new(chat_ui::init(room.downgrade(), window, cx)),
|
||||
DockPlacement::Center,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})?;
|
||||
|
||||
// Reset the find panel
|
||||
@@ -341,13 +358,17 @@ impl SearchPanel {
|
||||
this.select(&pkey_clone, cx);
|
||||
});
|
||||
|
||||
TreeRow::new(
|
||||
ElementId::NamedInteger("search-result".into(), (range.start + ix) as u64),
|
||||
TreeRowKind::Room,
|
||||
profile.name(),
|
||||
Nav::new(ElementId::NamedInteger(
|
||||
"search-result".into(),
|
||||
(range.start + ix) as u64,
|
||||
))
|
||||
.label(profile.name())
|
||||
.text_sm()
|
||||
.font_medium()
|
||||
.when_some(
|
||||
nav_avatar(Some(profile.avatar_seed()), profile.avatar(), cx),
|
||||
|this, avatar| this.prefix(avatar),
|
||||
)
|
||||
.avatar(profile.avatar_seed())
|
||||
.picture(profile.avatar())
|
||||
.on_click(handler)
|
||||
.selected(selected)
|
||||
.into_any_element()
|
||||
@@ -382,13 +403,17 @@ impl SearchPanel {
|
||||
this.select(&pkey_clone, cx);
|
||||
});
|
||||
|
||||
TreeRow::new(
|
||||
ElementId::NamedInteger("contact".into(), (range.start + ix) as u64),
|
||||
TreeRowKind::Room,
|
||||
profile.name().trim(),
|
||||
Nav::new(ElementId::NamedInteger(
|
||||
"contact".into(),
|
||||
(range.start + ix) as u64,
|
||||
))
|
||||
.label(profile.name().trim())
|
||||
.text_sm()
|
||||
.font_medium()
|
||||
.when_some(
|
||||
nav_avatar(Some(profile.avatar_seed()), profile.avatar(), cx),
|
||||
|this, avatar| this.prefix(avatar),
|
||||
)
|
||||
.avatar(profile.avatar_seed())
|
||||
.picture(profile.avatar())
|
||||
.on_click(handler)
|
||||
.selected(selected)
|
||||
.into_any_element()
|
||||
|
||||
+602
-626
File diff suppressed because it is too large
Load Diff
@@ -1,74 +0,0 @@
|
||||
use gpui::{App, InteractiveElement, IntoElement, ParentElement, Styled, Window, div, svg};
|
||||
use theme::{ActiveTheme, TABBAR_HEIGHT};
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::{StyledExt, h_flex, title_bar_drag_handlers, v_flex};
|
||||
|
||||
use crate::dialogs::import;
|
||||
|
||||
const TITLE: &str = "Welcome to Coop!";
|
||||
const DESCRIPTION: &str = "Chat Freely, Stay Private on Nostr.";
|
||||
|
||||
pub(super) fn render(window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
v_flex()
|
||||
.size_full()
|
||||
.relative()
|
||||
.bg(cx.theme().surface_background)
|
||||
.child(title_bar_drag_handlers(
|
||||
div()
|
||||
.id("onboarding-drag")
|
||||
.absolute()
|
||||
.top_0()
|
||||
.left_0()
|
||||
.h(TABBAR_HEIGHT)
|
||||
.w_full(),
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.child(
|
||||
v_flex()
|
||||
.size_full()
|
||||
.justify_end()
|
||||
.gap_4()
|
||||
.p_4()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
svg()
|
||||
.path("brand/coop.svg")
|
||||
.size_8()
|
||||
.text_color(cx.theme().icon_muted),
|
||||
)
|
||||
.child(
|
||||
v_flex().child(div().font_semibold().child(TITLE)).child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(DESCRIPTION),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.w_full()
|
||||
.child(
|
||||
Button::new("join-now")
|
||||
.label("Join now")
|
||||
.primary()
|
||||
.font_semibold()
|
||||
.h_8()
|
||||
.w_full(),
|
||||
)
|
||||
.child(
|
||||
Button::new("import-identity")
|
||||
.label("Import identity")
|
||||
.secondary()
|
||||
.font_semibold()
|
||||
.h_8()
|
||||
.w_full()
|
||||
.on_click(|_event, window, cx| import::open(window, cx)),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -1,121 +1,38 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{App, InteractiveElement, IntoElement, ParentElement, RenderOnce, Styled, Window, div};
|
||||
use theme::ActiveTheme;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::{IconName, Selectable, h_flex};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum SidebarTab {
|
||||
Recents,
|
||||
Chats,
|
||||
Inbox,
|
||||
Communities,
|
||||
}
|
||||
|
||||
impl SidebarTab {
|
||||
pub const ALL: [SidebarTab; 3] = [Self::Recents, Self::Chats, Self::Communities];
|
||||
pub const ALL: [SidebarTab; 2] = [Self::Inbox, Self::Communities];
|
||||
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Recents => "Recents",
|
||||
Self::Chats => "Chats",
|
||||
Self::Inbox => "Inbox",
|
||||
Self::Communities => "Communities",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn icon(self) -> IconName {
|
||||
/// Heading for the tab's list of items.
|
||||
pub fn list_title(self) -> &'static str {
|
||||
match self {
|
||||
Self::Recents => IconName::History,
|
||||
Self::Chats => IconName::Message,
|
||||
Self::Communities => IconName::Group,
|
||||
Self::Inbox => "Direct Messages",
|
||||
Self::Communities => "Communities",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn list_id(self) -> &'static str {
|
||||
match self {
|
||||
Self::Recents => "sidebar-recents",
|
||||
Self::Chats => "sidebar-chats",
|
||||
Self::Inbox => "sidebar-inbox",
|
||||
Self::Communities => "sidebar-communities",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn index(self) -> usize {
|
||||
match self {
|
||||
Self::Recents => 0,
|
||||
Self::Chats => 1,
|
||||
Self::Communities => 2,
|
||||
Self::Inbox => 0,
|
||||
Self::Communities => 1,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn chat(self) -> bool {
|
||||
matches!(self, Self::Chats)
|
||||
}
|
||||
|
||||
pub fn community(self) -> bool {
|
||||
matches!(self, Self::Communities)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub struct TabBar {
|
||||
active: SidebarTab,
|
||||
on_select: Option<Rc<dyn Fn(SidebarTab, &mut Window, &mut App)>>,
|
||||
}
|
||||
|
||||
impl TabBar {
|
||||
pub fn new(active: SidebarTab) -> Self {
|
||||
Self {
|
||||
active,
|
||||
on_select: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_select(
|
||||
mut self,
|
||||
handler: impl Fn(SidebarTab, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_select = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for TabBar {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let Self { active, on_select } = self;
|
||||
|
||||
div()
|
||||
.id("sidebar-tabs")
|
||||
.absolute()
|
||||
.bottom_3()
|
||||
.left_0()
|
||||
.w_full()
|
||||
.px_4()
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.p_1()
|
||||
.gap_1()
|
||||
.rounded_full()
|
||||
.bg(cx.theme().background)
|
||||
.when(cx.theme().shadow, |this| this.shadow_md())
|
||||
.children(SidebarTab::ALL.into_iter().map(|tab| {
|
||||
let on_select = on_select.clone();
|
||||
|
||||
Button::new(format!("tab-{}", tab.list_id()))
|
||||
.icon(tab.icon())
|
||||
.ghost()
|
||||
.flex_1()
|
||||
.rounded()
|
||||
.selected(tab == active)
|
||||
.tooltip(tab.label())
|
||||
.on_click(move |_event, window, cx| {
|
||||
if let Some(on_select) = on_select.as_ref() {
|
||||
on_select(tab, window, cx);
|
||||
}
|
||||
})
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,235 +0,0 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use chat::Room;
|
||||
use community::Community;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
App, ClickEvent, ElementId, Entity, ImageSource, InteractiveElement, IntoElement,
|
||||
ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window, div, px,
|
||||
};
|
||||
use settings::AppSettings;
|
||||
use theme::ActiveTheme;
|
||||
use ui::avatar::{Avatar, PixelAvatar};
|
||||
use ui::{Icon, IconName, Selectable, Sizable, StyledExt, h_flex};
|
||||
|
||||
use super::tab::SidebarTab;
|
||||
|
||||
pub enum SidebarRow {
|
||||
Section {
|
||||
label: SharedString,
|
||||
count: usize,
|
||||
},
|
||||
Room {
|
||||
room: Entity<Room>,
|
||||
},
|
||||
Community {
|
||||
community: Entity<Community>,
|
||||
},
|
||||
Action {
|
||||
label: SharedString,
|
||||
tab: SidebarTab,
|
||||
},
|
||||
Hint {
|
||||
text: SharedString,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum TreeRowKind {
|
||||
Section,
|
||||
Room,
|
||||
Community,
|
||||
Action,
|
||||
Hint,
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct TreeRow {
|
||||
id: ElementId,
|
||||
kind: TreeRowKind,
|
||||
label: SharedString,
|
||||
avatar: Option<SharedString>,
|
||||
picture: Option<ImageSource>,
|
||||
icon: Option<IconName>,
|
||||
count: Option<usize>,
|
||||
created_at: Option<SharedString>,
|
||||
selected: bool,
|
||||
#[allow(clippy::type_complexity)]
|
||||
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
|
||||
}
|
||||
|
||||
impl TreeRow {
|
||||
pub fn new(
|
||||
id: impl Into<ElementId>,
|
||||
kind: TreeRowKind,
|
||||
label: impl Into<SharedString>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
kind,
|
||||
label: label.into(),
|
||||
avatar: None,
|
||||
picture: None,
|
||||
icon: None,
|
||||
count: None,
|
||||
created_at: None,
|
||||
selected: false,
|
||||
on_click: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the seed for the row's generated avatar.
|
||||
pub fn avatar(mut self, seed: impl Into<SharedString>) -> Self {
|
||||
self.avatar = Some(seed.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Shows `picture` instead of the generated avatar.
|
||||
pub fn picture(mut self, picture: Option<impl Into<ImageSource>>) -> Self {
|
||||
self.picture = picture.map(Into::into);
|
||||
self
|
||||
}
|
||||
|
||||
/// Shows `icon` in the avatar slot when the row has no avatar or picture.
|
||||
pub fn icon(mut self, icon: IconName) -> Self {
|
||||
self.icon = Some(icon);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn count(mut self, count: usize) -> Self {
|
||||
self.count = Some(count);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn created_at(mut self, created_at: impl Into<SharedString>) -> Self {
|
||||
self.created_at = Some(created_at.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_click(
|
||||
mut self,
|
||||
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_click = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Selectable for TreeRow {
|
||||
fn selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
|
||||
fn is_selected(&self) -> bool {
|
||||
self.selected
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for TreeRow {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let hide_avatar = AppSettings::get_hide_avatar(cx);
|
||||
|
||||
let is_section = self.kind == TreeRowKind::Section;
|
||||
let is_room = self.kind == TreeRowKind::Room;
|
||||
let is_community = self.kind == TreeRowKind::Community;
|
||||
let is_action = self.kind == TreeRowKind::Action;
|
||||
let is_hint = self.kind == TreeRowKind::Hint;
|
||||
let is_selected = self.selected;
|
||||
|
||||
let avatar = if hide_avatar {
|
||||
None
|
||||
} else {
|
||||
match (self.avatar, self.picture) {
|
||||
(None, None) => None,
|
||||
(seed, Some(picture)) => Some(
|
||||
Avatar::from_source(picture)
|
||||
.when_some(seed, |avatar, seed| avatar.seed(seed))
|
||||
.xsmall()
|
||||
.flex_shrink_0()
|
||||
.into_any_element(),
|
||||
),
|
||||
(Some(seed), None) => Some(
|
||||
PixelAvatar::new(seed)
|
||||
.xsmall()
|
||||
.flex_shrink_0()
|
||||
.into_any_element(),
|
||||
),
|
||||
}
|
||||
};
|
||||
|
||||
let avatar = avatar.or_else(|| {
|
||||
self.icon.map(|icon| {
|
||||
h_flex()
|
||||
.flex_shrink_0()
|
||||
.w(px(20.))
|
||||
.justify_center()
|
||||
.text_color(cx.theme().icon_muted)
|
||||
.child(Icon::new(icon).small())
|
||||
.into_any_element()
|
||||
})
|
||||
});
|
||||
|
||||
h_flex()
|
||||
.id(self.id)
|
||||
.h_8()
|
||||
.w_full()
|
||||
.px_2()
|
||||
.gap_2()
|
||||
.rounded(cx.theme().radius)
|
||||
.when(is_section, |this| {
|
||||
this.text_xs()
|
||||
.text_color(cx.theme().text_placeholder)
|
||||
.font_semibold()
|
||||
})
|
||||
.when(is_room || is_community, |this| this.text_sm())
|
||||
.when(is_action, |this| {
|
||||
this.text_sm().text_color(cx.theme().text_muted)
|
||||
})
|
||||
.when(is_hint, |this| {
|
||||
this.text_xs()
|
||||
.font_normal()
|
||||
.text_color(cx.theme().text_placeholder)
|
||||
})
|
||||
.when_some(avatar, |this, avatar| this.child(avatar))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.flex_1()
|
||||
.child(
|
||||
div()
|
||||
.truncate()
|
||||
.min_w_0()
|
||||
.when(is_room, |this| this.font_medium())
|
||||
.child(self.label),
|
||||
)
|
||||
.when(is_selected, |this| {
|
||||
this.child(
|
||||
Icon::new(IconName::CheckCircle)
|
||||
.small()
|
||||
.flex_shrink_0()
|
||||
.text_color(cx.theme().icon_accent),
|
||||
)
|
||||
})
|
||||
.when_some(self.count, |this, count| {
|
||||
this.child(div().flex_shrink_0().font_normal().child(count.to_string()))
|
||||
})
|
||||
.when_some(self.created_at, |this, created_at| {
|
||||
this.child(div().flex_1()).child(
|
||||
div()
|
||||
.flex_shrink_0()
|
||||
.text_color(cx.theme().text_placeholder)
|
||||
.text_xs()
|
||||
.child(created_at),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.when_some(self.on_click, |this, handler| {
|
||||
this.cursor_pointer()
|
||||
.when(!is_section, |this| {
|
||||
this.hover(|this| this.bg(cx.theme().ghost_element_hover))
|
||||
})
|
||||
.on_click(move |event, window, cx| handler(event, window, cx))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{AnyElement, App, ImageSource, IntoElement, ParentElement, SharedString, Styled, px};
|
||||
use settings::AppSettings;
|
||||
use theme::ActiveTheme;
|
||||
use ui::avatar::{Avatar, PixelAvatar};
|
||||
use ui::{Icon, IconName, Sizable, h_flex};
|
||||
|
||||
pub(crate) fn nav_avatar(
|
||||
seed: Option<impl Into<SharedString>>,
|
||||
picture: Option<impl Into<ImageSource>>,
|
||||
cx: &App,
|
||||
) -> Option<AnyElement> {
|
||||
if AppSettings::get_hide_avatar(cx) {
|
||||
return None;
|
||||
}
|
||||
|
||||
match (seed.map(Into::into), picture.map(Into::into)) {
|
||||
(None, None) => None,
|
||||
(seed, Some(picture)) => Some(
|
||||
Avatar::from_source(picture)
|
||||
.when_some(seed, |avatar, seed| avatar.seed(seed))
|
||||
.small()
|
||||
.flex_shrink_0()
|
||||
.into_any_element(),
|
||||
),
|
||||
(Some(seed), None) => Some(
|
||||
PixelAvatar::new(seed)
|
||||
.small()
|
||||
.flex_shrink_0()
|
||||
.into_any_element(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn nav_icon(icon: IconName, cx: &App) -> AnyElement {
|
||||
h_flex()
|
||||
.flex_shrink_0()
|
||||
.w(px(20.))
|
||||
.justify_center()
|
||||
.text_color(cx.theme().icon_muted)
|
||||
.child(Icon::new(icon).small())
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// Brand backgrounds shown behind the signed-out screen; one is picked at random.
|
||||
const SIGNED_OUT_BANNERS: [&str; 2] = ["brand/bg1.jpg", "brand/bg2.jpg"];
|
||||
|
||||
/// Pick one of the signed-out backgrounds at random.
|
||||
pub(crate) fn pick_banner() -> SharedString {
|
||||
let nanos = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|elapsed| elapsed.subsec_nanos())
|
||||
.unwrap_or_default();
|
||||
let index = nanos as usize % SIGNED_OUT_BANNERS.len();
|
||||
SIGNED_OUT_BANNERS[index].into()
|
||||
}
|
||||
Reference in New Issue
Block a user