From 8f6bedf70a4309e65eb8103c50b2f5713463785d Mon Sep 17 00:00:00 2001 From: reya Date: Fri, 17 Jan 2025 09:05:46 +0700 Subject: [PATCH] feat: improve compose modal --- assets/brand/coop-dark.png | Bin 19757 -> 0 bytes assets/brand/coop-light.png | Bin 19338 -> 0 bytes assets/brand/coop.svg | 4 + crates/app/src/states/chat/room.rs | 2 +- crates/app/src/utils.rs | 68 +++-- crates/app/src/views/chat/mod.rs | 8 +- crates/app/src/views/sidebar/compose.rs | 222 +++++++++++++++++ crates/app/src/views/sidebar/contact_list.rs | 248 ------------------- crates/app/src/views/sidebar/inbox.rs | 6 +- crates/app/src/views/sidebar/mod.rs | 66 +++-- crates/app/src/views/welcome.rs | 26 +- crates/ui/src/button.rs | 4 +- crates/ui/src/dropdown.rs | 6 +- crates/ui/src/modal.rs | 10 +- 14 files changed, 328 insertions(+), 342 deletions(-) delete mode 100644 assets/brand/coop-dark.png delete mode 100644 assets/brand/coop-light.png create mode 100644 assets/brand/coop.svg create mode 100644 crates/app/src/views/sidebar/compose.rs delete mode 100644 crates/app/src/views/sidebar/contact_list.rs diff --git a/assets/brand/coop-dark.png b/assets/brand/coop-dark.png deleted file mode 100644 index cb69627a1a0b04ee5ad4fa2e2dcb9aee933fc090..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19757 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

