migrate to gpui-component

This commit is contained in:
2026-06-02 18:15:54 +07:00
parent 5d4c8634ef
commit bac04ab4da
116 changed files with 1165 additions and 24445 deletions

View File

@@ -6,8 +6,6 @@ publish.workspace = true
[dependencies]
state = { path = "../state" }
ui = { path = "../ui" }
theme = { path = "../theme" }
common = { path = "../common" }
person = { path = "../person" }
chat = { path = "../chat" }
@@ -15,6 +13,7 @@ settings = { path = "../settings" }
gpui.workspace = true
gpui_tokio.workspace = true
gpui-component.workspace = true
nostr-sdk.workspace = true
anyhow.workspace = true

View File

@@ -4,15 +4,26 @@ use std::sync::Arc;
pub use actions::*;
use anyhow::{Context as AnyhowContext, Error};
use chat::{ChatRegistry, Message, RenderedMessage, Room, RoomEvent, SendReport, SendStatus};
use common::{TimestampExt, coop_cache};
use common::{CoopIcon, TimestampExt, coop_cache};
use gpui::prelude::FluentBuilder;
use gpui::{
AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
Focusable, InteractiveElement, IntoElement, ListAlignment, ListOffset, ListState, MouseButton,
ObjectFit, ParentElement, PathPromptOptions, Render, SharedString, SharedUri,
AnyElement, App, AppContext, ClipboardItem, Context, Element, Entity, EventEmitter,
FocusHandle, Focusable, InteractiveElement, IntoElement, ListAlignment, ListOffset, ListState,
MouseButton, ObjectFit, ParentElement, PathPromptOptions, Render, SharedString, SharedUri,
StatefulInteractiveElement, Styled, StyledImage, Subscription, Task, WeakEntity, Window,
deferred, div, img, list, px, red, relative, svg, white,
};
use gpui_component::avatar::Avatar;
use gpui_component::button::{Button, ButtonVariants};
use gpui_component::dock::{Panel, PanelEvent};
use gpui_component::input::{Input, InputEvent, InputState};
use gpui_component::menu::DropdownMenu;
use gpui_component::notification::Notification;
use gpui_component::scroll::Scrollbar;
use gpui_component::{
ActiveTheme, Disableable, Icon, InteractiveElementExt, Sizable, StyledExt, WindowExt, h_flex,
v_flex,
};
use itertools::Itertools;
use nostr_sdk::prelude::*;
use person::{Person, PersonRegistry};
@@ -20,18 +31,6 @@ use settings::{AppSettings, SignerKind};
use smallvec::{SmallVec, smallvec};
use smol::lock::RwLock;
use state::{NostrRegistry, upload};
use theme::ActiveTheme;
use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants};
use ui::dock::{Panel, PanelEvent};
use ui::input::{InputEvent, InputState, TextInput};
use ui::menu::DropdownMenu;
use ui::notification::Notification;
use ui::scroll::Scrollbar;
use ui::{
Disableable, Icon, IconName, InteractiveElementExt, Sizable, StyledExt, WindowExtension,
h_flex, v_flex,
};
use crate::text::RenderedText;
@@ -119,7 +118,7 @@ impl ChatPanel {
InputState::new(window, cx)
.placeholder(format!("Message {}", name))
.auto_grow(1, 20)
.prevent_new_line_on_enter()
.submit_on_enter(true)
.clean_on_escape()
});
@@ -674,8 +673,8 @@ impl ChatPanel {
let chat = ChatRegistry::global(cx);
let seen_on = chat.read(cx).rumor_seen_on(id);
window.open_modal(cx, move |this, _window, cx| {
this.title("Seen on").show_close(true).child(
window.open_dialog(cx, move |this, _window, cx| {
this.title("Seen on").close_button(true).child(
v_flex()
.gap_1()
.when_none(&seen_on, |this| {
@@ -684,7 +683,7 @@ impl ChatPanel {
.h_10()
.justify_center()
.text_sm()
.bg(cx.theme().elevated_surface_background)
.bg(cx.theme().muted)
.rounded(cx.theme().radius)
.child("Message isn't traced yet"),
)
@@ -699,7 +698,7 @@ impl ChatPanel {
.h_7()
.px_2()
.gap_2()
.bg(cx.theme().elevated_surface_background)
.bg(cx.theme().muted)
.rounded(cx.theme().radius)
.text_sm()
.child(div().size_1p5().rounded_full().bg(gpui::green()))
@@ -717,11 +716,11 @@ impl ChatPanel {
fn open_relays(&mut self, public_key: &PublicKey, window: &mut Window, cx: &mut Context<Self>) {
let profile = self.profile(public_key, cx);
window.open_modal(cx, move |this, _window, cx| {
window.open_dialog(cx, move |this, _window, cx| {
let relays = profile.messaging_relays();
this.title("Messaging Relays")
.show_close(true)
.close_button(true)
.child(v_flex().gap_1().children({
let mut items = vec![];
@@ -731,7 +730,7 @@ impl ChatPanel {
.h_7()
.px_2()
.gap_2()
.bg(cx.theme().elevated_surface_background)
.bg(cx.theme().muted)
.rounded(cx.theme().radius)
.text_sm()
.child(div().size_1p5().rounded_full().bg(gpui::green()))
@@ -760,13 +759,13 @@ impl ChatPanel {
.justify_center()
.text_center()
.text_xs()
.text_color(cx.theme().text_placeholder)
.text_color(cx.theme().muted_foreground)
.line_height(relative(1.3))
.child(
svg()
.path("brand/coop.svg")
.size_12()
.text_color(cx.theme().ghost_element_active),
.text_color(cx.theme().muted_foreground),
)
.child(SharedString::from(ANNOUNCEMENT))
.into_any_element()
@@ -789,9 +788,9 @@ impl ChatPanel {
.size_8()
.justify_center()
.rounded_full()
.bg(cx.theme().warning_background)
.bg(cx.theme().warning)
.text_color(cx.theme().warning_foreground)
.child(Icon::new(IconName::Warning).small()),
.child(Icon::new(CoopIcon::Warning).small()),
)
.child(
div()
@@ -867,15 +866,11 @@ impl ChatPanel {
.gap_3()
.when(!hide_avatar, |this| {
this.child(
Avatar::new(author.avatar())
Avatar::new()
.src(author.avatar())
.name(author.name())
.flex_shrink_0()
.relative()
.dropdown_menu(move |this, _window, _cx| {
this.menu("Public Key", Box::new(Command::Copy(pk)))
.menu("View Relays", Box::new(Command::Relays(pk)))
.separator()
.menu("View on njump.me", Box::new(Command::Njump(pk)))
}),
.relative(),
)
})
.child(
@@ -888,17 +883,17 @@ impl ChatPanel {
h_flex()
.gap_2()
.text_sm()
.text_color(cx.theme().text_placeholder)
.text_color(cx.theme().muted_foreground)
.child(
div()
.font_semibold()
.text_color(cx.theme().text)
.text_color(cx.theme().foreground)
.child(author.name()),
)
.when(encrypted_by_dekey, |this| {
this.child(
Button::new(format!("dekey-{ix}"))
.icon(IconName::Shield)
.icon(CoopIcon::Shield)
.ghost()
.xsmall()
.px_4()
@@ -920,13 +915,13 @@ impl ChatPanel {
)
.child(
div()
.group_hover("", |this| this.bg(cx.theme().element_active))
.group_hover("", |this| this.bg(cx.theme().primary_active))
.absolute()
.left_0()
.top_0()
.w(px(2.))
.h_full()
.bg(cx.theme().border_transparent),
.bg(cx.theme().transparent),
)
.child(self.render_actions(&id, &pk, cx))
.on_mouse_down(
@@ -938,7 +933,7 @@ impl ChatPanel {
.on_double_click(cx.listener(move |this, _, _window, cx| {
this.reply_to(&id, cx);
}))
.hover(|this| this.bg(cx.theme().surface_background))
.hover(|this| this.bg(cx.theme().muted))
.into_any_element()
}
@@ -953,7 +948,7 @@ impl ChatPanel {
return div().child(
img(media[0].clone())
.border_1()
.border_color(cx.theme().border_variant)
.border_color(cx.theme().border)
.h(px(250.))
.object_fit(ObjectFit::Cover)
.rounded(cx.theme().radius),
@@ -981,7 +976,7 @@ impl ChatPanel {
img(item.clone())
.h_32()
.border_1()
.border_color(cx.theme().border_variant)
.border_color(cx.theme().border)
.rounded(cx.theme().radius),
),
);
@@ -1010,11 +1005,11 @@ impl ChatPanel {
.w_full()
.px_2()
.border_l_2()
.border_color(cx.theme().element_selected)
.border_color(cx.theme().primary_active)
.text_sm()
.child(
div()
.text_color(cx.theme().text_accent)
.text_color(cx.theme().accent_foreground)
.child(author.name()),
)
.child(
@@ -1024,7 +1019,7 @@ impl ChatPanel {
.line_clamp(1)
.child(SharedString::from(&message.content)),
)
.hover(|this| this.bg(cx.theme().elevated_surface_background))
.hover(|this| this.bg(cx.theme().muted))
.on_click({
let id = *id;
cx.listener(move |this, _event, _window, _cx| {
@@ -1065,15 +1060,15 @@ impl ChatPanel {
div()
.id(SharedString::from(id.to_hex()))
.child(label)
.when(failed, |this| this.text_color(cx.theme().text_danger))
.when(failed, |this| this.text_color(cx.theme().danger_foreground))
.when_some(reports, |this, reports| {
this.when(!pending, |this| {
this.on_click(move |_e, window, cx| {
let reports = reports.clone();
window.open_modal(cx, move |this, _window, cx| {
window.open_dialog(cx, move |this, _window, cx| {
this.title(SharedString::from("Sent Reports"))
.show_close(true)
.close_button(true)
.child(v_flex().gap_4().children({
let mut items = Vec::with_capacity(reports.len());
@@ -1107,7 +1102,7 @@ impl ChatPanel {
h_flex()
.gap_1()
.font_semibold()
.child(Avatar::new(avatar).small())
.child(Avatar::new().src(avatar).name(name.clone()).small())
.child(name.clone()),
),
)
@@ -1121,7 +1116,7 @@ impl ChatPanel {
.w_full()
.text_sm()
.rounded(cx.theme().radius)
.bg(cx.theme().warning_background)
.bg(cx.theme().warning)
.text_color(cx.theme().warning_foreground)
.child(div().flex_1().w_full().text_center().child(error)),
)
@@ -1141,7 +1136,7 @@ impl ChatPanel {
.p_1()
.w_full()
.rounded(cx.theme().radius)
.bg(cx.theme().danger_background)
.bg(cx.theme().danger)
.child(
div()
.text_xs()
@@ -1171,7 +1166,7 @@ impl ChatPanel {
.p_1()
.w_full()
.rounded(cx.theme().radius)
.bg(cx.theme().elevated_surface_background)
.bg(cx.theme().muted)
.child(
div()
.text_xs()
@@ -1214,7 +1209,7 @@ impl ChatPanel {
.bg(cx.theme().background)
.child(
Button::new("reply")
.icon(IconName::Reply)
.icon(CoopIcon::Reply)
.tooltip("Reply")
.small()
.ghost()
@@ -1227,7 +1222,7 @@ impl ChatPanel {
)
.child(
Button::new("copy")
.icon(IconName::Copy)
.icon(CoopIcon::Copy)
.tooltip("Copy")
.small()
.ghost()
@@ -1241,7 +1236,7 @@ impl ChatPanel {
.child(div().flex_shrink_0().h_4().w_px().bg(cx.theme().border))
.child(
Button::new("advance")
.icon(IconName::Ellipsis)
.icon(CoopIcon::Ellipsis)
.small()
.ghost()
.dropdown_menu({
@@ -1279,7 +1274,7 @@ impl ChatPanel {
.justify_center()
.rounded_full()
.bg(red())
.child(Icon::new(IconName::Close).size_2().text_color(white())),
.child(Icon::new(CoopIcon::Close).size_2().text_color(white())),
)
.on_click({
let url = url.clone();
@@ -1312,7 +1307,7 @@ impl ChatPanel {
.w_full()
.pl_2()
.border_l_2()
.border_color(cx.theme().element_active)
.border_color(cx.theme().secondary_active)
.child(
div()
.flex()
@@ -1324,17 +1319,17 @@ impl ChatPanel {
.items_baseline()
.gap_1()
.text_xs()
.text_color(cx.theme().text_muted)
.text_color(cx.theme().muted_foreground)
.child(SharedString::from("Replying to:"))
.child(
div()
.text_color(cx.theme().text_accent)
.text_color(cx.theme().secondary_foreground)
.child(profile.name()),
),
)
.child(
Button::new("remove-reply")
.icon(IconName::Close)
.icon(CoopIcon::Close)
.xsmall()
.ghost()
.on_click({
@@ -1382,7 +1377,7 @@ impl ChatPanel {
.unwrap_or((true, SignerKind::default()));
Button::new("encryption")
.icon(IconName::Settings2)
.icon(CoopIcon::Settings2)
.tooltip("Configuration")
.ghost()
.large()
@@ -1418,11 +1413,11 @@ impl ChatPanel {
fn render_emoji_menu(&self, _window: &Window, _cx: &Context<Self>) -> impl IntoElement {
Button::new("emoji")
.icon(IconName::Emoji)
.icon(CoopIcon::Emoji)
.ghost()
.large()
.dropdown_menu_with_anchor(gpui::Anchor::BottomLeft, move |this, _window, _cx| {
this.horizontal()
this.separator()
.menu("👍", Box::new(Command::Insert("👍")))
.menu("👎", Box::new(Command::Insert("👎")))
.menu("😄", Box::new(Command::Insert("😄")))
@@ -1436,11 +1431,11 @@ impl ChatPanel {
}
impl Panel for ChatPanel {
fn panel_id(&self) -> SharedString {
self.id.clone()
fn panel_name(&self) -> &'static str {
"Chat"
}
fn title(&self, cx: &App) -> AnyElement {
fn title(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
self.room
.read_with(cx, |this, cx| {
let label = this.display_name(cx);
@@ -1448,19 +1443,24 @@ impl Panel for ChatPanel {
h_flex()
.gap_1p5()
.child(Avatar::new(url).xsmall())
.child(Avatar::new().src(url).xsmall())
.child(label)
.into_any_element()
})
.unwrap_or(div().child("Unknown").into_any_element())
.unwrap_or(div().child("Unknown").into_any())
.into_any()
}
fn toolbar_buttons(&self, _window: &Window, _cx: &App) -> Vec<Button> {
fn toolbar_buttons(
&mut self,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<Vec<Button>> {
let subject_bar = self.subject_bar.clone();
vec![
Some(vec![
Button::new("subject")
.icon(IconName::Input)
.icon(CoopIcon::Input)
.tooltip("Change subject")
.small()
.ghost()
@@ -1470,7 +1470,7 @@ impl Panel for ChatPanel {
cx.notify();
});
}),
]
])
}
}
@@ -1497,15 +1497,10 @@ impl Render for ChatPanel {
.gap_2()
.border_b_1()
.border_color(cx.theme().border)
.child(
TextInput::new(&self.subject_input)
.text_sm()
.small()
.bordered(false),
)
.child(Input::new(&self.subject_input).text_sm().bordered(false))
.child(
Button::new("change")
.icon(IconName::CheckCircle)
.icon(CoopIcon::CheckCircle)
.label("Change")
.secondary()
.disabled(self.uploading)
@@ -1543,7 +1538,7 @@ impl Render for ChatPanel {
.items_end()
.child(
Button::new("upload")
.icon(IconName::Plus)
.icon(CoopIcon::Plus)
.tooltip("Upload media")
.loading(self.uploading)
.disabled(self.uploading)
@@ -1553,12 +1548,7 @@ impl Render for ChatPanel {
this.upload(window, cx);
})),
)
.child(
TextInput::new(&self.input)
.appearance(false)
.text_sm()
.flex_1(),
)
.child(Input::new(&self.input).appearance(false).text_sm().flex_1())
.child(
h_flex()
.pl_1()
@@ -1567,7 +1557,7 @@ impl Render for ChatPanel {
.child(self.render_config_menu(window, cx))
.child(
Button::new("send")
.icon(IconName::PaperPlaneFill)
.icon(CoopIcon::PaperPlaneFill)
.disabled(self.uploading)
.ghost()
.large()

View File

@@ -7,8 +7,8 @@ use gpui::{
AnyElement, App, ElementId, Entity, FontStyle, FontWeight, HighlightStyle, InteractiveText,
IntoElement, SharedString, StrikethroughStyle, StyledText, UnderlineStyle, Window,
};
use gpui_component::ActiveTheme;
use person::PersonRegistry;
use theme::ActiveTheme;
#[allow(clippy::enum_variant_names)]
#[allow(dead_code)]
@@ -68,8 +68,8 @@ impl RenderedText {
}
pub fn element(&self, id: ElementId, window: &Window, cx: &App) -> AnyElement {
let code_background = cx.theme().elevated_surface_background;
let color = cx.theme().text_accent;
let code_background = cx.theme().secondary;
let color = cx.theme().primary_active;
InteractiveText::new(
id,