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);
})),
)
.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| {
this.child(
h_flex()
@@ -2082,17 +2070,25 @@ impl Render for ChatPanel {
.flex_1(),
)
})
.child(
h_flex()
.items_end()
.gap_1()
.pl_1()
.child(SpeechButton::new(&self.speech)),
)
.child(
h_flex()
.pl_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_config_menu(dictating, window, cx))
.child(
+78 -21
View File
@@ -2,13 +2,13 @@ use std::collections::BTreeMap;
use std::time::Duration;
use anyhow::Result;
use chat_ui::ToggleDictation;
use chat_ui::{Command, ToggleDictation};
use gpui::prelude::FluentBuilder;
use gpui::{
Action, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
FollowMode, InteractiveElement, IntoElement, ListAlignment, ListScrollEvent, ListState,
ParentElement, Render, SharedString, Styled, Subscription, Task, WeakEntity, Window, div, list,
px,
Action, Anchor, AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter,
FocusHandle, Focusable, FollowMode, InteractiveElement, IntoElement, ListAlignment,
ListScrollEvent, ListState, ParentElement, Render, SharedString, Styled, Subscription, Task,
WeakEntity, Window, div, list, px,
};
use gpui_kit::assets::IconName;
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::dock::{Panel, PanelEvent};
use gpui_kit::component::input::{InputEvent, Textarea, TextareaState};
use gpui_kit::component::menu::DropdownMenu;
use gpui_kit::component::notification::Notification;
use gpui_kit::component::scroll::Scrollbar;
use gpui_kit::component::speech::{SpeechButton, SpeechEvent, SpeechState, SpeechWaveform};
@@ -28,7 +29,7 @@ use settings::AppSettings;
use smallvec::{SmallVec, smallvec};
use speech::speaker::Speaker;
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};
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 group = group.read(cx);
@@ -1135,14 +1152,13 @@ impl GroupPanel {
.flex_shrink_0()
.w_full()
.p_2()
.gap_1()
.gap_1p5()
.when_some(self.reply_to, |this, target| {
this.child(self.render_reply_target(target, cx))
})
.children(self.render_attachments(cx))
.child(
h_flex()
.gap_1()
.items_end()
.child(
Button::new("upload")
@@ -1161,6 +1177,7 @@ impl GroupPanel {
.h_8()
.flex_1()
.gap_2()
.pl_1()
.child(SpeechWaveform::new(&self.speech).small().flex_1())
.when_some(self.speech_started, |this, started| {
this.child(speech::elapsed(started, cx))
@@ -1171,28 +1188,67 @@ impl GroupPanel {
this.child(
Textarea::new(&self.input)
.appearance(false)
.bg(cx.theme().input)
.small()
.min_h_8()
.py_1()
.flex_1(),
)
})
.child(SpeechButton::new(&self.speech))
.child(
Button::new("send")
.icon(ui::IconName::PaperPlaneFill)
.tooltip("Send")
.ghost()
.disabled(dictating || self.uploading)
.on_click(cx.listener(|this, _event, window, cx| {
this.send(window, cx);
})),
h_flex()
.pl_1()
.gap_1()
.child(SpeechButton::new(&self.speech))
.child(self.render_emoji_menu(dictating, window, cx))
.child(
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()
}
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 {
let context = self.group.upgrade().and_then(|entity| {
entity.read_with(cx, |group, cx| {
@@ -1375,13 +1431,14 @@ impl Focusable 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 empty = self.item_count(cx) == 2;
v_flex()
.size_full()
.min_w_0()
.on_action(cx.listener(Self::on_command))
.on_action(cx.listener(Self::toggle_dictation))
.child(self.render_encryption_notice(cx))
.child(self.render_banner(cx))
@@ -1422,6 +1479,6 @@ impl Render for GroupPanel {
})
.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_ui::Command;
use common::TimestampExt;
use gpui::prelude::FluentBuilder;
use gpui::{
@@ -17,8 +18,8 @@ use settings::AppSettings;
use speech::speaker::END_GRACE;
use state::NostrRegistry;
use ui::{
Avatar, MessageActions, MessageMedia, MessageRow, REACTION_EMOJIS, ReactionBar, ReplyPreview,
group_reactions, react_menu,
Avatar, COMPACT_REACTION_EMOJIS, MessageActions, MessageMedia, MessageRow, REACTION_EMOJIS,
ReactionBar, ReplyPreview, group_reactions, react_menu,
};
use crate::GroupPanel;
@@ -91,15 +92,45 @@ pub(crate) fn render(
let admin = actions.admin;
let pinned = actions.pinned;
let group = actions.group;
let speech_panel = actions.panel;
let panel = speech_panel.clone();
let panel = actions.panel;
let speech_panel = panel.clone();
let speech_text = message.preview();
let speech_id = message.id;
let speaking = actions.speaking;
let author = message.author;
let row = row.overlay(
MessageActions::new().speech(speaking, move |_event, _window, cx| {
_ = speech_panel.update_in(cx, |panel, window, cx| {
let mut bar = MessageActions::new();
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) {
Ok(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)
}
}
});
}),
);
}) {
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
.read(cx)