{;_>#jM0 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 diff --git a/assets/brand/coop-light.png b/assets/brand/coop-light.png deleted file mode 100644 index c7f31c206a27b1ba6c40fe539de64b512ef08fe9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19338 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 diff --git a/assets/brand/coop.svg b/assets/brand/coop.svg new file mode 100644 index 0000000..c463b0c --- /dev/null +++ b/assets/brand/coop.svg @@ -0,0 +1,4 @@ + + + + diff --git a/crates/app/src/states/chat/room.rs b/crates/app/src/states/chat/room.rs index 9925d81..b88991c 100644 --- a/crates/app/src/states/chat/room.rs +++ b/crates/app/src/states/chat/room.rs @@ -31,7 +31,7 @@ impl Member { IMAGE_SERVICE, picture ) } else { - "brands/avatar.png".into() + "brand/avatar.png".into() } } diff --git a/crates/app/src/utils.rs b/crates/app/src/utils.rs index 6adf7ef..39f55b1 100644 --- a/crates/app/src/utils.rs +++ b/crates/app/src/utils.rs @@ -1,12 +1,11 @@ -use chrono::{Duration, Local, TimeZone}; +use crate::{constants::NIP96_SERVER, get_client}; +use chrono::{Datelike, Local, TimeZone}; use nostr_sdk::prelude::*; use std::{ collections::HashSet, hash::{DefaultHasher, Hash, Hasher}, }; -use crate::{constants::NIP96_SERVER, get_client}; - pub async fn nip96_upload(file: Vec) -> anyhow::Result { let client = get_client(); let signer = client.signer().await?; @@ -42,50 +41,47 @@ pub fn shorted_public_key(public_key: PublicKey) -> String { format!("{}:{}", &pk[0..4], &pk[pk.len() - 4..]) } -pub fn show_npub(public_key: PublicKey, len: usize) -> String { - let bech32 = public_key.to_bech32().unwrap_or_default(); - let separator = " ... "; - - let sep_len = separator.len(); - let chars_to_show = len - sep_len; - let front_chars = (chars_to_show + 1) / 2; // ceil - let back_chars = chars_to_show / 2; // floor - - format!( - "{}{}{}", - &bech32[..front_chars], - separator, - &bech32[bech32.len() - back_chars..] - ) -} - -pub fn ago(time: Timestamp) -> String { +pub fn message_ago(time: Timestamp) -> String { let now = Local::now(); let input_time = Local.timestamp_opt(time.as_u64() as i64, 0).unwrap(); let diff = (now - input_time).num_hours(); if diff < 24 { let duration = now.signed_duration_since(input_time); - format_duration(duration) + + if duration.num_seconds() < 60 { + "now".to_string() + } else if duration.num_minutes() == 1 { + "1m".to_string() + } else if duration.num_minutes() < 60 { + format!("{}m", duration.num_minutes()) + } else if duration.num_hours() == 1 { + "1h".to_string() + } else if duration.num_hours() < 24 { + format!("{}h", duration.num_hours()) + } else if duration.num_days() == 1 { + "1d".to_string() + } else { + format!("{}d", duration.num_days()) + } } else { input_time.format("%b %d").to_string() } } -pub fn format_duration(duration: Duration) -> String { - if duration.num_seconds() < 60 { - "now".to_string() - } else if duration.num_minutes() == 1 { - "1m".to_string() - } else if duration.num_minutes() < 60 { - format!("{}m", duration.num_minutes()) - } else if duration.num_hours() == 1 { - "1h".to_string() - } else if duration.num_hours() < 24 { - format!("{}h", duration.num_hours()) - } else if duration.num_days() == 1 { - "1d".to_string() +pub fn message_time(time: Timestamp) -> String { + let now = Local::now(); + let input_time = Local.timestamp_opt(time.as_u64() as i64, 0).unwrap(); + + if input_time.day() == now.day() { + format!("Today at {}", input_time.format("%H:%M %p")) + } else if input_time.day() == now.day() - 1 { + format!("Yesterday at {}", input_time.format("%H:%M %p")) } else { - format!("{}d", duration.num_days()) + format!( + "{}, {}", + input_time.format("%d/%m/%y"), + input_time.format("%H:%M %p") + ) } } diff --git a/crates/app/src/views/chat/mod.rs b/crates/app/src/views/chat/mod.rs index d434157..e9837ca 100644 --- a/crates/app/src/views/chat/mod.rs +++ b/crates/app/src/views/chat/mod.rs @@ -2,7 +2,7 @@ use crate::{ constants::IMAGE_SERVICE, get_client, states::chat::room::Room, - utils::{ago, compare, nip96_upload}, + utils::{compare, message_time, nip96_upload}, }; use async_utility::task::spawn; use gpui::{ @@ -197,7 +197,7 @@ impl ChatPanel { Some(Message::new( member, ev.content.into(), - ago(ev.created_at).into(), + message_time(ev.created_at).into(), )) } else { None @@ -228,7 +228,7 @@ impl ChatPanel { Message::new( member, event.content.clone().into(), - ago(event.created_at).into(), + message_time(event.created_at).into(), ) }) }) @@ -298,7 +298,7 @@ impl ChatPanel { let message = Message::new( owner, content.to_string().into(), - ago(Timestamp::now()).into(), + message_time(Timestamp::now()).into(), ); model.items.extend(vec![message]); diff --git a/crates/app/src/views/sidebar/compose.rs b/crates/app/src/views/sidebar/compose.rs new file mode 100644 index 0000000..90f05ca --- /dev/null +++ b/crates/app/src/views/sidebar/compose.rs @@ -0,0 +1,222 @@ +use crate::{get_client, states::chat::room::Member}; +use gpui::{ + div, img, impl_internal_actions, px, uniform_list, Context, FocusHandle, InteractiveElement, + IntoElement, Model, ParentElement, Render, StatefulInteractiveElement, Styled, View, + ViewContext, VisualContext, WindowContext, +}; +use nostr_sdk::prelude::*; +use serde::Deserialize; +use std::collections::HashSet; +use ui::{ + indicator::Indicator, + input::TextInput, + prelude::FluentBuilder, + theme::{scale::ColorScaleStep, ActiveTheme}, + Icon, IconName, Sizable, StyledExt, +}; + +#[derive(Clone, PartialEq, Eq, Deserialize)] +struct SelectContact(PublicKey); + +impl_internal_actions!(contacts, [SelectContact]); + +pub struct Compose { + input: View, + contacts: Model>>, + selected: Model>, + focus_handle: FocusHandle, +} + +impl Compose { + pub fn new(cx: &mut ViewContext<'_, Self>) -> Self { + let contacts = cx.new_model(|_| None); + let selected = cx.new_model(|_| HashSet::new()); + let input = cx.new_view(|cx| { + TextInput::new(cx) + .appearance(false) + .text_size(ui::Size::Small) + .placeholder("npub1...") + .cleanable() + }); + + cx.spawn(|this, mut async_cx| { + let client = get_client(); + + async move { + let query: anyhow::Result, anyhow::Error> = async_cx + .background_executor() + .spawn(async move { + let signer = client.signer().await?; + let public_key = signer.get_public_key().await?; + let profiles = client.database().contacts(public_key).await?; + let members: Vec = profiles + .into_iter() + .map(|profile| Member::new(profile.public_key(), profile.metadata())) + .collect(); + + Ok(members) + }) + .await; + + if let Ok(contacts) = query { + if let Some(view) = this.upgrade() { + _ = async_cx.update_view(&view, |this, cx| { + this.contacts.update(cx, |this, cx| { + *this = Some(contacts); + cx.notify(); + }); + + cx.notify(); + }); + } + } + } + }) + .detach(); + + Self { + input, + contacts, + selected, + focus_handle: cx.focus_handle(), + } + } + + pub fn selected<'a>(&self, cx: &'a WindowContext) -> Vec<&'a PublicKey> { + self.selected.read(cx).iter().collect() + } + + fn on_action_select(&mut self, action: &SelectContact, cx: &mut ViewContext) { + self.selected.update(cx, |this, cx| { + if this.contains(&action.0) { + this.remove(&action.0); + } else { + this.insert(action.0); + }; + cx.notify(); + }); + + // TODO + } +} + +impl Render for Compose { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let msg = + "Start a conversation with someone using their npub or NIP-05 (like foo@bar.com)."; + + div() + .track_focus(&self.focus_handle) + .on_action(cx.listener(Self::on_action_select)) + .flex() + .flex_col() + .gap_3() + .child( + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_xs() + .text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN)) + .child(msg), + ) + .child( + div() + .bg(cx.theme().base.step(cx, ColorScaleStep::FOUR)) + .rounded(px(cx.theme().radius)) + .px_2() + .child(self.input.clone()), + ), + ) + .child( + div() + .flex() + .flex_col() + .gap_1() + .child(div().text_xs().font_semibold().child("Contacts")) + .child(div().map(|this| { + if let Some(contacts) = self.contacts.read(cx).clone() { + this.child( + uniform_list( + cx.view().clone(), + "contacts", + contacts.len(), + move |this, range, cx| { + let selected = this.selected.read(cx); + let mut items = Vec::new(); + + for ix in range { + let item = contacts.get(ix).unwrap().clone(); + let is_select = selected.contains(&item.public_key()); + + items.push( + div() + .id(ix) + .w_full() + .h_10() + .px_1p5() + .rounded(px(cx.theme().radius)) + .flex() + .items_center() + .justify_between() + .child( + div() + .flex() + .items_center() + .gap_2() + .text_sm() + .child( + div().flex_shrink_0().child( + img(item.avatar()).size_8(), + ), + ) + .child(item.name()), + ) + .when(is_select, |this| { + this.child( + Icon::new(IconName::CircleCheck) + .size_4() + .text_color(cx.theme().base.step( + cx, + ColorScaleStep::TWELVE, + )), + ) + }) + .hover(|this| { + this.bg(cx + .theme() + .base + .step(cx, ColorScaleStep::FOUR)) + .text_color( + cx.theme().base.step( + cx, + ColorScaleStep::ELEVEN, + ), + ) + }) + .on_click(move |_, cx| { + cx.dispatch_action(Box::new( + SelectContact(item.public_key()), + )); + }), + ); + } + + items + }, + ) + .h(px(320.)), + ) + } else { + this.flex() + .items_center() + .justify_center() + .h_16() + .child(Indicator::new().small()) + } + })), + ) + } +} diff --git a/crates/app/src/views/sidebar/contact_list.rs b/crates/app/src/views/sidebar/contact_list.rs deleted file mode 100644 index e70fa03..0000000 --- a/crates/app/src/views/sidebar/contact_list.rs +++ /dev/null @@ -1,248 +0,0 @@ -use crate::{constants::IMAGE_SERVICE, get_client, utils::show_npub}; -use gpui::{ - div, img, impl_internal_actions, list, px, Context, ElementId, FocusHandle, InteractiveElement, - IntoElement, ListAlignment, ListState, Model, ParentElement, Pixels, Render, RenderOnce, - SharedString, StatefulInteractiveElement, Styled, ViewContext, WindowContext, -}; -use nostr_sdk::prelude::*; -use serde::Deserialize; -use std::collections::{BTreeSet, HashSet}; -use ui::{ - prelude::FluentBuilder, - theme::{scale::ColorScaleStep, ActiveTheme}, - Icon, IconName, Selectable, StyledExt, -}; - -#[derive(Clone, PartialEq, Eq, Deserialize)] -struct SelectContact(PublicKey); - -impl_internal_actions!(contacts, [SelectContact]); - -#[derive(Clone, IntoElement)] -struct ContactListItem { - id: ElementId, - public_key: PublicKey, - metadata: Metadata, - selected: bool, -} - -impl ContactListItem { - pub fn new(public_key: PublicKey, metadata: Metadata) -> Self { - let id = SharedString::from(public_key.to_hex()).into(); - - Self { - id, - public_key, - metadata, - selected: false, - } - } -} - -impl Selectable for ContactListItem { - fn selected(mut self, selected: bool) -> Self { - self.selected = selected; - self - } - - fn element_id(&self) -> &gpui::ElementId { - &self.id - } -} - -impl RenderOnce for ContactListItem { - fn render(self, cx: &mut WindowContext) -> impl IntoElement { - let fallback = show_npub(self.public_key, 16); - - div() - .id(self.id) - .w_full() - .h_8() - .px_1() - .rounded_md() - .flex() - .items_center() - .justify_between() - .child( - div() - .flex() - .items_center() - .gap_2() - .text_sm() - .map(|this| { - if let Some(picture) = self.metadata.picture { - this.flex_shrink_0().child( - img(format!( - "{}/?url={}&w=72&h=72&fit=cover&mask=circle&n=-1", - IMAGE_SERVICE, picture - )) - .size_6(), - ) - } else { - this.flex_shrink_0() - .child(img("brand/avatar.png").size_6().rounded_full()) - } - }) - .map(|this| { - if let Some(display_name) = self.metadata.display_name { - this.flex_1().child(display_name) - } else { - this.flex_1().child(fallback) - } - }), - ) - .when(self.selected, |this| { - this.child( - Icon::new(IconName::CircleCheck) - .size_4() - .text_color(cx.theme().accent.step(cx, ColorScaleStep::NINE)), - ) - }) - .hover(|this| { - this.bg(cx.theme().base.step(cx, ColorScaleStep::FOUR)) - .text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN)) - }) - .on_click(move |_, cx| { - cx.dispatch_action(Box::new(SelectContact(self.public_key))); - }) - } -} - -#[derive(Clone)] -struct Contacts { - #[allow(dead_code)] - count: usize, - items: Vec, -} - -pub struct ContactList { - list: ListState, - contacts: Model>, - selected: HashSet, - focus_handle: FocusHandle, -} - -impl ContactList { - pub fn new(cx: &mut ViewContext<'_, Self>) -> Self { - let list = ListState::new(0, ListAlignment::Top, Pixels(50.), move |_, _| { - div().into_any_element() - }); - - let contacts = cx.new_model(|_| BTreeSet::new()); - let async_contacts = contacts.clone(); - - let mut async_cx = cx.to_async(); - - cx.foreground_executor() - .spawn({ - let client = get_client(); - - async move { - let query: anyhow::Result, anyhow::Error> = async_cx - .background_executor() - .spawn(async move { - let signer = client.signer().await?; - let public_key = signer.get_public_key().await?; - let profiles = client.database().contacts(public_key).await?; - - Ok(profiles) - }) - .await; - - if let Ok(profiles) = query { - _ = async_cx.update_model(&async_contacts, |model, cx| { - *model = profiles; - cx.notify(); - }); - } - } - }) - .detach(); - - cx.observe(&contacts, |this, model, cx| { - let profiles = model.read(cx).clone(); - let contacts = Contacts { - count: profiles.len(), - items: profiles - .into_iter() - .map(|contact| ContactListItem::new(contact.public_key(), contact.metadata())) - .collect(), - }; - - this.list = ListState::new( - contacts.items.len(), - ListAlignment::Top, - Pixels(50.), - move |idx, _cx| { - let item = contacts.items.get(idx).unwrap().clone(); - div().child(item).into_any_element() - }, - ); - - cx.notify(); - }) - .detach(); - - Self { - list, - contacts, - selected: HashSet::new(), - focus_handle: cx.focus_handle(), - } - } - - pub fn selected(&self) -> Vec { - self.selected.clone().into_iter().collect() - } - - fn on_action_select(&mut self, action: &SelectContact, cx: &mut ViewContext) { - self.selected.insert(action.0); - - let profiles = self.contacts.read(cx).clone(); - let contacts = Contacts { - count: profiles.len(), - items: profiles - .into_iter() - .map(|contact| { - let public_key = contact.public_key(); - let metadata = contact.metadata(); - - ContactListItem::new(contact.public_key(), metadata) - .selected(self.selected.contains(&public_key)) - }) - .collect(), - }; - - self.list = ListState::new( - contacts.items.len(), - ListAlignment::Top, - Pixels(50.), - move |idx, _cx| { - let item = contacts.items.get(idx).unwrap().clone(); - div().child(item).into_any_element() - }, - ); - - cx.notify(); - } -} - -impl Render for ContactList { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - div() - .track_focus(&self.focus_handle) - .on_action(cx.listener(Self::on_action_select)) - .flex() - .flex_col() - .gap_1() - .child(div().font_semibold().text_sm().child("Contacts")) - .child( - div() - .p_1() - .bg(cx.theme().base.step(cx, ColorScaleStep::THREE)) - .text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN)) - .rounded_lg() - .child(list(self.list.clone()).h(px(300.))), - ) - } -} diff --git a/crates/app/src/views/sidebar/inbox.rs b/crates/app/src/views/sidebar/inbox.rs index b7f416d..1ea0dd3 100644 --- a/crates/app/src/views/sidebar/inbox.rs +++ b/crates/app/src/views/sidebar/inbox.rs @@ -1,6 +1,6 @@ use crate::{ states::chat::ChatRegistry, - utils::ago, + utils::message_ago, views::app::{AddPanel, PanelKind}, }; use gpui::{ @@ -22,7 +22,7 @@ pub struct Inbox { impl Inbox { pub fn new(_cx: &mut ViewContext<'_, Self>) -> Self { Self { - label: "Inbox".into(), + label: "Direct Messages".into(), is_collapsed: false, } } @@ -54,7 +54,7 @@ impl Inbox { let room = model.read(cx); let id = room.id; let room_id: SharedString = id.to_string().into(); - let ago: SharedString = ago(room.last_seen).into(); + let ago: SharedString = message_ago(room.last_seen).into(); div() .id(room_id) diff --git a/crates/app/src/views/sidebar/mod.rs b/crates/app/src/views/sidebar/mod.rs index ca546a2..d664b7a 100644 --- a/crates/app/src/views/sidebar/mod.rs +++ b/crates/app/src/views/sidebar/mod.rs @@ -1,7 +1,7 @@ use crate::views::sidebar::inbox::Inbox; -use contact_list::ContactList; +use compose::Compose; use gpui::{ - div, AnyElement, AppContext, Entity, EntityId, EventEmitter, FocusHandle, FocusableView, + AnyElement, AppContext, Entity, EntityId, EventEmitter, FocusHandle, FocusableView, IntoElement, ParentElement, Render, SharedString, Styled, View, ViewContext, VisualContext, WindowContext, }; @@ -16,7 +16,7 @@ use ui::{ v_flex, ContextModal, Icon, IconName, Sizable, StyledExt, }; -mod contact_list; +mod compose; mod inbox; pub struct Sidebar { @@ -49,24 +49,27 @@ impl Sidebar { } fn show_compose(&mut self, cx: &mut ViewContext) { - let contact_list = cx.new_view(ContactList::new); + let compose = cx.new_view(Compose::new); - cx.open_modal(move |modal, _cx| { - modal.child(contact_list.clone()).footer( - div().flex().gap_2().child( + cx.open_modal(move |modal, cx| { + let selected = compose.model.read(cx).selected(cx); + let label = if selected.len() > 1 { + "Create Group DM" + } else { + "Create DM" + }; + + modal + .title("Direct Messages") + .child(compose.clone()) + .footer( Button::new("create") - .label("Create DM") + .label(label) .primary() + .bold() .rounded(ButtonRounded::Large) - .w_full() - .on_click({ - let contact_list = contact_list.clone(); - move |_, cx| { - let _selected = contact_list.model.read(cx).selected(); - } - }), - ), - ) + .w_full(), + ) }) } } @@ -116,26 +119,15 @@ impl Render for Sidebar { .py_3() .gap_3() .child( - v_flex() - .px_2() - .gap_0p5() - .child( - Button::new("compose") - .small() - .ghost() - .not_centered() - .icon(Icon::new(IconName::ComposeFill)) - .label("New Message") - .on_click(cx.listener(|this, _, cx| this.show_compose(cx))), - ) - .child( - Button::new("contacts") - .small() - .ghost() - .not_centered() - .icon(Icon::new(IconName::GroupFill)) - .label("Contacts"), - ), + v_flex().px_2().gap_0p5().child( + Button::new("compose") + .small() + .ghost() + .not_centered() + .icon(Icon::new(IconName::ComposeFill)) + .label("New Message") + .on_click(cx.listener(|this, _, cx| this.show_compose(cx))), + ), ) .child(self.inbox.clone()) } diff --git a/crates/app/src/views/welcome.rs b/crates/app/src/views/welcome.rs index 84768c5..5e1131e 100644 --- a/crates/app/src/views/welcome.rs +++ b/crates/app/src/views/welcome.rs @@ -1,5 +1,5 @@ use gpui::{ - div, AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView, IntoElement, + div, svg, AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView, IntoElement, ParentElement, Render, SharedString, Styled, View, ViewContext, VisualContext, WindowContext, }; use ui::{ @@ -80,9 +80,25 @@ impl Render for WelcomePanel { .flex() .items_center() .justify_center() - .child("coop on nostr.") - .text_color(cx.theme().base.step(cx, ColorScaleStep::THREE)) - .font_black() - .text_sm() + .child( + div() + .flex() + .flex_col() + .items_center() + .gap_1() + .child( + svg() + .path("brand/coop.svg") + .size_12() + .text_color(cx.theme().base.step(cx, ColorScaleStep::THREE)), + ) + .child( + div() + .child("coop on nostr.") + .text_color(cx.theme().base.step(cx, ColorScaleStep::FOUR)) + .font_black() + .text_sm(), + ), + ) } } diff --git a/crates/ui/src/button.rs b/crates/ui/src/button.rs index 132901f..17a3ba1 100644 --- a/crates/ui/src/button.rs +++ b/crates/ui/src/button.rs @@ -353,7 +353,7 @@ impl RenderOnce for Button { Size::Size(size) => this.px(size * 0.2), Size::XSmall => this.h_6().px_0p5(), Size::Small => this.h_8().px_2(), - _ => this.h_10().px_4(), + _ => this.h_9().px_3(), } } }) @@ -487,7 +487,7 @@ impl ButtonVariant { fn text_color(&self, cx: &WindowContext) -> Hsla { match self { - ButtonVariant::Primary => cx.theme().accent.step(cx, ColorScaleStep::ONE), + ButtonVariant::Primary => cx.theme().base.step(cx, ColorScaleStep::TWELVE), ButtonVariant::Link => cx.theme().accent.step(cx, ColorScaleStep::NINE), ButtonVariant::Custom(colors) => colors.foreground, _ => cx.theme().base.step(cx, ColorScaleStep::TWELVE), diff --git a/crates/ui/src/dropdown.rs b/crates/ui/src/dropdown.rs index d909356..a9c54c4 100644 --- a/crates/ui/src/dropdown.rs +++ b/crates/ui/src/dropdown.rs @@ -72,7 +72,7 @@ pub trait DropdownDelegate: Sized { Self::Item: DropdownItem, V: PartialEq, { - (0..self.len()).find(|&i| self.get(i).map_or(false, |item| item.value() == value)) + (0..self.len()).find(|&i| self.get(i).is_some_and(|item| item.value() == value)) } fn can_search(&self) -> bool { @@ -125,9 +125,7 @@ where } fn render_item(&self, ix: usize, cx: &mut gpui::ViewContext>) -> Option { - let selected = self - .selected_index - .map_or(false, |selected_index| selected_index == ix); + let selected = self.selected_index == Some(ix); let size = self .dropdown .upgrade() diff --git a/crates/ui/src/modal.rs b/crates/ui/src/modal.rs index 31ac969..ba45e15 100644 --- a/crates/ui/src/modal.rs +++ b/crates/ui/src/modal.rs @@ -2,7 +2,7 @@ use crate::{ animation::cubic_bezier, button::{Button, ButtonVariants as _}, theme::{scale::ColorScaleStep, ActiveTheme as _}, - v_flex, ContextModal, IconName, Sizable as _, + v_flex, ContextModal, IconName, Sizable as _, StyledExt, }; use gpui::{ actions, anchored, div, hsla, point, prelude::FluentBuilder, px, relative, Animation, @@ -219,7 +219,13 @@ impl RenderOnce for Modal { .w(self.width) .when_some(self.max_width, |this, w| this.max_w(w)) .when_some(self.title, |this, title| { - this.child(div().line_height(relative(1.)).child(title)) + this.child( + div() + .text_sm() + .font_semibold() + .line_height(relative(1.)) + .child(title), + ) }) .when(self.show_close, |this| { this.child(