chore: use same composer and message actions for nip29

This commit is contained in:
2026-11-11 10:40:34 +07:00
parent 4166b30319
commit 9ec8b128db
3 changed files with 139 additions and 50 deletions
+15 -19
View File
@@ -2047,18 +2047,6 @@ impl Render for ChatPanel {
this.upload(window, cx); this.upload(window, cx);
})), })),
) )
.when(cfg!(not(target_arch = "wasm32")), |this| {
this.child(
Button::new("nutzap")
.icon(IconName::Wallet)
.tooltip("Send eCash")
.disabled(dictating)
.ghost()
.on_click(cx.listener(move |this, _ev, window, cx| {
this.open_nutzap(window, cx);
})),
)
})
.when(dictating, |this| { .when(dictating, |this| {
this.child( this.child(
h_flex() h_flex()
@@ -2082,17 +2070,25 @@ impl Render for ChatPanel {
.flex_1(), .flex_1(),
) )
}) })
.child(
h_flex()
.items_end()
.gap_1()
.pl_1()
.child(SpeechButton::new(&self.speech)),
)
.child( .child(
h_flex() h_flex()
.pl_1() .pl_1()
.gap_1() .gap_1()
.child(SpeechButton::new(&self.speech))
.when(cfg!(not(target_arch = "wasm32")), |this| {
this.child(
Button::new("nutzap")
.icon(IconName::Wallet)
.tooltip("Send eCash")
.disabled(dictating)
.ghost()
.on_click(cx.listener(
move |this, _ev, window, cx| {
this.open_nutzap(window, cx);
},
)),
)
})
.child(self.render_emoji_menu(dictating, window, cx)) .child(self.render_emoji_menu(dictating, window, cx))
.child(self.render_config_menu(dictating, window, cx)) .child(self.render_config_menu(dictating, window, cx))
.child( .child(
+78 -21
View File
@@ -2,13 +2,13 @@ use std::collections::BTreeMap;
use std::time::Duration; use std::time::Duration;
use anyhow::Result; use anyhow::Result;
use chat_ui::ToggleDictation; use chat_ui::{Command, ToggleDictation};
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
Action, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, Action, Anchor, AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter,
FollowMode, InteractiveElement, IntoElement, ListAlignment, ListScrollEvent, ListState, FocusHandle, Focusable, FollowMode, InteractiveElement, IntoElement, ListAlignment,
ParentElement, Render, SharedString, Styled, Subscription, Task, WeakEntity, Window, div, list, ListScrollEvent, ListState, ParentElement, Render, SharedString, Styled, Subscription, Task,
px, WeakEntity, Window, div, list, px,
}; };
use gpui_kit::assets::IconName; use gpui_kit::assets::IconName;
use gpui_kit::base::StyledExt as _; use gpui_kit::base::StyledExt as _;
@@ -16,6 +16,7 @@ use gpui_kit::component::button::{Button, ButtonVariant, ButtonVariants};
use gpui_kit::component::dialog::DialogButtonProps; use gpui_kit::component::dialog::DialogButtonProps;
use gpui_kit::component::dock::{Panel, PanelEvent}; use gpui_kit::component::dock::{Panel, PanelEvent};
use gpui_kit::component::input::{InputEvent, Textarea, TextareaState}; use gpui_kit::component::input::{InputEvent, Textarea, TextareaState};
use gpui_kit::component::menu::DropdownMenu;
use gpui_kit::component::notification::Notification; use gpui_kit::component::notification::Notification;
use gpui_kit::component::scroll::Scrollbar; use gpui_kit::component::scroll::Scrollbar;
use gpui_kit::component::speech::{SpeechButton, SpeechEvent, SpeechState, SpeechWaveform}; use gpui_kit::component::speech::{SpeechButton, SpeechEvent, SpeechState, SpeechWaveform};
@@ -28,7 +29,7 @@ use settings::AppSettings;
use smallvec::{SmallVec, smallvec}; use smallvec::{SmallVec, smallvec};
use speech::speaker::Speaker; use speech::speaker::Speaker;
use state::{NostrRegistry, pick_file, upload_file}; use state::{NostrRegistry, pick_file, upload_file};
use ui::{Avatar, RenderedText, RevealText, WelcomeMessage, rendered_text}; use ui::{Avatar, REACTION_EMOJIS, RenderedText, RevealText, WelcomeMessage, rendered_text};
use util::{attachment_name, display_name, opens_run, pin_label, report}; use util::{attachment_name, display_name, opens_run, pin_label, report};
mod details; mod details;
@@ -1104,7 +1105,23 @@ impl GroupPanel {
}); });
} }
fn render_composer(&self, cx: &mut Context<Self>) -> AnyElement { fn on_command(&mut self, command: &Command, window: &mut Window, cx: &mut Context<Self>) {
match command {
Command::Insert(content) => {
self.input.update(cx, |input, cx| {
let new_value = format!("{} {}", input.value(), content);
input.set_value(new_value, window, cx);
});
}
Command::Copy(public_key) => match public_key.to_bech32() {
Ok(content) => cx.write_to_clipboard(ClipboardItem::new_string(content)),
Err(error) => log::warn!("nip29: encoding the author key failed: {error}"),
},
_ => {}
}
}
fn render_composer(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
let notice = self.group.upgrade().and_then(|group| { let notice = self.group.upgrade().and_then(|group| {
let group = group.read(cx); let group = group.read(cx);
@@ -1135,14 +1152,13 @@ impl GroupPanel {
.flex_shrink_0() .flex_shrink_0()
.w_full() .w_full()
.p_2() .p_2()
.gap_1() .gap_1p5()
.when_some(self.reply_to, |this, target| { .when_some(self.reply_to, |this, target| {
this.child(self.render_reply_target(target, cx)) this.child(self.render_reply_target(target, cx))
}) })
.children(self.render_attachments(cx)) .children(self.render_attachments(cx))
.child( .child(
h_flex() h_flex()
.gap_1()
.items_end() .items_end()
.child( .child(
Button::new("upload") Button::new("upload")
@@ -1161,6 +1177,7 @@ impl GroupPanel {
.h_8() .h_8()
.flex_1() .flex_1()
.gap_2() .gap_2()
.pl_1()
.child(SpeechWaveform::new(&self.speech).small().flex_1()) .child(SpeechWaveform::new(&self.speech).small().flex_1())
.when_some(self.speech_started, |this, started| { .when_some(self.speech_started, |this, started| {
this.child(speech::elapsed(started, cx)) this.child(speech::elapsed(started, cx))
@@ -1171,28 +1188,67 @@ impl GroupPanel {
this.child( this.child(
Textarea::new(&self.input) Textarea::new(&self.input)
.appearance(false) .appearance(false)
.bg(cx.theme().input)
.small() .small()
.min_h_8() .min_h_8()
.py_1() .py_1()
.flex_1(), .flex_1(),
) )
}) })
.child(SpeechButton::new(&self.speech))
.child( .child(
Button::new("send") h_flex()
.icon(ui::IconName::PaperPlaneFill) .pl_1()
.tooltip("Send") .gap_1()
.ghost() .child(SpeechButton::new(&self.speech))
.disabled(dictating || self.uploading) .child(self.render_emoji_menu(dictating, window, cx))
.on_click(cx.listener(|this, _event, window, cx| { .child(
this.send(window, cx); Button::new("send")
})), .icon(ui::IconName::PaperPlaneFill)
.ghost()
.disabled(dictating || self.uploading)
.on_click(cx.listener(|this, _event, window, cx| {
this.send(window, cx);
})),
),
), ),
) )
.into_any_element() .into_any_element()
} }
fn render_emoji_menu(
&self,
disabled: bool,
_window: &Window,
cx: &Context<Self>,
) -> impl IntoElement {
let nostr = NostrRegistry::global(cx);
let current_user = nostr.read(cx).current_user();
let emojis = current_user
.map(|current_user| {
self.directory
.read(cx)
.person(&current_user, cx)
.emojis()
.to_vec()
})
.unwrap_or_default();
Button::new("emoji")
.icon(ui::IconName::Emoji)
.ghost()
.disabled(disabled)
.dropdown_menu_with_anchor(Anchor::BottomLeft, move |this, _window, _cx| {
let menu = REACTION_EMOJIS.iter().fold(this, |this, emoji| {
this.menu(*emoji, Box::new(Command::Insert(emoji.to_string())))
});
emojis.iter().fold(menu, |this, (shortcode, _)| {
let label = format!(":{shortcode}:");
this.menu(label.clone(), Box::new(Command::Insert(label)))
})
})
}
fn render_reply_target(&self, target: EventId, cx: &mut Context<Self>) -> AnyElement { fn render_reply_target(&self, target: EventId, cx: &mut Context<Self>) -> AnyElement {
let context = self.group.upgrade().and_then(|entity| { let context = self.group.upgrade().and_then(|entity| {
entity.read_with(cx, |group, cx| { entity.read_with(cx, |group, cx| {
@@ -1375,13 +1431,14 @@ impl Focusable for GroupPanel {
} }
impl Render for GroupPanel { impl Render for GroupPanel {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let notice = self.notice(cx); let notice = self.notice(cx);
let empty = self.item_count(cx) == 2; let empty = self.item_count(cx) == 2;
v_flex() v_flex()
.size_full() .size_full()
.min_w_0() .min_w_0()
.on_action(cx.listener(Self::on_command))
.on_action(cx.listener(Self::toggle_dictation)) .on_action(cx.listener(Self::toggle_dictation))
.child(self.render_encryption_notice(cx)) .child(self.render_encryption_notice(cx))
.child(self.render_banner(cx)) .child(self.render_banner(cx))
@@ -1422,6 +1479,6 @@ impl Render for GroupPanel {
}) })
.child(Scrollbar::vertical(&self.list_state)), .child(Scrollbar::vertical(&self.list_state)),
) )
.child(self.render_composer(cx)) .child(self.render_composer(window, cx))
} }
} }
+46 -10
View File
@@ -1,4 +1,5 @@
use chat::Message; use chat::Message;
use chat_ui::Command;
use common::TimestampExt; use common::TimestampExt;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
@@ -17,8 +18,8 @@ use settings::AppSettings;
use speech::speaker::END_GRACE; use speech::speaker::END_GRACE;
use state::NostrRegistry; use state::NostrRegistry;
use ui::{ use ui::{
Avatar, MessageActions, MessageMedia, MessageRow, REACTION_EMOJIS, ReactionBar, ReplyPreview, Avatar, COMPACT_REACTION_EMOJIS, MessageActions, MessageMedia, MessageRow, REACTION_EMOJIS,
group_reactions, react_menu, ReactionBar, ReplyPreview, group_reactions, react_menu,
}; };
use crate::GroupPanel; use crate::GroupPanel;
@@ -91,15 +92,45 @@ pub(crate) fn render(
let admin = actions.admin; let admin = actions.admin;
let pinned = actions.pinned; let pinned = actions.pinned;
let group = actions.group; let group = actions.group;
let speech_panel = actions.panel; let panel = actions.panel;
let panel = speech_panel.clone(); let speech_panel = panel.clone();
let speech_text = message.preview(); let speech_text = message.preview();
let speech_id = message.id; let speech_id = message.id;
let speaking = actions.speaking; let speaking = actions.speaking;
let author = message.author;
let row = row.overlay( let mut bar = MessageActions::new();
MessageActions::new().speech(speaking, move |_event, _window, cx| {
_ = speech_panel.update_in(cx, |panel, window, cx| { for emoji in COMPACT_REACTION_EMOJIS {
let emoji = *emoji;
let group = group.clone();
let panel = panel.clone();
bar = bar.reaction(emoji, move |_event, _window, cx| {
if let Err(error) = panel.update_in(cx, |panel, window, cx| {
panel.react(group.clone(), id, emoji.to_owned(), window, cx);
}) {
log::warn!("nip29: sending a reaction failed: {error}");
}
});
}
let reply_panel = panel.clone();
let bar = bar.reply(move |_event, _window, cx| {
if let Err(error) = reply_panel.update_in(cx, |panel, window, cx| {
panel.start_reply(id, window, cx);
}) {
log::warn!("nip29: starting a reply failed: {error}");
}
});
let bar = bar
.copy(
SharedString::from(format!("message-copy-{id}")),
message.preview(),
)
.speech(speaking, move |_event, _window, cx| {
if let Err(error) = speech_panel.update_in(cx, |panel, window, cx| {
match panel.speaker.speak(&speech_text) { match panel.speaker.speak(&speech_text) {
Ok(duration) => { Ok(duration) => {
panel.speech_reveal = Some((speech_id, duration)); panel.speech_reveal = Some((speech_id, duration));
@@ -125,9 +156,14 @@ pub(crate) fn render(
window.push_notification(Notification::error(error.to_string()), cx) window.push_notification(Notification::error(error.to_string()), cx)
} }
} }
}); }) {
}), log::warn!("nip29: reading the message aloud failed: {error}");
); }
});
let row = row.overlay(bar.dropdown(move |menu, _window, _cx| {
menu.menu("Copy author", Box::new(Command::Copy(author)))
}));
let custom = nostr let custom = nostr
.read(cx) .read(cx)