From 26d953395bcbd3acc364e8b8095eebf4d6ecdea9 Mon Sep 17 00:00:00 2001 From: Joscha Date: Tue, 2 Aug 2022 01:08:02 +0200 Subject: [PATCH] Edit messages in the tree view --- src/ui.rs | 4 +- src/ui/chat.rs | 26 +++-- src/ui/chat/tree.rs | 188 +++++++++++++++++++++++++++++++++---- src/ui/chat/tree/cursor.rs | 52 ++++++++++ src/ui/room.rs | 38 +++++--- 5 files changed, 268 insertions(+), 40 deletions(-) diff --git a/src/ui.rs b/src/ui.rs index b29448e..622821f 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -216,7 +216,9 @@ impl Ui { .await } Mode::Log => { - self.log_chat.handle_navigation(event).await; + self.log_chat + .handle_key_event(terminal, crossterm_lock, event, false) + .await; } } diff --git a/src/ui/chat.rs b/src/ui/chat.rs index 1ec0fea..90448d2 100644 --- a/src/ui/chat.rs +++ b/src/ui/chat.rs @@ -66,23 +66,35 @@ impl> ChatState { Mode::Tree => Chat::Tree(self.tree.widget(nick)), } } +} - pub async fn handle_navigation(&mut self, event: KeyEvent) -> bool { - match self.mode { - Mode::Tree => self.tree.handle_navigation(event).await, - } +pub enum Reaction { + NotHandled, + Handled, + Composed { + parent: Option, + content: String, + }, +} + +impl Reaction { + pub fn handled(&self) -> bool { + !matches!(self, Self::NotHandled) } +} - pub async fn handle_messaging( +impl> ChatState { + pub async fn handle_key_event( &mut self, terminal: &mut Terminal, crossterm_lock: &Arc>, event: KeyEvent, - ) -> Option<(Option, String)> { + can_compose: bool, + ) -> Reaction { match self.mode { Mode::Tree => { self.tree - .handle_messaging(terminal, crossterm_lock, event) + .handle_key_event(terminal, crossterm_lock, event, can_compose) .await } } diff --git a/src/ui/chat/tree.rs b/src/ui/chat/tree.rs index c0cddc4..441c4c0 100644 --- a/src/ui/chat/tree.rs +++ b/src/ui/chat/tree.rs @@ -18,7 +18,7 @@ use crate::ui::widgets::Widget; use self::cursor::Cursor; -use super::ChatMsg; +use super::{ChatMsg, Reaction}; /////////// // State // @@ -58,26 +58,181 @@ impl> InnerTreeViewState { } } - async fn handle_navigation(&mut self, event: KeyEvent) -> bool { + fn handle_editor_key_event( + &mut self, + terminal: &mut Terminal, + crossterm_lock: &Arc>, + event: KeyEvent, + coming_from: Option, + parent: Option, + ) -> Reaction { + let harmless_char = event.modifiers.difference(KeyModifiers::SHIFT).is_empty(); + match event.code { - KeyCode::Char('k') | KeyCode::Up => self.move_cursor_up().await, - KeyCode::Char('j') | KeyCode::Down => self.move_cursor_down().await, - KeyCode::Char('g') | KeyCode::Home => self.move_cursor_to_top().await, - KeyCode::Char('G') | KeyCode::End => self.move_cursor_to_bottom().await, + KeyCode::Esc => { + self.cursor = coming_from.map(Cursor::Msg).unwrap_or(Cursor::Bottom); + Reaction::Handled + } + KeyCode::Enter => { + let content = self.editor.text(); + if content.trim().is_empty() { + Reaction::Handled + } else { + self.cursor = Cursor::Pseudo { + coming_from, + parent: parent.clone(), + }; + Reaction::Composed { parent, content } + } + } + KeyCode::Backspace => { + self.editor.backspace(); + self.correction = Some(Correction::MakeCursorVisible); + Reaction::Handled + } + KeyCode::Left => { + self.editor.move_cursor_left(); + self.correction = Some(Correction::MakeCursorVisible); + Reaction::Handled + } + KeyCode::Right => { + self.editor.move_cursor_right(); + self.correction = Some(Correction::MakeCursorVisible); + Reaction::Handled + } + KeyCode::Delete => { + self.editor.delete(); + self.correction = Some(Correction::MakeCursorVisible); + Reaction::Handled + } + KeyCode::Char(ch) if harmless_char => { + self.editor.insert_char(ch); + self.correction = Some(Correction::MakeCursorVisible); + Reaction::Handled + } + KeyCode::Char('e') if event.modifiers == KeyModifiers::CONTROL => { + self.editor.edit_externally(terminal, crossterm_lock); + self.correction = Some(Correction::MakeCursorVisible); + Reaction::Handled + } + _ => Reaction::NotHandled, + } + } + + async fn handle_movement_key_event(&mut self, event: KeyEvent) -> bool { + let shift_only = event.modifiers.difference(KeyModifiers::SHIFT).is_empty(); + + match event.code { + KeyCode::Char('k') | KeyCode::Up if shift_only => self.move_cursor_up().await, + KeyCode::Char('j') | KeyCode::Down if shift_only => self.move_cursor_down().await, + KeyCode::Char('g') | KeyCode::Home if shift_only => self.move_cursor_to_top().await, + KeyCode::Char('G') | KeyCode::End if shift_only => self.move_cursor_to_bottom().await, KeyCode::Char('y') if event.modifiers == KeyModifiers::CONTROL => self.scroll_up(1), KeyCode::Char('e') if event.modifiers == KeyModifiers::CONTROL => self.scroll_down(1), _ => return false, } + true } - async fn handle_messaging( - &self, + async fn handle_edit_initiating_key_event( + &mut self, + event: KeyEvent, + id: Option, + ) -> bool { + let shift_only = event.modifiers.difference(KeyModifiers::SHIFT).is_empty(); + if !shift_only { + return false; + } + + match event.code { + KeyCode::Char('r') => { + if let Some(parent) = self.parent_for_normal_reply().await { + self.cursor = Cursor::Editor { + coming_from: id, + parent, + }; + } + } + KeyCode::Char('R') => { + if let Some(parent) = self.parent_for_alternate_reply().await { + self.cursor = Cursor::Editor { + coming_from: id, + parent, + }; + } + } + KeyCode::Char('t' | 'T') => { + self.cursor = Cursor::Editor { + coming_from: id, + parent: None, + }; + } + _ => return false, + } + + true + } + + async fn handle_normal_key_event( + &mut self, + event: KeyEvent, + can_compose: bool, + id: Option, + ) -> bool { + if self.handle_movement_key_event(event).await { + true + } else if can_compose { + self.handle_edit_initiating_key_event(event, id).await + } else { + false + } + } + + async fn handle_key_event( + &mut self, terminal: &mut Terminal, crossterm_lock: &Arc>, event: KeyEvent, - ) -> Option<(Option, String)> { - None + can_compose: bool, + ) -> Reaction { + match &self.cursor { + Cursor::Bottom => { + if self.handle_normal_key_event(event, can_compose, None).await { + Reaction::Handled + } else { + Reaction::NotHandled + } + } + Cursor::Msg(id) => { + let id = id.clone(); + if self + .handle_normal_key_event(event, can_compose, Some(id)) + .await + { + Reaction::Handled + } else { + Reaction::NotHandled + } + } + Cursor::Editor { + coming_from, + parent, + } => self.handle_editor_key_event( + terminal, + crossterm_lock, + event, + coming_from.clone(), + parent.clone(), + ), + Cursor::Pseudo { .. } => { + if self.handle_movement_key_event(event).await { + Reaction::Handled + } else { + Reaction::NotHandled + } + } + } } } @@ -95,20 +250,17 @@ impl> TreeViewState { } } - pub async fn handle_navigation(&mut self, event: KeyEvent) -> bool { - self.0.lock().await.handle_navigation(event).await - } - - pub async fn handle_messaging( - &self, + pub async fn handle_key_event( + &mut self, terminal: &mut Terminal, crossterm_lock: &Arc>, event: KeyEvent, - ) -> Option<(Option, String)> { + can_compose: bool, + ) -> Reaction { self.0 .lock() .await - .handle_messaging(terminal, crossterm_lock, event) + .handle_key_event(terminal, crossterm_lock, event, can_compose) .await } } diff --git a/src/ui/chat/tree/cursor.rs b/src/ui/chat/tree/cursor.rs index 0febfca..1a1003c 100644 --- a/src/ui/chat/tree/cursor.rs +++ b/src/ui/chat/tree/cursor.rs @@ -233,6 +233,58 @@ impl> InnerTreeViewState { self.scroll -= amount; self.correction = Some(Correction::MoveCursorToVisibleArea); } + + pub async fn parent_for_normal_reply(&self) -> Option> { + match &self.cursor { + Cursor::Bottom => Some(None), + Cursor::Msg(id) => { + let path = self.store.path(id).await; + let tree = self.store.tree(path.first()).await; + + Some(Some(if tree.next_sibling(id).is_some() { + // A reply to a message that has further siblings should be a + // direct reply. An indirect reply might end up a lot further + // down in the current conversation. + id.clone() + } else if let Some(parent) = tree.parent(id) { + // A reply to a message without younger siblings should be + // an indirect reply so as not to create unnecessarily deep + // threads. In the case that our message has children, this + // might get a bit confusing. I'm not sure yet how well this + // "smart" reply actually works in practice. + parent + } else { + // When replying to a top-level message, it makes sense to avoid + // creating unnecessary new threads. + id.clone() + })) + } + _ => None, + } + } + + pub async fn parent_for_alternate_reply(&self) -> Option> { + match &self.cursor { + Cursor::Bottom => Some(None), + Cursor::Msg(id) => { + let path = self.store.path(id).await; + let tree = self.store.tree(path.first()).await; + + Some(Some(if tree.next_sibling(id).is_none() { + // The opposite of replying normally + id.clone() + } else if let Some(parent) = tree.parent(id) { + // The opposite of replying normally + parent + } else { + // The same as replying normally, still to avoid creating + // unnecessary new threads + id.clone() + })) + } + _ => None, + } + } } /* diff --git a/src/ui/room.rs b/src/ui/room.rs index b9b1f18..b38db8a 100644 --- a/src/ui/room.rs +++ b/src/ui/room.rs @@ -12,7 +12,7 @@ use crate::euph::api::{SessionType, SessionView}; use crate::euph::{self, Joined, Status}; use crate::vault::EuphVault; -use super::chat::ChatState; +use super::chat::{ChatState, Reaction}; use super::widgets::background::Background; use super::widgets::border::Border; use super::widgets::editor::EditorState; @@ -284,12 +284,26 @@ impl EuphRoom { ) -> bool { match &self.state { State::Normal => { - if self.chat.handle_navigation(event).await { - return true; - } - + // TODO Use if-let chain if let Some(room) = &self.room { if let Ok(Some(Status::Joined(joined))) = room.status().await { + match self + .chat + .handle_key_event(terminal, crossterm_lock, event, true) + .await + { + Reaction::NotHandled => {} + Reaction::Handled => return true, + Reaction::Composed { parent, content } => { + let _ = room.send(parent, content); + return true; + } + } + + if !event.modifiers.is_empty() { + return false; + } + if let KeyCode::Char('n' | 'N') = event.code { self.state = State::ChooseNick(EditorState::with_initial_text( joined.session.name.clone(), @@ -297,18 +311,14 @@ impl EuphRoom { return true; } - let potential_message = self - .chat - .handle_messaging(terminal, crossterm_lock, event) - .await; - if let Some((parent, content)) = potential_message { - let _ = room.send(parent, content); - return true; - } + return false; } } - false + self.chat + .handle_key_event(terminal, crossterm_lock, event, false) + .await + .handled() } State::ChooseNick(ed) => { match event.code {