diff --git a/Cargo.lock b/Cargo.lock
index 583adc7d..ae99581b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -561,7 +561,7 @@ dependencies = [
"num-traits",
"pastey",
"rayon",
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
"v_frame",
"y4m",
]
@@ -1788,6 +1788,46 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376"
+[[package]]
+name = "defmt"
+version = "0.3.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0963443817029b2024136fc4dd07a5107eb8f977eaf18fcd1fdeb11306b64ad"
+dependencies = [
+ "defmt 1.1.1",
+]
+
+[[package]]
+name = "defmt"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1"
+dependencies = [
+ "bitflags 1.3.2",
+ "defmt-macros",
+]
+
+[[package]]
+name = "defmt-macros"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8"
+dependencies = [
+ "defmt-parser",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "defmt-parser"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e"
+dependencies = [
+ "thiserror 2.0.21",
+]
+
[[package]]
name = "deranged"
version = "0.5.8"
@@ -1989,13 +2029,12 @@ dependencies = [
[[package]]
name = "encoding_rs"
-version = "0.8.41"
+version = "0.8.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b5ef0006ac9ab233c38522f5ae99cae3625151de8f706cacee1cba4b8e2832a"
+checksum = "8e985e0451871ad22fb8d2b6b076e2028a502a0d3950998c2c5c0a4f9b5d9679"
dependencies = [
"cfg-if",
"core_detect",
- "multiversion",
"multiversion_no_op",
"rustversion",
"scopeguard",
@@ -2145,10 +2184,12 @@ dependencies = [
[[package]]
name = "faster-hex"
-version = "0.10.0"
+version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7223ae2d2f179b803433d9c830478527e92b8117eab39460edae7f1614d9fb73"
+checksum = "04839bdf9d8c10f66806fad16b852fc72aab80873aebc3cb69d85b4fa41543ed"
dependencies = [
+ "autocfg",
+ "defmt 0.3.100",
"heapless 0.8.0",
"serde",
]
@@ -2685,7 +2726,7 @@ dependencies = [
"hashbrown 0.16.1",
"log",
"presser",
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
"windows 0.62.2",
]
@@ -2805,7 +2846,7 @@ dependencies = [
"spin 0.10.1",
"strum",
"taffy",
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
"ttf-parser",
"url",
"usvg",
@@ -3293,7 +3334,7 @@ dependencies = [
"serde",
"serde_json",
"sha2 0.10.9",
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
"tracing",
"ureq",
]
@@ -3624,16 +3665,17 @@ dependencies = [
[[package]]
name = "hyper-util"
-version = "0.1.20"
+version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
+checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff"
dependencies = [
- "base64 0.22.1",
+ "base64 0.23.1",
"bytes",
"futures-channel",
"futures-util",
"http",
"http-body",
+ "httparse",
"hyper",
"ipnet",
"libc",
@@ -3954,7 +3996,7 @@ dependencies = [
"jni-sys 0.4.1",
"log",
"simd_cesu8",
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
"walkdir",
"windows-link 0.2.1",
]
@@ -4210,9 +4252,9 @@ dependencies = [
[[package]]
name = "lru"
-version = "0.18.4"
+version = "0.18.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ff9840bcc50b71349309900da0ce7279aa336ae71d73250b07998932c7d97c25"
+checksum = "ef9ac18847474e638e3702b76c65d4eb93428471a74778ef0f1be711717f89b5"
[[package]]
name = "lru-slab"
@@ -4487,27 +4529,6 @@ dependencies = [
"pxfm",
]
-[[package]]
-name = "multiversion"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b4ca4bea16ffc3f443cf7d866912118196bfef4c6a1556ca00f9f9b00bb43f7c"
-dependencies = [
- "multiversion-macros",
-]
-
-[[package]]
-name = "multiversion-macros"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d416831a7317ef4b08bee00b69cbbb9c8763da7959a7026244d6266869f9c83"
-dependencies = [
- "proc-macro2",
- "quote",
- "rustversion",
- "syn 3.0.6",
-]
-
[[package]]
name = "multiversion_no_op"
version = "1.0.0"
@@ -4536,7 +4557,7 @@ dependencies = [
"once_cell",
"rustc-hash 1.1.0",
"spirv",
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
"unicode-ident",
]
@@ -5912,7 +5933,7 @@ dependencies = [
"rustc-hash 2.1.3",
"rustls",
"socket2",
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
"tokio",
"tracing",
"web-time",
@@ -5934,7 +5955,7 @@ dependencies = [
"rustls",
"rustls-pki-types",
"slab",
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
"tinyvec",
"tracing",
"web-time",
@@ -6102,7 +6123,7 @@ dependencies = [
"rand 0.9.5",
"rand_chacha 0.9.0",
"simd_helpers",
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
"v_frame",
"wasm-bindgen",
]
@@ -6500,9 +6521,9 @@ dependencies = [
[[package]]
name = "rustls-platform-verifier"
-version = "0.7.0"
+version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
+checksum = "1167586491e2b18b8bfbb293e8180ec17c201c4f076d7cb3070ca964e7598f98"
dependencies = [
"core-foundation 0.10.1",
"core-foundation-sys",
@@ -6521,9 +6542,9 @@ dependencies = [
[[package]]
name = "rustls-platform-verifier-android"
-version = "0.1.1"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
+checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f"
[[package]]
name = "rustls-webpki"
@@ -7426,7 +7447,7 @@ version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed071c670382e85fc2f48ae706492d8c338f4f89bf72520d32f8abfe880aade"
dependencies = [
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
"windows 0.61.3",
"windows-version",
]
@@ -7490,11 +7511,11 @@ dependencies = [
[[package]]
name = "thiserror"
-version = "2.0.20"
+version = "2.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
+checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
dependencies = [
- "thiserror-impl 2.0.20",
+ "thiserror-impl 2.0.21",
]
[[package]]
@@ -7510,9 +7531,9 @@ dependencies = [
[[package]]
name = "thiserror-impl"
-version = "2.0.20"
+version = "2.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
+checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
dependencies = [
"proc-macro2",
"quote",
@@ -7958,7 +7979,7 @@ dependencies = [
"rustls",
"rustls-pki-types",
"sha1",
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
"utf-8",
]
@@ -8641,7 +8662,7 @@ dependencies = [
"raw-window-handle",
"rustc-hash 1.1.0",
"smallvec",
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
"wgpu-core-deps-apple",
"wgpu-core-deps-emscripten",
"wgpu-core-deps-wasm",
@@ -8730,7 +8751,7 @@ dependencies = [
"raw-window-metal",
"renderdoc-sys",
"smallvec",
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
"wasm-bindgen",
"wayland-sys",
"web-sys",
@@ -8858,7 +8879,7 @@ checksum = "3a4df73e95feddb9ec1a7e9c2ca6323b8c97d5eeeff78d28f1eccdf19c882b24"
dependencies = [
"parking_lot",
"rayon",
- "thiserror 2.0.20",
+ "thiserror 2.0.21",
"windows 0.61.3",
"windows-future 0.2.1",
]
@@ -9810,18 +9831,18 @@ checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524"
[[package]]
name = "zerocopy"
-version = "0.8.57"
+version = "0.8.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d35102a9f36d089ccae9e4c6802bc118be4487b80aaffc0ab4e0cf5ce92d2873"
+checksum = "c17e8fafad82b542ff3717217ecdc736231b59e387768c9630123b4ce4d2db44"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
-version = "0.8.57"
+version = "0.8.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "146c01f5ab44258da43cf276c74a2763db2ff3969c9c652c3f2de07041d0b2bc"
+checksum = "595f56e044df4f46a0c9a626f65c3d99eb8488f7e8a8baa12dd76326d9710bf2"
dependencies = [
"proc-macro2",
"quote",
diff --git a/assets/brand/bg1.jpg b/assets/brand/bg1.jpg
new file mode 100644
index 00000000..b93eea22
Binary files /dev/null and b/assets/brand/bg1.jpg differ
diff --git a/assets/brand/bg2.jpg b/assets/brand/bg2.jpg
new file mode 100644
index 00000000..8f317235
Binary files /dev/null and b/assets/brand/bg2.jpg differ
diff --git a/assets/brand/headline.png b/assets/brand/headline.png
new file mode 100644
index 00000000..56b31520
Binary files /dev/null and b/assets/brand/headline.png differ
diff --git a/assets/icons/hashtag.svg b/assets/icons/hashtag.svg
new file mode 100644
index 00000000..2459408b
--- /dev/null
+++ b/assets/icons/hashtag.svg
@@ -0,0 +1,3 @@
+
diff --git a/crates/ui/src/icon.rs b/crates/ui/src/icon.rs
index fef5db14..e47f01df 100644
--- a/crates/ui/src/icon.rs
+++ b/crates/ui/src/icon.rs
@@ -67,6 +67,7 @@ pub enum IconName {
Ship,
Shield,
Group,
+ Hashtag,
History,
UserKey,
Upload,
@@ -148,6 +149,7 @@ impl IconNamed for IconName {
Self::Upload => "icons/upload.svg",
Self::Usb => "icons/usb.svg",
Self::Group => "icons/group.svg",
+ Self::Hashtag => "icons/hashtag.svg",
Self::History => "icons/history.svg",
Self::PanelLeft => "icons/panel-left.svg",
Self::PanelLeftOpen => "icons/panel-left-open.svg",
diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs
index 626216e5..8815144d 100644
--- a/crates/ui/src/lib.rs
+++ b/crates/ui/src/lib.rs
@@ -19,6 +19,7 @@ pub mod input;
pub mod menu;
pub mod message;
pub mod modal;
+pub mod nav;
pub mod nav_item;
pub mod notification;
pub mod popover;
diff --git a/crates/ui/src/nav.rs b/crates/ui/src/nav.rs
new file mode 100644
index 00000000..21a17538
--- /dev/null
+++ b/crates/ui/src/nav.rs
@@ -0,0 +1,123 @@
+use std::rc::Rc;
+
+use gpui::prelude::FluentBuilder as _;
+use gpui::{
+ AnyElement, App, ClickEvent, ElementId, InteractiveElement, IntoElement, ParentElement,
+ RenderOnce, SharedString, StatefulInteractiveElement as _, StyleRefinement, Styled, Window,
+ div,
+};
+use theme::ActiveTheme;
+
+use crate::{Selectable, StyledExt, h_flex, v_flex};
+
+/// A single navigation row, such as an entry in a sidebar list.
+#[derive(IntoElement)]
+pub struct Nav {
+ id: ElementId,
+ style: StyleRefinement,
+ prefix: Option,
+ label: SharedString,
+ suffix: Option,
+ selected: bool,
+ #[allow(clippy::type_complexity)]
+ on_click: Option>,
+}
+
+impl Nav {
+ pub fn new(id: impl Into) -> Self {
+ Self {
+ id: id.into(),
+ style: StyleRefinement::default(),
+ prefix: None,
+ label: SharedString::default(),
+ suffix: None,
+ selected: false,
+ on_click: None,
+ }
+ }
+
+ /// Sets the element shown before the label, such as an avatar or icon.
+ pub fn prefix(mut self, prefix: impl IntoElement) -> Self {
+ self.prefix = Some(prefix.into_any_element());
+ self
+ }
+
+ /// Sets the row's label.
+ pub fn label(mut self, label: impl Into) -> Self {
+ self.label = label.into();
+ self
+ }
+
+ /// Sets the element shown after the label, such as a timestamp or badge.
+ pub fn suffix(mut self, suffix: impl IntoElement) -> Self {
+ self.suffix = Some(suffix.into_any_element());
+ self
+ }
+
+ pub fn on_click(
+ mut self,
+ handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
+ ) -> Self {
+ self.on_click = Some(Rc::new(handler));
+ self
+ }
+}
+
+impl Selectable for Nav {
+ fn selected(mut self, selected: bool) -> Self {
+ self.selected = selected;
+ self
+ }
+
+ fn is_selected(&self) -> bool {
+ self.selected
+ }
+}
+
+impl Styled for Nav {
+ fn style(&mut self) -> &mut StyleRefinement {
+ &mut self.style
+ }
+}
+
+impl RenderOnce for Nav {
+ fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
+ let clickable = self.on_click.is_some();
+
+ v_flex()
+ .id(format!("nav-{}", self.id))
+ .w_full()
+ .h_10()
+ .child(
+ h_flex()
+ .id(self.id)
+ .h_9()
+ .w_full()
+ .px_1p5()
+ .gap_1p5()
+ .rounded(cx.theme().radius_lg)
+ .when_some(self.prefix, |this, prefix| this.child(prefix))
+ .child(
+ h_flex()
+ .gap_1()
+ .flex_1()
+ .child(div().truncate().min_w_0().child(self.label))
+ .child(div().flex_1())
+ .when_some(self.suffix, |this, suffix| {
+ this.child(div().flex_shrink_0().child(suffix))
+ }),
+ )
+ .when(clickable, |this| {
+ this.cursor_pointer()
+ .hover(|this| this.bg(cx.theme().ghost_element_hover))
+ .when(self.selected, |this| {
+ this.bg(cx.theme().ghost_element_active)
+ })
+ })
+ .when_some(self.on_click, |this, handler| {
+ this.on_click(move |event, window, cx| handler(event, window, cx))
+ })
+ .refine_style(&self.style),
+ )
+ }
+}
diff --git a/crates/workspace/src/dialogs/import.rs b/crates/workspace/src/dialogs/import.rs
index a43c7449..ed118245 100644
--- a/crates/workspace/src/dialogs/import.rs
+++ b/crates/workspace/src/dialogs/import.rs
@@ -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())
});
}
diff --git a/crates/workspace/src/panels/search.rs b/crates/workspace/src/panels/search.rs
index 64feab68..10b6db2e 100644
--- a/crates/workspace/src/panels/search.rs
+++ b/crates/workspace/src/panels/search.rs
@@ -19,10 +19,11 @@ use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants};
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";
@@ -357,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()
@@ -398,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()
diff --git a/crates/workspace/src/sidebar/mod.rs b/crates/workspace/src/sidebar/mod.rs
index c52254b8..4e19ffb8 100644
--- a/crates/workspace/src/sidebar/mod.rs
+++ b/crates/workspace/src/sidebar/mod.rs
@@ -2,12 +2,12 @@ use std::ops::Range;
use std::rc::Rc;
use auto_update::AutoUpdater;
-use chat::{ChatEvent, ChatRegistry, RoomKind};
+use chat::{ChatEvent, ChatRegistry, Room, RoomKind};
use common::TimestampExt;
use community::{ChannelId, Community, CommunityEvent, CommunityRegistry};
use gpui::prelude::FluentBuilder;
use gpui::{
- AnyElement, App, Context, Div, ElementId, Entity, EventEmitter, FocusHandle, Focusable,
+ AnyElement, App, Context, Div, Entity, EventEmitter, FocusHandle, Focusable,
InteractiveElement, IntoElement, ObjectFit, ParentElement, Render, SharedString, Stateful,
Styled, StyledImage, Subscription, UniformListScrollHandle, WeakEntity, Window, div, img, px,
retain_all, uniform_list,
@@ -15,32 +15,59 @@ use gpui::{
use nostr_sdk::prelude::*;
use person::PersonRegistry;
use smallvec::{SmallVec, smallvec};
-use state::NostrRegistry;
+use state::{NostrRegistry, StateEvent};
use theme::{ActiveTheme, TABBAR_HEIGHT};
use ui::avatar::Avatar;
-use ui::button::{Button, ButtonVariants};
+use ui::button::{Button, ButtonCustomVariant, ButtonVariants};
use ui::dock::{DockArea, DockPlacement, Panel, PanelEvent, PanelHandle};
use ui::indicator::Indicator;
use ui::menu::{DropdownMenu, PopupMenuItem};
+use ui::nav::Nav;
use ui::nav_item::NavItem;
use ui::notification::Notification;
use ui::scroll::Scrollbar;
use ui::tab::Tab;
use ui::tab::tab_bar::TabBar;
use ui::{
- Disableable, Icon, IconName, Sizable, StyledExt, TRAFFIC_LIGHT_PADDING, WindowExtension,
- h_flex, title_bar_drag_handlers, v_flex,
+ Disableable, Icon, IconName, Selectable, Sizable, StyledExt, TRAFFIC_LIGHT_PADDING,
+ WindowExtension, h_flex, title_bar_drag_handlers, v_flex,
};
use crate::Command;
use crate::dialogs::import;
mod tab;
-mod tree;
+mod utils;
use tab::SidebarTab;
-use tree::{CommunityRow, CommunitySection, SidebarRow};
-pub(crate) use tree::{TreeRow, TreeRowKind};
+pub(crate) use utils::{nav_avatar, nav_icon, pick_banner};
+
+pub enum SidebarRow {
+ Room { room: Entity },
+ Community { community: Entity },
+}
+
+/// A collapsible group of rows in the sidebar's community view.
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub enum CommunitySection {
+ Channels,
+ Admins,
+ Members,
+}
+
+/// A row in the sidebar's community view.
+pub enum CommunityRow {
+ Section(CommunitySection),
+ Channel {
+ id: ChannelId,
+ name: SharedString,
+ private: bool,
+ selected: bool,
+ },
+ Member {
+ public_key: PublicKey,
+ },
+}
pub struct Sidebar {
focus_handle: FocusHandle,
@@ -49,6 +76,8 @@ pub struct Sidebar {
community_scroll: UniformListScrollHandle,
/// The dock the sidebar opens its panels in
dock: WeakEntity,
+ /// Background shown behind the signed-out screen, picked at random
+ banner: SharedString,
active_tab: SidebarTab,
/// The community the sidebar is browsing, if any
community: Option>,
@@ -63,6 +92,7 @@ impl Sidebar {
pub fn new(window: &mut Window, dock: WeakEntity, cx: &mut Context) -> Self {
let chat = ChatRegistry::global(cx);
let communities = CommunityRegistry::global(cx);
+ let nostr = NostrRegistry::global(cx);
let mut subscriptions = smallvec![];
@@ -85,6 +115,16 @@ impl Sidebar {
},
));
+ subscriptions.push(
+ cx.subscribe_in(&nostr, window, |this, _nostr, event, _window, cx| {
+ // Re-pick the background each time the signed-out screen is shown.
+ if let StateEvent::NoSigner = event {
+ this.banner = pick_banner();
+ cx.notify();
+ }
+ }),
+ );
+
Self {
focus_handle: cx.focus_handle(),
scroll_handles: [
@@ -93,6 +133,7 @@ impl Sidebar {
],
community_scroll: UniformListScrollHandle::new(),
dock,
+ banner: pick_banner(),
active_tab: SidebarTab::Inbox,
community: None,
channels_open: true,
@@ -381,6 +422,7 @@ impl Sidebar {
let (banner, rows) = {
let community = community.read(cx);
let owner = community.state().owner;
+
let mut admins = Vec::new();
let mut members = Vec::new();
@@ -412,25 +454,26 @@ impl Sidebar {
rows.push(CommunityRow::Section(CommunitySection::Admins));
if self.admins_open {
- rows.extend(admins.into_iter().map(|public_key| CommunityRow::Member {
- prefix: "community-admin",
- public_key,
- }));
+ rows.extend(
+ admins
+ .into_iter()
+ .map(|public_key| CommunityRow::Member { public_key }),
+ );
}
rows.push(CommunityRow::Section(CommunitySection::Members));
if self.members_open {
- rows.extend(members.into_iter().map(|public_key| CommunityRow::Member {
- prefix: "member",
- public_key,
- }));
+ rows.extend(
+ members
+ .into_iter()
+ .map(|public_key| CommunityRow::Member { public_key }),
+ );
}
(banner, rows)
};
let rows = Rc::new(rows);
- let scroll_handle = &self.community_scroll;
v_flex()
.flex_1()
@@ -439,7 +482,7 @@ impl Sidebar {
.gap_2()
.when_some(banner, |this, banner| {
this.child(
- div().px_2().child(
+ div().px_2().flex_shrink_0().child(
img(banner)
.w_full()
.h_20()
@@ -460,11 +503,11 @@ impl Sidebar {
this.render_community_rows(range, rows.as_slice(), &community, cx)
}),
)
- .track_scroll(scroll_handle)
+ .track_scroll(&self.community_scroll)
.h_full()
.px_2(),
)
- .child(Scrollbar::vertical(scroll_handle)),
+ .child(Scrollbar::vertical(&self.community_scroll)),
)
.into_any_element()
}
@@ -480,36 +523,14 @@ impl Sidebar {
.into_iter()
.flatten()
.map(|row| match row {
- CommunityRow::Section(CommunitySection::Channels) => self.section_row(
- "channels",
- "Channels",
- self.channels_open,
- |sidebar| sidebar.channels_open = !sidebar.channels_open,
- cx,
- ),
- CommunityRow::Section(CommunitySection::Admins) => self.section_row(
- "admins",
- "Admins",
- self.admins_open,
- |sidebar| sidebar.admins_open = !sidebar.admins_open,
- cx,
- ),
- CommunityRow::Section(CommunitySection::Members) => self.section_row(
- "members",
- "Members",
- self.members_open,
- |sidebar| sidebar.members_open = !sidebar.members_open,
- cx,
- ),
+ CommunityRow::Section(section) => self.section_row(section, cx),
+ CommunityRow::Member { public_key } => self.member_row(public_key, cx),
CommunityRow::Channel {
id,
name,
private,
selected,
} => self.channel_row(*id, name.clone(), *private, *selected, community, cx),
- CommunityRow::Member { prefix, public_key } => {
- self.member_row(prefix, *public_key, cx)
- }
})
.collect()
}
@@ -559,26 +580,30 @@ impl Sidebar {
let dock = self.dock.clone();
let room = room.clone();
- TreeRow::new(
- ElementId::NamedInteger("tree-row".into(), index as u64),
- TreeRowKind::Room,
- name,
- )
- .avatar(seed)
- .picture(picture)
- .created_at(created_at)
- // Run in an app context: docking a new panel reads every
- // docked panel's id, which would panic inside a sidebar update.
- .on_click(move |_event, window, cx| {
- ui::dock::add_panel_to(
- &dock,
- PanelHandle::new(chat_ui::init(room.downgrade(), window, cx)),
- DockPlacement::Center,
- window,
- cx,
- );
- })
- .into_any_element()
+ Nav::new(SharedString::from(format!("room-{index}")))
+ .label(name)
+ .text_sm()
+ .font_medium()
+ .when_some(nav_avatar(Some(seed), picture, cx), |this, avatar| {
+ this.prefix(avatar)
+ })
+ .suffix(
+ div()
+ .font_normal()
+ .text_xs()
+ .text_color(cx.theme().text_placeholder)
+ .child(created_at),
+ )
+ .on_click(move |_event, window, cx| {
+ ui::dock::add_panel_to(
+ &dock,
+ PanelHandle::new(chat_ui::init(room.downgrade(), window, cx)),
+ DockPlacement::Center,
+ window,
+ cx,
+ );
+ })
+ .into_any_element()
}
SidebarRow::Community { community } => {
let name = community.read(cx).name();
@@ -588,58 +613,66 @@ impl Sidebar {
let sidebar = cx.entity().downgrade();
let community = community.clone();
- TreeRow::new(
- ElementId::NamedInteger("tree-row".into(), index as u64),
- TreeRowKind::Community,
- name,
- )
- .avatar(seed)
- .picture(picture)
- .on_click(move |_event, window, cx| {
- ui::dock::add_panel_to(
- &dock,
- PanelHandle::new(community_ui::init(community.clone(), window, cx)),
- DockPlacement::Center,
- window,
- cx,
- );
+ Nav::new(SharedString::from(format!("com-{index}")))
+ .label(name)
+ .text_sm()
+ .when_some(nav_avatar(Some(seed), picture, cx), |this, avatar| {
+ this.prefix(avatar)
+ })
+ .on_click(move |_event, window, cx| {
+ ui::dock::add_panel_to(
+ &dock,
+ PanelHandle::new(community_ui::init(
+ community.clone(),
+ window,
+ cx,
+ )),
+ DockPlacement::Center,
+ window,
+ cx,
+ );
- if let Err(error) = sidebar.update(cx, |this, cx| {
- this.community = Some(community.downgrade());
- cx.notify();
- }) {
- log::error!("Failed to show community in sidebar: {error}");
- }
- })
- .into_any_element()
+ if let Err(error) = sidebar.update(cx, |this, cx| {
+ this.community = Some(community.downgrade());
+ cx.notify();
+ }) {
+ log::error!("Failed to show community in sidebar: {error}");
+ }
+ })
+ .into_any_element()
}
}
})
.collect()
}
- fn section_row(
- &self,
- id: &'static str,
- label: &'static str,
- open: bool,
- toggle: impl Fn(&mut Sidebar) + 'static,
- cx: &mut Context,
- ) -> AnyElement {
- div()
- .flex_shrink_0()
- .child(
- TreeRow::new(ElementId::Name(id.into()), TreeRowKind::Section, label)
- .icon(if open {
- IconName::CaretDown
- } else {
- IconName::CaretRight
- })
- .on_click(cx.listener(move |this, _event, _window, cx| {
- toggle(this);
- cx.notify();
- })),
- )
+ fn section_row(&self, section: &CommunitySection, cx: &mut Context) -> AnyElement {
+ let section = *section;
+ let (label, open) = match section {
+ CommunitySection::Channels => ("Channels", self.channels_open),
+ CommunitySection::Admins => ("Admins", self.admins_open),
+ CommunitySection::Members => ("Members", self.members_open),
+ };
+ let icon = if open {
+ IconName::CaretDown
+ } else {
+ IconName::CaretRight
+ };
+
+ Nav::new(label)
+ .label(label)
+ .suffix(nav_icon(icon, cx))
+ .text_xs()
+ .font_semibold()
+ .text_color(cx.theme().text_placeholder)
+ .on_click(cx.listener(move |this, _ev, _window, cx| {
+ match section {
+ CommunitySection::Channels => this.channels_open = !this.channels_open,
+ CommunitySection::Admins => this.admins_open = !this.admins_open,
+ CommunitySection::Members => this.members_open = !this.members_open,
+ }
+ cx.notify();
+ }))
.into_any_element()
}
@@ -653,50 +686,38 @@ impl Sidebar {
cx: &mut Context,
) -> AnyElement {
let community = community.clone();
+ let icon = if private {
+ IconName::Lock
+ } else {
+ IconName::Hashtag
+ };
- div()
- .flex_shrink_0()
- .rounded(cx.theme().radius)
- .child(
- TreeRow::new(
- ElementId::Name(SharedString::from(format!(
- "community-channel-{}",
- id.to_hex()
- ))),
- TreeRowKind::Room,
- name,
- )
- .icon(if private {
- IconName::Lock
- } else {
- IconName::Message
- })
- .on_click(cx.listener(move |_this, _event, _window, cx| {
- community.update(cx, |community, cx| community.set_active_channel(id, cx));
- cx.notify();
- })),
- )
- .when(selected, |this| this.bg(cx.theme().ghost_element_active))
+ Nav::new(id.to_hex())
+ .label(name)
+ .prefix(nav_icon(icon, cx))
+ .text_sm()
+ .font_medium()
+ .selected(selected)
+ .on_click(cx.listener(move |_this, _event, _window, cx| {
+ community.update(cx, |community, cx| {
+ community.set_active_channel(id, cx);
+ });
+ cx.notify();
+ }))
.into_any_element()
}
- fn member_row(&self, prefix: &str, public_key: PublicKey, cx: &App) -> AnyElement {
+ fn member_row(&self, public_key: &PublicKey, cx: &App) -> AnyElement {
let persons = PersonRegistry::global(cx);
- let person = persons.read(cx).get(&public_key, cx);
+ let person = persons.read(cx).get(public_key, cx);
- div()
- .flex_shrink_0()
- .child(
- TreeRow::new(
- ElementId::Name(SharedString::from(format!(
- "{prefix}-{}",
- public_key.to_hex()
- ))),
- TreeRowKind::Room,
- person.name(),
- )
- .avatar(person.avatar_seed())
- .picture(person.avatar()),
+ Nav::new(public_key.to_hex())
+ .label(person.name())
+ .text_sm()
+ .font_medium()
+ .when_some(
+ nav_avatar(Some(person.avatar_seed()), person.avatar(), cx),
+ |this, avatar| this.prefix(avatar),
)
.into_any_element()
}
@@ -770,27 +791,32 @@ impl Render for Sidebar {
window,
cx,
))
+ .child(
+ div().absolute().inset_0().child(
+ img(self.banner.clone())
+ .size_full()
+ .object_fit(ObjectFit::Cover),
+ ),
+ )
.child(
v_flex()
.size_full()
.justify_end()
.gap_4()
.p_4()
+ .child(img("brand/headline.png").max_w_48())
.child(
- v_flex().child(div().font_semibold().child("Coop")).child(
- div()
- .text_xs()
- .text_color(cx.theme().text_muted)
- .child("TODO"),
- ),
- )
- .child(
- Button::new("import-identity")
- .label("Import identity")
- .primary()
+ Button::new("import")
+ .label("Import Identity")
+ .custom(
+ ButtonCustomVariant::new(window, cx)
+ .color(gpui::white())
+ .foreground(gpui::black())
+ .hover(gpui::white().opacity(0.9))
+ .active(gpui::white().opacity(0.8)),
+ )
+ .large()
.font_semibold()
- .h_8()
- .w_full()
.on_click(|_, window, cx| {
import::open(window, cx);
}),
diff --git a/crates/workspace/src/sidebar/tree.rs b/crates/workspace/src/sidebar/tree.rs
deleted file mode 100644
index 2bd5db00..00000000
--- a/crates/workspace/src/sidebar/tree.rs
+++ /dev/null
@@ -1,221 +0,0 @@
-use std::rc::Rc;
-
-use chat::Room;
-use community::{ChannelId, Community};
-use gpui::prelude::FluentBuilder;
-use gpui::{
- App, ClickEvent, ElementId, Entity, ImageSource, InteractiveElement, IntoElement,
- ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window, div, px,
-};
-use nostr_sdk::prelude::PublicKey;
-use settings::AppSettings;
-use theme::ActiveTheme;
-use ui::avatar::{Avatar, PixelAvatar};
-use ui::{Icon, IconName, Selectable, Sizable, StyledExt, h_flex};
-
-pub enum SidebarRow {
- Room { room: Entity },
- Community { community: Entity },
-}
-
-/// A collapsible group of rows in the sidebar's community view.
-#[derive(Clone, Copy, PartialEq, Eq)]
-pub enum CommunitySection {
- Channels,
- Admins,
- Members,
-}
-
-/// A row in the sidebar's community view.
-pub enum CommunityRow {
- Section(CommunitySection),
- Channel {
- id: ChannelId,
- name: SharedString,
- private: bool,
- selected: bool,
- },
- Member {
- prefix: &'static str,
- public_key: PublicKey,
- },
-}
-
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub enum TreeRowKind {
- Section,
- Room,
- Community,
-}
-
-#[derive(IntoElement)]
-pub struct TreeRow {
- id: ElementId,
- kind: TreeRowKind,
- label: SharedString,
- avatar: Option,
- picture: Option,
- icon: Option,
- created_at: Option,
- selected: bool,
- #[allow(clippy::type_complexity)]
- on_click: Option>,
-}
-
-impl TreeRow {
- pub fn new(
- id: impl Into,
- kind: TreeRowKind,
- label: impl Into,
- ) -> Self {
- Self {
- id: id.into(),
- kind,
- label: label.into(),
- avatar: None,
- picture: None,
- icon: 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) -> Self {
- self.avatar = Some(seed.into());
- self
- }
-
- /// Shows `picture` instead of the generated avatar.
- pub fn picture(mut self, picture: Option>) -> 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 created_at(mut self, created_at: impl Into) -> 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_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))
- .small()
- .flex_shrink_0()
- .into_any_element(),
- ),
- (Some(seed), None) => Some(
- PixelAvatar::new(seed)
- .small()
- .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)
- .w_full()
- .h_10()
- .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_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),
- )
- .child(div().flex_1())
- .when(is_selected, |this| {
- this.child(
- Icon::new(IconName::CheckCircle)
- .small()
- .flex_shrink_0()
- .text_color(cx.theme().icon_accent),
- )
- })
- .when_some(self.created_at, |this, created_at| {
- this.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))
- })
- }
-}
diff --git a/crates/workspace/src/sidebar/utils.rs b/crates/workspace/src/sidebar/utils.rs
new file mode 100644
index 00000000..b01f3520
--- /dev/null
+++ b/crates/workspace/src/sidebar/utils.rs
@@ -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>,
+ picture: Option>,
+ cx: &App,
+) -> Option {
+ 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()
+}