feat: add contact list panel #10

Merged
reya merged 4 commits from feat/contact-list into master 2026-02-28 01:50:34 +00:00
7 changed files with 56 additions and 34 deletions
Showing only changes of commit 0236316999 - Show all commits

View File

@@ -8,7 +8,7 @@ use chat::{Message, RenderedMessage, Room, RoomEvent, SendReport};
use common::RenderedTimestamp; use common::RenderedTimestamp;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
deferred, div, img, list, px, red, relative, rems, svg, white, AnyElement, App, AppContext, deferred, div, img, list, px, red, relative, svg, white, AnyElement, App, AppContext,
ClipboardItem, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, ClipboardItem, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement,
IntoElement, ListAlignment, ListOffset, ListState, MouseButton, ObjectFit, ParentElement, IntoElement, ListAlignment, ListOffset, ListState, MouseButton, ObjectFit, ParentElement,
PathPromptOptions, Render, SharedString, StatefulInteractiveElement, Styled, StyledImage, PathPromptOptions, Render, SharedString, StatefulInteractiveElement, Styled, StyledImage,
@@ -758,7 +758,7 @@ impl ChatPanel {
this.child( this.child(
div() div()
.id(SharedString::from(format!("{ix}-avatar"))) .id(SharedString::from(format!("{ix}-avatar")))
.child(Avatar::new(author.avatar()).size(rems(2.))) .child(Avatar::new(author.avatar()))
.context_menu(move |this, _window, _cx| { .context_menu(move |this, _window, _cx| {
let view = Box::new(OpenPublicKey(public_key)); let view = Box::new(OpenPublicKey(public_key));
let copy = Box::new(CopyPublicKey(public_key)); let copy = Box::new(CopyPublicKey(public_key));
@@ -940,7 +940,7 @@ impl ChatPanel {
h_flex() h_flex()
.gap_1() .gap_1()
.font_semibold() .font_semibold()
.child(Avatar::new(avatar).size(rems(1.25))) .child(Avatar::new(avatar).small())
.child(name.clone()), .child(name.clone()),
), ),
) )
@@ -1283,7 +1283,7 @@ impl Panel for ChatPanel {
h_flex() h_flex()
.gap_1p5() .gap_1p5()
.child(Avatar::new(url).size(rems(1.25))) .child(Avatar::new(url).small())
.child(label) .child(label)
.into_any_element() .into_any_element()
}) })

View File

@@ -5,9 +5,8 @@ use anyhow::{Context as AnyhowContext, Error};
use common::RenderedTimestamp; use common::RenderedTimestamp;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, px, relative, rems, uniform_list, App, AppContext, Context, Div, Entity, div, px, relative, uniform_list, App, AppContext, Context, Div, Entity, InteractiveElement,
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Task, Window,
Task, Window,
}; };
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use person::{shorten_pubkey, Person, PersonRegistry}; use person::{shorten_pubkey, Person, PersonRegistry};
@@ -275,7 +274,7 @@ impl Screening {
.rounded(cx.theme().radius) .rounded(cx.theme().radius)
.text_sm() .text_sm()
.hover(|this| this.bg(cx.theme().elevated_surface_background)) .hover(|this| this.bg(cx.theme().elevated_surface_background))
.child(Avatar::new(profile.avatar()).size(rems(1.75))) .child(Avatar::new(profile.avatar()).small())
.child(profile.name()), .child(profile.name()),
); );
} }
@@ -315,7 +314,7 @@ impl Render for Screening {
.items_center() .items_center()
.justify_center() .justify_center()
.text_center() .text_center()
.child(Avatar::new(profile.avatar()).size(rems(4.))) .child(Avatar::new(profile.avatar()).large())
.child( .child(
div() div()
.font_semibold() .font_semibold()

View File

@@ -234,7 +234,7 @@ impl ContactListPanel {
h_flex() h_flex()
.gap_2() .gap_2()
.text_sm() .text_sm()
.child(Avatar::new(profile.avatar()).size(rems(1.5))) .child(Avatar::new(profile.avatar()).small())
.child(profile.name()), .child(profile.name()),
) )
.child( .child(

View File

@@ -3,9 +3,9 @@ use std::time::Duration;
use anyhow::{Context as AnyhowContext, Error}; use anyhow::{Context as AnyhowContext, Error};
use gpui::{ use gpui::{
div, rems, AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, div, AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions, Render, SharedString, Focusable, IntoElement, ParentElement, PathPromptOptions, Render, SharedString, Styled, Task,
Styled, Task, Window, Window,
}; };
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use person::{shorten_pubkey, Person, PersonRegistry}; use person::{shorten_pubkey, Person, PersonRegistry};
@@ -322,7 +322,7 @@ impl Render for ProfilePanel {
.items_center() .items_center()
.justify_center() .justify_center()
.gap_4() .gap_4()
.child(Avatar::new(avatar).size(rems(4.25))) .child(Avatar::new(avatar).large())
.child( .child(
Button::new("upload") Button::new("upload")
.icon(IconName::PlusCircle) .icon(IconName::PlusCircle)

View File

@@ -3,7 +3,7 @@ use std::rc::Rc;
use chat::RoomKind; use chat::RoomKind;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, rems, App, ClickEvent, InteractiveElement, IntoElement, ParentElement as _, RenderOnce, div, App, ClickEvent, InteractiveElement, IntoElement, ParentElement as _, RenderOnce,
SharedString, StatefulInteractiveElement, Styled, Window, SharedString, StatefulInteractiveElement, Styled, Window,
}; };
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
@@ -106,14 +106,7 @@ impl RenderOnce for RoomEntry {
.rounded(cx.theme().radius) .rounded(cx.theme().radius)
.when(!hide_avatar, |this| { .when(!hide_avatar, |this| {
this.when_some(self.avatar, |this, avatar| { this.when_some(self.avatar, |this, avatar| {
this.child( this.child(Avatar::new(avatar).small().flex_shrink_0())
div()
.flex_shrink_0()
.size_6()
.rounded_full()
.overflow_hidden()
.child(Avatar::new(avatar).size(rems(1.5))),
)
}) })
}) })
.child( .child(

View File

@@ -4,7 +4,7 @@ use ::settings::AppSettings;
use chat::{ChatEvent, ChatRegistry, InboxState}; use chat::{ChatEvent, ChatRegistry, InboxState};
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, px, rems, Action, App, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement, div, px, Action, App, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement,
ParentElement, Render, SharedString, Styled, Subscription, Window, ParentElement, Render, SharedString, Styled, Subscription, Window,
}; };
use person::PersonRegistry; use person::PersonRegistry;
@@ -385,7 +385,7 @@ impl Workspace {
this.child( this.child(
Button::new("current-user") Button::new("current-user")
.child(Avatar::new(profile.avatar()).size(rems(1.25))) .child(Avatar::new(profile.avatar()).xsmall())
.small() .small()
.caret() .caret()
.compact() .compact()

View File

@@ -1,10 +1,24 @@
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, img, px, rems, AbsoluteLength, App, Hsla, ImageSource, Img, IntoElement, ParentElement, div, img, px, AbsoluteLength, App, Div, Hsla, ImageSource, Img, InteractiveElement,
RenderOnce, Styled, StyledImage, Window, Interactivity, IntoElement, ParentElement, RenderOnce, StyleRefinement, Styled, StyledImage,
Window,
}; };
use theme::ActiveTheme; use theme::ActiveTheme;
use crate::{Sizable, Size};
/// Returns the size of the avatar based on the given [`Size`].
pub(super) fn avatar_size(size: Size) -> AbsoluteLength {
match size {
Size::Large => px(64.).into(),
Size::Medium => px(32.).into(),
Size::Small => px(24.).into(),
Size::XSmall => px(20.).into(),
Size::Size(size) => size.into(),
}
}
/// An element that renders a user avatar with customizable appearance options. /// An element that renders a user avatar with customizable appearance options.
/// ///
/// # Examples /// # Examples
@@ -18,8 +32,10 @@ use theme::ActiveTheme;
/// ``` /// ```
#[derive(IntoElement)] #[derive(IntoElement)]
pub struct Avatar { pub struct Avatar {
base: Div,
image: Img, image: Img,
size: Option<AbsoluteLength>, style: StyleRefinement,
size: Size,
border_color: Option<Hsla>, border_color: Option<Hsla>,
} }
@@ -27,8 +43,10 @@ impl Avatar {
/// Creates a new avatar element with the specified image source. /// Creates a new avatar element with the specified image source.
pub fn new(src: impl Into<ImageSource>) -> Self { pub fn new(src: impl Into<ImageSource>) -> Self {
Avatar { Avatar {
base: div(),
image: img(src), image: img(src),
size: None, style: StyleRefinement::default(),
size: Size::Medium,
border_color: None, border_color: None,
} }
} }
@@ -56,14 +74,27 @@ impl Avatar {
self.border_color = Some(color.into()); self.border_color = Some(color.into());
self self
} }
}
/// Size overrides the avatar size. By default they are 1rem. impl Sizable for Avatar {
pub fn size<L: Into<AbsoluteLength>>(mut self, size: impl Into<Option<L>>) -> Self { fn with_size(mut self, size: impl Into<Size>) -> Self {
self.size = size.into().map(Into::into); self.size = size.into();
self self
} }
} }
impl Styled for Avatar {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
impl InteractiveElement for Avatar {
fn interactivity(&mut self) -> &mut Interactivity {
self.base.interactivity()
}
}
impl RenderOnce for Avatar { impl RenderOnce for Avatar {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let border_width = if self.border_color.is_some() { let border_width = if self.border_color.is_some() {
@@ -71,8 +102,7 @@ impl RenderOnce for Avatar {
} else { } else {
px(0.) px(0.)
}; };
let image_size = avatar_size(self.size);
let image_size = self.size.unwrap_or_else(|| rems(1.).into());
let container_size = image_size.to_pixels(window.rem_size()) + border_width * 2.; let container_size = image_size.to_pixels(window.rem_size()) + border_width * 2.;
div() div()