{
- let room = room.read(cx);
-
- div()
- .id(ix)
- .px_1()
- .h_8()
- .w_full()
- .flex()
- .items_center()
- .justify_between()
- .text_xs()
- .rounded(px(cx.theme().radius))
- .hover(|this| this.bg(cx.theme().base.step(cx, ColorScaleStep::FOUR)))
- .child(div().flex_1().truncate().font_medium().map(|this| {
- if room.is_group() {
- this.flex()
- .items_center()
- .gap_2()
- .child(
- div()
- .flex()
- .justify_center()
- .items_center()
- .size_6()
- .rounded_full()
- .bg(cx.theme().accent.step(cx, ColorScaleStep::THREE))
- .child(Icon::new(IconName::GroupFill).size_3().text_color(
- cx.theme().accent.step(cx, ColorScaleStep::TWELVE),
- )),
- )
- .when_some(room.name(), |this, name| this.child(name))
- } else {
- this.when_some(room.first_member(), |this, member| {
- this.flex()
- .items_center()
- .gap_2()
- .child(img(member.avatar.clone()).size_6().flex_shrink_0())
- .child(member.name.clone())
- })
- }
- }))
- .child(
- div()
- .flex_shrink_0()
- .text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
- .child(room.ago()),
- )
- .on_click({
- let id = room.id;
-
- cx.listener(move |this, _, window, cx| {
- this.open(id, window, cx);
- })
- })
+ fn open_room(&self, id: u64, window: &mut Window, cx: &mut Context
) {
+ window.dispatch_action(
+ Box::new(AddPanel::new(
+ PanelKind::Room(id),
+ ui::dock_area::dock::DockPlacement::Center,
+ )),
+ cx,
+ );
}
+ fn ongoing(&mut self, cx: &mut Context) {
+ self.ongoing = !self.ongoing;
+ cx.notify();
+ }
+
+ fn incoming(&mut self, cx: &mut Context) {
+ self.incoming = !self.incoming;
+ cx.notify();
+ }
+
+ fn trusted(&mut self, cx: &mut Context) {
+ self.trusted = !self.trusted;
+ cx.notify();
+ }
+
+ fn unknown(&mut self, cx: &mut Context) {
+ self.unknown = !self.unknown;
+ cx.notify();
+ }
+
+ #[allow(dead_code)]
fn render_skeleton(&self, total: i32) -> impl IntoIterator- {
(0..total).map(|_| {
div()
@@ -151,20 +133,49 @@ impl Sidebar {
})
}
- fn open(&self, id: u64, window: &mut Window, cx: &mut Context) {
- window.dispatch_action(
- Box::new(AddPanel::new(
- PanelKind::Room(id),
- ui::dock_area::dock::DockPlacement::Center,
- )),
- cx,
- );
+ fn render_items(rooms: &Vec<&Entity>, cx: &Context) -> Vec {
+ let mut items = Vec::with_capacity(rooms.len());
+
+ for room in rooms {
+ let room = room.read(cx);
+ let room_id = room.id;
+ let ago = room.last_seen().ago();
+ let Some(member) = room.first_member() else {
+ continue;
+ };
+
+ let label = if room.is_group() {
+ room.subject().unwrap_or("Unnamed".into())
+ } else {
+ member.name.clone()
+ };
+
+ let img = if !room.is_group() {
+ Some(img(member.avatar.clone()))
+ } else {
+ None
+ };
+
+ let item = FolderItem::new(room_id as usize)
+ .label(label)
+ .description(ago)
+ .img(img)
+ .on_click({
+ cx.listener(move |this, _, window, cx| {
+ this.open_room(room_id, window, cx);
+ })
+ });
+
+ items.push(item);
+ }
+
+ items
}
}
impl Panel for Sidebar {
fn panel_id(&self) -> SharedString {
- "Sidebar".into()
+ self.name.clone()
}
fn title(&self, _cx: &App) -> AnyElement {
@@ -190,150 +201,77 @@ impl Focusable for Sidebar {
impl Render for Sidebar {
fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement {
- let entity = cx.entity();
+ let registry = ChatRegistry::global(cx).read(cx);
+ let rooms = registry.rooms(cx);
+ let loading = registry.loading();
+
+ let ongoing = rooms.get(&RoomKind::Ongoing);
+ let trusted = rooms.get(&RoomKind::Trusted);
+ let unknown = rooms.get(&RoomKind::Unknown);
div()
+ .scrollable(cx.entity_id(), ScrollbarAxis::Vertical)
+ .size_full()
.flex()
.flex_col()
- .size_full()
- .child(
- div()
- .px_2()
- .py_3()
- .w_full()
- .flex_shrink_0()
- .flex()
- .flex_col()
- .gap_1()
+ .gap_3()
+ .px_2()
+ .py_3()
+ .child(ComposeButton::new("New Message").on_click(cx.listener(
+ |this, _, window, cx| {
+ this.render_compose(window, cx);
+ },
+ )))
+ .map(|this| {
+ if loading {
+ this.children(self.render_skeleton(6))
+ } else {
+ this.when_some(ongoing, |this, rooms| {
+ this.child(
+ Folder::new("Ongoing")
+ .icon(IconName::FolderFill)
+ .active_icon(IconName::FolderOpenFill)
+ .collapsed(self.ongoing)
+ .on_click(cx.listener(move |this, _, _, cx| {
+ this.ongoing(cx);
+ }))
+ .children(Self::render_items(rooms, cx)),
+ )
+ })
.child(
- div()
- .id("new_message")
- .flex()
- .items_center()
- .gap_2()
- .px_1()
- .h_7()
- .text_xs()
- .font_semibold()
- .rounded(px(cx.theme().radius))
- .child(
- div()
- .size_6()
- .flex()
- .items_center()
- .justify_center()
- .rounded_full()
- .bg(cx.theme().accent.step(cx, ColorScaleStep::NINE))
- .child(
- Icon::new(IconName::ComposeFill)
- .small()
- .text_color(cx.theme().base.darken(cx)),
- ),
- )
- .child("New Message")
- .hover(|this| this.bg(cx.theme().base.step(cx, ColorScaleStep::THREE)))
- .on_click(cx.listener(|this, _, window, cx| {
- // Open compose modal
- this.render_compose(window, cx);
- })),
- )
- .child(Empty),
- )
- .child(
- div()
- .px_2()
- .w_full()
- .flex_1()
- .flex()
- .flex_col()
- .gap_1()
- .child(
- div()
- .id("inbox_header")
- .px_1()
- .h_7()
- .flex()
- .items_center()
- .flex_shrink_0()
- .rounded(px(cx.theme().radius))
- .text_xs()
- .font_semibold()
- .child(
- Icon::new(IconName::ChevronDown)
- .size_6()
- .when(self.is_collapsed, |this| {
- this.rotate(percentage(270. / 360.))
- }),
- )
- .child(self.label.clone())
- .hover(|this| this.bg(cx.theme().base.step(cx, ColorScaleStep::THREE)))
- .on_click(cx.listener(move |view, _event, _window, cx| {
- view.is_collapsed = !view.is_collapsed;
- cx.notify();
- })),
- )
- .when(!self.is_collapsed, |this| {
- this.flex_1().w_full().map(|this| {
- let state = ChatRegistry::global(cx);
- let is_loading = state.read(cx).is_loading();
- let len = state.read(cx).rooms().len();
-
- if is_loading {
- this.children(self.render_skeleton(5))
- } else if state.read(cx).rooms().is_empty() {
+ Parent::new("Incoming")
+ .icon(IconName::FolderFill)
+ .active_icon(IconName::FolderOpenFill)
+ .collapsed(self.incoming)
+ .on_click(cx.listener(move |this, _, _, cx| {
+ this.incoming(cx);
+ }))
+ .when_some(trusted, |this, rooms| {
this.child(
- div()
- .px_1()
- .w_full()
- .h_20()
- .flex()
- .flex_col()
- .items_center()
- .justify_center()
- .text_center()
- .rounded(px(cx.theme().radius))
- .bg(cx.theme().base.step(cx, ColorScaleStep::THREE))
- .child(
- div()
- .text_xs()
- .font_semibold()
- .line_height(relative(1.2))
- .child("No chats"),
- )
- .child(
- div()
- .text_xs()
- .text_color(
- cx.theme()
- .base
- .step(cx, ColorScaleStep::ELEVEN),
- )
- .child("Recent chats will appear here."),
- ),
+ Folder::new("Trusted")
+ .icon(IconName::FolderFill)
+ .active_icon(IconName::FolderOpenFill)
+ .collapsed(self.trusted)
+ .on_click(cx.listener(move |this, _, _, cx| {
+ this.trusted(cx);
+ }))
+ .children(Self::render_items(rooms, cx)),
)
- } else {
+ })
+ .when_some(unknown, |this, rooms| {
this.child(
- uniform_list(
- entity,
- "rooms",
- len,
- move |this, range, _, cx| {
- let mut items = vec![];
-
- for ix in range {
- if let Some(room) = state.read(cx).rooms().get(ix) {
- items.push(this.render_room(ix, room, cx));
- }
- }
-
- items
- },
- )
- .size_full(),
+ Folder::new("Unknown")
+ .icon(IconName::FolderFill)
+ .active_icon(IconName::FolderOpenFill)
+ .collapsed(self.unknown)
+ .on_click(cx.listener(move |this, _, _, cx| {
+ this.unknown(cx);
+ }))
+ .children(Self::render_items(rooms, cx)),
)
- }
- })
- }),
- )
+ }),
+ )
+ }
+ })
}
}
diff --git a/crates/chats/src/lib.rs b/crates/chats/src/lib.rs
index 0359ba8..e58ced0 100644
--- a/crates/chats/src/lib.rs
+++ b/crates/chats/src/lib.rs
@@ -1,11 +1,13 @@
-use std::cmp::Reverse;
+use std::{cmp::Reverse, collections::HashMap};
use anyhow::anyhow;
use common::{last_seen::LastSeen, utils::room_hash};
use global::get_client;
-use gpui::{App, AppContext, Context, Entity, Global, Task, Window};
+use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task, Window};
use itertools::Itertools;
use nostr_sdk::prelude::*;
+use room::RoomKind;
+use smallvec::{smallvec, SmallVec};
use crate::room::Room;
@@ -13,7 +15,7 @@ pub mod message;
pub mod room;
pub fn init(cx: &mut App) {
- ChatRegistry::set_global(cx.new(|_| ChatRegistry::new()), cx);
+ ChatRegistry::set_global(cx.new(ChatRegistry::new), cx);
}
struct GlobalChatRegistry(Entity);
@@ -22,7 +24,9 @@ impl Global for GlobalChatRegistry {}
pub struct ChatRegistry {
rooms: Vec>,
- is_loading: bool,
+ loading: bool,
+ #[allow(dead_code)]
+ subscriptions: SmallVec<[Subscription; 1]>,
}
impl ChatRegistry {
@@ -34,21 +38,40 @@ impl ChatRegistry {
cx.set_global(GlobalChatRegistry(state));
}
- fn new() -> Self {
+ fn new(cx: &mut Context) -> Self {
+ let mut subscriptions = smallvec![];
+
+ subscriptions.push(cx.observe_new::(|this, _, cx| {
+ let load_metadata = this.load_metadata(cx);
+
+ cx.spawn(async move |this, cx| {
+ if let Ok(profiles) = load_metadata.await {
+ cx.update(|cx| {
+ this.update(cx, |this, cx| {
+ this.update_members(profiles, cx);
+ })
+ .ok();
+ })
+ .ok();
+ }
+ })
+ .detach();
+ }));
+
Self {
rooms: vec![],
- is_loading: true,
+ loading: true,
+ subscriptions,
}
}
- pub fn current_rooms_ids(&self, cx: &mut Context) -> Vec {
- self.rooms.iter().map(|room| room.read(cx).id).collect()
- }
-
- pub fn load_chat_rooms(&mut self, window: &mut Window, cx: &mut Context) {
+ pub fn load_rooms(&mut self, window: &mut Window, cx: &mut Context) {
let client = get_client();
+ let room_ids = self.room_ids(cx);
- let task: Task, Error>> = cx.background_spawn(async move {
+ type LoadResult = Result, Error>;
+
+ let task: Task = cx.background_spawn(async move {
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
@@ -64,11 +87,31 @@ impl ChatRegistry {
let recv_events = client.database().query(recv).await?;
let events = send_events.merge(recv_events);
- let result: Vec = events
+ let mut room_map: HashMap = HashMap::new();
+
+ for event in events
.into_iter()
.filter(|ev| ev.tags.public_keys().peekable().peek().is_some())
- .unique_by(room_hash)
- .sorted_by_key(|ev| Reverse(ev.created_at))
+ {
+ let hash = room_hash(&event);
+
+ if !room_ids.iter().any(|id| id == &hash) {
+ let filter = Filter::new().kind(Kind::ContactList).pubkey(event.pubkey);
+ let is_trust = client.database().count(filter).await? >= 1;
+
+ room_map
+ .entry(hash)
+ .and_modify(|(_, count, trusted)| {
+ *count += 1;
+ *trusted = is_trust;
+ })
+ .or_insert((event, 1, is_trust));
+ }
+ }
+
+ let result: Vec<(Event, usize, bool)> = room_map
+ .into_values()
+ .sorted_by_key(|(ev, _, _)| Reverse(ev.created_at))
.collect();
Ok(result)
@@ -76,30 +119,27 @@ impl ChatRegistry {
cx.spawn_in(window, async move |this, cx| {
if let Ok(events) = task.await {
+ let rooms: Vec> = events
+ .into_iter()
+ .map(|(event, count, trusted)| {
+ let kind = if count > 2 {
+ // If frequency count is greater than 2, mark this room as ongoing
+ RoomKind::Ongoing
+ } else if trusted {
+ RoomKind::Trusted
+ } else {
+ RoomKind::Unknown
+ };
+
+ cx.new(|_| Room::new(&event, kind)).unwrap()
+ })
+ .collect();
+
cx.update(|_, cx| {
this.update(cx, |this, cx| {
- if !events.is_empty() {
- let current_ids = this.current_rooms_ids(cx);
- let items: Vec> = events
- .into_iter()
- .filter_map(|ev| {
- let new = room_hash(&ev);
- // Filter all seen rooms
- if !current_ids.iter().any(|this| this == &new) {
- Some(Room::new(&ev, cx))
- } else {
- None
- }
- })
- .collect();
-
- this.is_loading = false;
- this.rooms.extend(items);
- this.rooms
- .sort_by_key(|room| Reverse(room.read(cx).last_seen()));
- } else {
- this.is_loading = false;
- }
+ this.rooms.extend(rooms);
+ this.rooms.sort_by_key(|r| Reverse(r.read(cx).last_seen()));
+ this.loading = false;
cx.notify();
})
@@ -111,14 +151,40 @@ impl ChatRegistry {
.detach();
}
- pub fn rooms(&self) -> &[Entity] {
- &self.rooms
+ /// Get the IDs of all rooms.
+ pub fn room_ids(&self, cx: &mut Context) -> Vec {
+ self.rooms.iter().map(|room| room.read(cx).id).collect()
}
- pub fn is_loading(&self) -> bool {
- self.is_loading
+ /// Get all rooms.
+ pub fn rooms(&self, cx: &App) -> HashMap>> {
+ let mut groups = HashMap::new();
+ groups.insert(RoomKind::Ongoing, Vec::new());
+ groups.insert(RoomKind::Trusted, Vec::new());
+ groups.insert(RoomKind::Unknown, Vec::new());
+
+ for room in self.rooms.iter() {
+ let kind = room.read(cx).kind();
+ groups.entry(kind).or_insert_with(Vec::new).push(room);
+ }
+
+ groups
}
+ /// Get rooms by their kind.
+ pub fn rooms_by_kind(&self, kind: RoomKind, cx: &App) -> Vec<&Entity> {
+ self.rooms
+ .iter()
+ .filter(|room| room.read(cx).kind() == kind)
+ .collect()
+ }
+
+ /// Get the loading status of the rooms.
+ pub fn loading(&self) -> bool {
+ self.loading
+ }
+
+ /// Get a room by its ID.
pub fn get(&self, id: &u64, cx: &App) -> Option> {
self.rooms
.iter()
@@ -126,11 +192,10 @@ impl ChatRegistry {
.cloned()
}
- pub fn push_room(
- &mut self,
- room: Entity,
- cx: &mut Context,
- ) -> Result<(), anyhow::Error> {
+ /// Push a room to the list.
+ pub fn push(&mut self, room: Room, cx: &mut Context) -> Result<(), anyhow::Error> {
+ let room = cx.new(|_| room);
+
if !self
.rooms
.iter()
@@ -145,6 +210,7 @@ impl ChatRegistry {
}
}
+ /// Push a message to a room.
pub fn push_message(&mut self, event: Event, window: &mut Window, cx: &mut Context) {
let id = room_hash(&event);
@@ -158,7 +224,7 @@ impl ChatRegistry {
self.rooms
.sort_by_key(|room| Reverse(room.read(cx).last_seen()));
} else {
- let new_room = Room::new(&event, cx);
+ let new_room = cx.new(|_| Room::new(&event, RoomKind::default()));
// Push the new room to the front of the list
self.rooms.insert(0, new_room);
diff --git a/crates/chats/src/room.rs b/crates/chats/src/room.rs
index 90b4657..915e003 100644
--- a/crates/chats/src/room.rs
+++ b/crates/chats/src/room.rs
@@ -7,10 +7,10 @@ use common::{
utils::{compare, room_hash},
};
use global::get_client;
-use gpui::{App, AppContext, Context, Entity, EventEmitter, SharedString, Task, Window};
+use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task, Window};
use itertools::Itertools;
use nostr_sdk::prelude::*;
-use smallvec::{smallvec, SmallVec};
+use smallvec::SmallVec;
use crate::message::{Message, RoomMessage};
@@ -19,13 +19,25 @@ pub struct IncomingEvent {
pub event: RoomMessage,
}
+#[derive(Clone, Copy, Hash, Debug, PartialEq, Eq, Default)]
+pub enum RoomKind {
+ Ongoing,
+ Trusted,
+ #[default]
+ Unknown,
+}
+
pub struct Room {
pub id: u64,
pub last_seen: LastSeen,
/// Subject of the room
- pub name: Option,
+ pub subject: Option,
/// All members of the room
pub members: Arc>,
+ /// Kind
+ pub kind: RoomKind,
+ /// All public keys of the room members
+ pubkeys: Vec,
}
impl EventEmitter for Room {}
@@ -37,66 +49,34 @@ impl PartialEq for Room {
}
impl Room {
- pub fn new(event: &Event, cx: &mut App) -> Entity {
+ /// Create a new room from an Nostr Event
+ pub fn new(event: &Event, kind: RoomKind) -> Self {
let id = room_hash(event);
let last_seen = LastSeen(event.created_at);
// Get the subject from the event's tags
- let name = if let Some(tag) = event.tags.find(TagKind::Subject) {
+ let subject = if let Some(tag) = event.tags.find(TagKind::Subject) {
tag.content().map(|s| s.to_owned().into())
} else {
None
};
- // Create a task for loading metadata
- let load_metadata = Self::load_metadata(event, cx);
+ // Get all public keys from the event's tags
+ let mut pubkeys = vec![];
+ pubkeys.extend(event.tags.public_keys().collect::>());
+ pubkeys.push(event.pubkey);
- // Create a new GPUI's Entity
- cx.new(|cx| {
- let this = Self {
- id,
- last_seen,
- name,
- members: Arc::new(smallvec![]),
- };
-
- cx.spawn(async move |this, cx| {
- if let Ok(profiles) = load_metadata.await {
- cx.update(|cx| {
- this.update(cx, |this: &mut Room, cx| {
- // Update the room's name if it's not already set
- if this.name.is_none() {
- let mut name = profiles
- .iter()
- .take(2)
- .map(|profile| profile.name.to_string())
- .collect::>()
- .join(", ");
-
- if profiles.len() > 2 {
- name = format!("{}, +{}", name, profiles.len() - 2);
- }
-
- this.name = Some(name.into())
- };
-
- let mut new_members = SmallVec::new();
- new_members.extend(profiles);
- this.members = Arc::new(new_members);
-
- cx.notify();
- })
- .ok();
- })
- .ok();
- }
- })
- .detach();
-
- this
- })
+ Self {
+ id,
+ last_seen,
+ subject,
+ kind,
+ members: Arc::new(SmallVec::with_capacity(pubkeys.len())),
+ pubkeys,
+ }
}
+ /// Get room's id
pub fn id(&self) -> u64 {
self.id
}
@@ -116,12 +96,17 @@ impl Room {
/// Collect room's member's public keys
pub fn public_keys(&self) -> Vec {
- self.members.iter().map(|m| m.public_key).collect()
+ self.pubkeys.clone()
}
/// Get room's display name
- pub fn name(&self) -> Option {
- self.name.clone()
+ pub fn subject(&self) -> Option {
+ self.subject.clone()
+ }
+
+ /// Get room's kind
+ pub fn kind(&self) -> RoomKind {
+ self.kind
}
/// Determine if room is a group
@@ -145,6 +130,31 @@ impl Room {
self.last_seen.ago()
}
+ pub fn update_members(&mut self, profiles: Vec, cx: &mut Context) {
+ // Update the room's name if it's not already set
+ if self.subject.is_none() {
+ // Merge all members into a single name
+ let mut name = profiles
+ .iter()
+ .take(2)
+ .map(|profile| profile.name.to_string())
+ .collect::>()
+ .join(", ");
+
+ // Create a specific name for group
+ if profiles.len() > 2 {
+ name = format!("{}, +{}", name, profiles.len() - 2);
+ }
+
+ self.subject = Some(name.into());
+ };
+
+ // Update the room's members
+ self.members = Arc::new(profiles.into());
+
+ cx.notify();
+ }
+
/// Verify messaging_relays for all room's members
pub fn messaging_relays(&self, cx: &App) -> Task, Error>> {
let client = get_client();
@@ -208,6 +218,38 @@ impl Room {
})
}
+ /// Load metadata for all members
+ pub fn load_metadata(&self, cx: &mut Context) -> Task, Error>> {
+ let client = get_client();
+ let pubkeys = self.public_keys();
+
+ cx.background_spawn(async move {
+ let signer = client.signer().await?;
+ let signer_pubkey = signer.get_public_key().await?;
+ let mut profiles = Vec::with_capacity(pubkeys.len());
+
+ for public_key in pubkeys.into_iter() {
+ let metadata = client
+ .database()
+ .metadata(public_key)
+ .await?
+ .unwrap_or_default();
+
+ // Convert metadata to profile
+ let profile = NostrProfile::new(public_key, metadata);
+
+ if public_key == signer_pubkey {
+ // Room's owner always push to the end of the vector
+ profiles.push(profile);
+ } else {
+ profiles.insert(0, profile);
+ }
+ }
+
+ Ok(profiles)
+ })
+ }
+
/// Load room messages
pub fn load_messages(&self, cx: &App) -> Task, Error>> {
let client = get_client();
@@ -350,40 +392,4 @@ impl Room {
})
.detach();
}
-
- /// Load metadata for all members
- fn load_metadata(event: &Event, cx: &App) -> Task, Error>> {
- let client = get_client();
- let mut pubkeys = vec![];
-
- // Get all pubkeys from event's tags
- pubkeys.extend(event.tags.public_keys().collect::>());
- pubkeys.push(event.pubkey);
-
- cx.background_spawn(async move {
- let signer = client.signer().await?;
- let signer_pubkey = signer.get_public_key().await?;
- let mut profiles = Vec::with_capacity(pubkeys.len());
-
- for public_key in pubkeys.into_iter() {
- let metadata = client
- .database()
- .metadata(public_key)
- .await?
- .unwrap_or_default();
-
- // Convert metadata to profile
- let profile = NostrProfile::new(public_key, metadata);
-
- if public_key == signer_pubkey {
- // Room's owner always push to the end of the vector
- profiles.push(profile);
- } else {
- profiles.insert(0, profile);
- }
- }
-
- Ok(profiles)
- })
- }
}
diff --git a/crates/global/src/constants.rs b/crates/global/src/constants.rs
index 5adcc14..8d914b9 100644
--- a/crates/global/src/constants.rs
+++ b/crates/global/src/constants.rs
@@ -5,9 +5,9 @@ pub const APP_ID: &str = "su.reya.coop";
pub const BOOTSTRAP_RELAYS: [&str; 5] = [
"wss://relay.damus.io",
"wss://relay.primal.net",
- "wss://purplepag.es",
"wss://user.kindpag.es",
"wss://relaydiscovery.com",
+ "wss://purplepag.es",
];
/// Subscriptions
diff --git a/crates/ui/src/icon.rs b/crates/ui/src/icon.rs
index d70bf04..f21071d 100644
--- a/crates/ui/src/icon.rs
+++ b/crates/ui/src/icon.rs
@@ -20,6 +20,7 @@ pub enum IconName {
Bell,
BookOpen,
Bot,
+ BubbleFill,
Calendar,
ChartPie,
Check,
@@ -42,6 +43,9 @@ pub enum IconName {
Eye,
EyeOff,
Frame,
+ Folder,
+ FolderFill,
+ FolderOpenFill,
GalleryVerticalEnd,
GitHub,
Globe,
@@ -104,6 +108,7 @@ impl IconName {
Self::Bell => "icons/bell.svg",
Self::BookOpen => "icons/book-open.svg",
Self::Bot => "icons/bot.svg",
+ Self::BubbleFill => "icons/bubble-fill.svg",
Self::Calendar => "icons/calendar.svg",
Self::ChartPie => "icons/chart-pie.svg",
Self::Check => "icons/check.svg",
@@ -126,6 +131,9 @@ impl IconName {
Self::Eye => "icons/eye.svg",
Self::EyeOff => "icons/eye-off.svg",
Self::Frame => "icons/frame.svg",
+ Self::Folder => "icons/folder.svg",
+ Self::FolderFill => "icons/folder-fill.svg",
+ Self::FolderOpenFill => "icons/folder-open-fill.svg",
Self::GalleryVerticalEnd => "icons/gallery-vertical-end.svg",
Self::GitHub => "icons/github.svg",
Self::Globe => "icons/globe.svg",
diff --git a/crates/ui/src/scroll/scrollable.rs b/crates/ui/src/scroll/scrollable.rs
index b09068a..2b263df 100644
--- a/crates/ui/src/scroll/scrollable.rs
+++ b/crates/ui/src/scroll/scrollable.rs
@@ -209,8 +209,8 @@ where
),
)
.into_any_element();
- let element_id = element.request_layout(window, cx);
+ let element_id = element.request_layout(window, cx);
let layout_id = window.request_layout(style, vec![element_id], cx);
(layout_id, element)
diff --git a/crates/ui/src/scroll/scrollbar.rs b/crates/ui/src/scroll/scrollbar.rs
index c1b2ee2..2f343ef 100644
--- a/crates/ui/src/scroll/scrollbar.rs
+++ b/crates/ui/src/scroll/scrollbar.rs
@@ -36,8 +36,8 @@ pub(crate) const WIDTH: Pixels = px(12.);
const MIN_THUMB_SIZE: f32 = 80.;
const THUMB_RADIUS: Pixels = Pixels(4.0);
const THUMB_INSET: Pixels = Pixels(3.);
-const FADE_OUT_DURATION: f32 = 3.0;
-const FADE_OUT_DELAY: f32 = 2.0;
+const FADE_OUT_DURATION: f32 = 2.0;
+const FADE_OUT_DELAY: f32 = 1.2;
pub trait ScrollHandleOffsetable {
fn offset(&self) -> Point;