Edit messages in the tree view
This commit is contained in:
parent
fe0f6c7520
commit
26d953395b
5 changed files with 268 additions and 40 deletions
|
|
@ -216,7 +216,9 @@ impl Ui {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
Mode::Log => {
|
Mode::Log => {
|
||||||
self.log_chat.handle_navigation(event).await;
|
self.log_chat
|
||||||
|
.handle_key_event(terminal, crossterm_lock, event, false)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -66,23 +66,35 @@ impl<M: Msg, S: MsgStore<M>> ChatState<M, S> {
|
||||||
Mode::Tree => Chat::Tree(self.tree.widget(nick)),
|
Mode::Tree => Chat::Tree(self.tree.widget(nick)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn handle_navigation(&mut self, event: KeyEvent) -> bool {
|
pub enum Reaction<M: Msg> {
|
||||||
match self.mode {
|
NotHandled,
|
||||||
Mode::Tree => self.tree.handle_navigation(event).await,
|
Handled,
|
||||||
}
|
Composed {
|
||||||
|
parent: Option<M::Id>,
|
||||||
|
content: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<M: Msg> Reaction<M> {
|
||||||
|
pub fn handled(&self) -> bool {
|
||||||
|
!matches!(self, Self::NotHandled)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn handle_messaging(
|
impl<M: Msg, S: MsgStore<M>> ChatState<M, S> {
|
||||||
|
pub async fn handle_key_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
terminal: &mut Terminal,
|
terminal: &mut Terminal,
|
||||||
crossterm_lock: &Arc<FairMutex<()>>,
|
crossterm_lock: &Arc<FairMutex<()>>,
|
||||||
event: KeyEvent,
|
event: KeyEvent,
|
||||||
) -> Option<(Option<M::Id>, String)> {
|
can_compose: bool,
|
||||||
|
) -> Reaction<M> {
|
||||||
match self.mode {
|
match self.mode {
|
||||||
Mode::Tree => {
|
Mode::Tree => {
|
||||||
self.tree
|
self.tree
|
||||||
.handle_messaging(terminal, crossterm_lock, event)
|
.handle_key_event(terminal, crossterm_lock, event, can_compose)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ use crate::ui::widgets::Widget;
|
||||||
|
|
||||||
use self::cursor::Cursor;
|
use self::cursor::Cursor;
|
||||||
|
|
||||||
use super::ChatMsg;
|
use super::{ChatMsg, Reaction};
|
||||||
|
|
||||||
///////////
|
///////////
|
||||||
// State //
|
// State //
|
||||||
|
|
@ -58,26 +58,181 @@ impl<M: Msg, S: MsgStore<M>> InnerTreeViewState<M, S> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_navigation(&mut self, event: KeyEvent) -> bool {
|
fn handle_editor_key_event(
|
||||||
|
&mut self,
|
||||||
|
terminal: &mut Terminal,
|
||||||
|
crossterm_lock: &Arc<FairMutex<()>>,
|
||||||
|
event: KeyEvent,
|
||||||
|
coming_from: Option<M::Id>,
|
||||||
|
parent: Option<M::Id>,
|
||||||
|
) -> Reaction<M> {
|
||||||
|
let harmless_char = event.modifiers.difference(KeyModifiers::SHIFT).is_empty();
|
||||||
|
|
||||||
match event.code {
|
match event.code {
|
||||||
KeyCode::Char('k') | KeyCode::Up => self.move_cursor_up().await,
|
KeyCode::Esc => {
|
||||||
KeyCode::Char('j') | KeyCode::Down => self.move_cursor_down().await,
|
self.cursor = coming_from.map(Cursor::Msg).unwrap_or(Cursor::Bottom);
|
||||||
KeyCode::Char('g') | KeyCode::Home => self.move_cursor_to_top().await,
|
Reaction::Handled
|
||||||
KeyCode::Char('G') | KeyCode::End => self.move_cursor_to_bottom().await,
|
}
|
||||||
|
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('y') if event.modifiers == KeyModifiers::CONTROL => self.scroll_up(1),
|
||||||
KeyCode::Char('e') if event.modifiers == KeyModifiers::CONTROL => self.scroll_down(1),
|
KeyCode::Char('e') if event.modifiers == KeyModifiers::CONTROL => self.scroll_down(1),
|
||||||
_ => return false,
|
_ => return false,
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_messaging(
|
async fn handle_edit_initiating_key_event(
|
||||||
&self,
|
&mut self,
|
||||||
|
event: KeyEvent,
|
||||||
|
id: Option<M::Id>,
|
||||||
|
) -> 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<M::Id>,
|
||||||
|
) -> 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,
|
terminal: &mut Terminal,
|
||||||
crossterm_lock: &Arc<FairMutex<()>>,
|
crossterm_lock: &Arc<FairMutex<()>>,
|
||||||
event: KeyEvent,
|
event: KeyEvent,
|
||||||
) -> Option<(Option<M::Id>, String)> {
|
can_compose: bool,
|
||||||
None
|
) -> Reaction<M> {
|
||||||
|
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<M: Msg, S: MsgStore<M>> TreeViewState<M, S> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_navigation(&mut self, event: KeyEvent) -> bool {
|
pub async fn handle_key_event(
|
||||||
self.0.lock().await.handle_navigation(event).await
|
&mut self,
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn handle_messaging(
|
|
||||||
&self,
|
|
||||||
terminal: &mut Terminal,
|
terminal: &mut Terminal,
|
||||||
crossterm_lock: &Arc<FairMutex<()>>,
|
crossterm_lock: &Arc<FairMutex<()>>,
|
||||||
event: KeyEvent,
|
event: KeyEvent,
|
||||||
) -> Option<(Option<M::Id>, String)> {
|
can_compose: bool,
|
||||||
|
) -> Reaction<M> {
|
||||||
self.0
|
self.0
|
||||||
.lock()
|
.lock()
|
||||||
.await
|
.await
|
||||||
.handle_messaging(terminal, crossterm_lock, event)
|
.handle_key_event(terminal, crossterm_lock, event, can_compose)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -233,6 +233,58 @@ impl<M: Msg, S: MsgStore<M>> InnerTreeViewState<M, S> {
|
||||||
self.scroll -= amount;
|
self.scroll -= amount;
|
||||||
self.correction = Some(Correction::MoveCursorToVisibleArea);
|
self.correction = Some(Correction::MoveCursorToVisibleArea);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn parent_for_normal_reply(&self) -> Option<Option<M::Id>> {
|
||||||
|
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<Option<M::Id>> {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ use crate::euph::api::{SessionType, SessionView};
|
||||||
use crate::euph::{self, Joined, Status};
|
use crate::euph::{self, Joined, Status};
|
||||||
use crate::vault::EuphVault;
|
use crate::vault::EuphVault;
|
||||||
|
|
||||||
use super::chat::ChatState;
|
use super::chat::{ChatState, Reaction};
|
||||||
use super::widgets::background::Background;
|
use super::widgets::background::Background;
|
||||||
use super::widgets::border::Border;
|
use super::widgets::border::Border;
|
||||||
use super::widgets::editor::EditorState;
|
use super::widgets::editor::EditorState;
|
||||||
|
|
@ -284,12 +284,26 @@ impl EuphRoom {
|
||||||
) -> bool {
|
) -> bool {
|
||||||
match &self.state {
|
match &self.state {
|
||||||
State::Normal => {
|
State::Normal => {
|
||||||
if self.chat.handle_navigation(event).await {
|
// TODO Use if-let chain
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(room) = &self.room {
|
if let Some(room) = &self.room {
|
||||||
if let Ok(Some(Status::Joined(joined))) = room.status().await {
|
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 {
|
if let KeyCode::Char('n' | 'N') = event.code {
|
||||||
self.state = State::ChooseNick(EditorState::with_initial_text(
|
self.state = State::ChooseNick(EditorState::with_initial_text(
|
||||||
joined.session.name.clone(),
|
joined.session.name.clone(),
|
||||||
|
|
@ -297,18 +311,14 @@ impl EuphRoom {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
let potential_message = self
|
return false;
|
||||||
.chat
|
|
||||||
.handle_messaging(terminal, crossterm_lock, event)
|
|
||||||
.await;
|
|
||||||
if let Some((parent, content)) = potential_message {
|
|
||||||
let _ = room.send(parent, content);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
false
|
self.chat
|
||||||
|
.handle_key_event(terminal, crossterm_lock, event, false)
|
||||||
|
.await
|
||||||
|
.handled()
|
||||||
}
|
}
|
||||||
State::ChooseNick(ed) => {
|
State::ChooseNick(ed) => {
|
||||||
match event.code {
|
match event.code {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue