notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

commit 2ecb26ef23de2826d6940d530d94dad5d5d6d80d
parent 015d502b981d93330d93f39c3f709b2a451ebcf8
Author: William Casarin <jb55@jb55.com>
Date:   Fri, 30 Jan 2026 13:34:57 -0800

composer: implement multiline input with Signal-style keybindings

Enter sends the message, Shift+Enter inserts a newline. The composer
height grows dynamically based on the number of lines (capped at 8).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Diffstat:
Mcrates/notedeck_messages/src/ui/convo.rs | 39+++++++++++++++++++++------------------
1 file changed, 21 insertions(+), 18 deletions(-)

diff --git a/crates/notedeck_messages/src/ui/convo.rs b/crates/notedeck_messages/src/ui/convo.rs @@ -1,6 +1,7 @@ use chrono::{DateTime, Duration, Local, NaiveDate}; use egui::{ - vec2, Align, Color32, CornerRadius, Frame, Key, Layout, Margin, RichText, ScrollArea, TextEdit, + vec2, Align, Color32, CornerRadius, Frame, Key, KeyboardShortcut, Layout, Margin, Modifiers, + RichText, ScrollArea, TextEdit, }; use egui_extras::{Size, StripBuilder}; use enostr::Pubkey; @@ -54,7 +55,12 @@ impl<'a> ConversationUi<'a> { let mut action = None; Frame::new().fill(ui.visuals().panel_fill).show(ui, |ui| { ui.with_layout(Layout::bottom_up(Align::Min), |ui| { - ui.allocate_ui(vec2(ui.available_width(), 64.0), |ui| { + // Calculate height based on number of lines (min 1, max 8) + let line_count = self.state.composer.lines().count().max(1).min(8); + let line_height = 20.0; // approximate line height + let base_height = 44.0; // padding + margin + let composer_height = base_height + (line_count as f32 * line_height); + ui.allocate_ui(vec2(ui.available_width(), composer_height), |ui| { let comp_resp = conversation_composer(ui, self.state, self.conversation.id, self.i18n); if action.is_none() { @@ -282,9 +288,6 @@ fn conversation_composer( let mut composer_has_focus = false; Frame::new().inner_margin(margin).show(ui, |ui| { ui.with_layout(Layout::left_to_right(Align::Center), |ui| { - // TODO(kernelkind): ideally this will be multiline, but the default multiline impl doesn't work the way - // signal's multiline works... TBC - let old = mut_visuals_corner_radius(ui, CornerRadius::same(16)); let hint_text = RichText::new(tr!( @@ -303,19 +306,24 @@ fn conversation_composer( .horizontal(|mut strip| { strip.cell(|ui| { let spacing = ui.spacing().item_spacing.x; - let text_height = ui.spacing().item_spacing.y * 1.4; let text_width = (ui.available_width() - spacing).max(0.0); - let size = vec2(text_width, text_height); - let text_edit = TextEdit::singleline(&mut state.composer) + let text_edit = TextEdit::multiline(&mut state.composer) .margin(Margin::symmetric(16, 8)) - .vertical_align(Align::Center) .desired_width(text_width) .hint_text(hint_text) - .min_size(size); + .desired_rows(1) + .return_key(KeyboardShortcut::new( + Modifiers { + shift: true, + ..Default::default() + }, + Key::Enter, + )); let text_resp = ui.add(text_edit); restore_widgets_corner_rad(ui, old); - send = text_resp.lost_focus() && ui.input(|i| i.key_pressed(Key::Enter)); + send = text_resp.has_focus() + && ui.input(|i| i.key_pressed(Key::Enter) && !i.modifiers.shift); include_input(ui, &text_resp); composer_has_focus = text_resp.has_focus(); }); @@ -423,10 +431,7 @@ fn self_chat_bubble( ui.with_layout(Layout::right_to_left(Align::Min), |ui| { chat_bubble(ui, msg_type, true, bubble_fill, |ui| { ui.with_layout(Layout::top_down(Align::Max), |ui| { - ui.add( - egui::Label::new(RichText::new(message).color(ui.visuals().text_color())) - .selectable(true), - ); + ui.label(RichText::new(message).color(ui.visuals().text_color())); if msg_type == MessageType::Standalone || msg_type == MessageType::LastInSeries { let timestamp_label = @@ -469,9 +474,7 @@ fn other_chat_bubble( ui.with_layout( Layout::left_to_right(Align::Max).with_main_wrap(true), |ui| { - ui.add( - egui::Label::new(RichText::new(message).color(text_color)).selectable(true), - ); + ui.label(RichText::new(message).color(text_color)); if msg_type == MessageType::Standalone || msg_type == MessageType::LastInSeries { ui.add_space(6.0);