diff --git a/src/ui/chat.rs b/src/ui/chat.rs index d4736de..5caaa04 100644 --- a/src/ui/chat.rs +++ b/src/ui/chat.rs @@ -106,6 +106,12 @@ impl> ChatState { } } + pub async fn cursor(&self) -> Option { + match self.mode { + Mode::Tree => self.tree.cursor().await, + } + } + /// A [`Reaction::Composed`] message was sent, either successfully or /// unsuccessfully. /// diff --git a/src/ui/chat/tree.rs b/src/ui/chat/tree.rs index e054855..fe52b26 100644 --- a/src/ui/chat/tree.rs +++ b/src/ui/chat/tree.rs @@ -337,6 +337,13 @@ impl> InnerTreeViewState { } } + fn cursor(&self) -> Option { + match &self.cursor { + Cursor::Msg(id) => Some(id.clone()), + Cursor::Bottom | Cursor::Editor { .. } | Cursor::Pseudo { .. } => None, + } + } + fn sent(&mut self, id: Option) { if let Cursor::Pseudo { coming_from, .. } = &self.cursor { if let Some(id) = id { @@ -385,6 +392,10 @@ impl> TreeViewState { .await } + pub async fn cursor(&self) -> Option { + self.0.lock().await.cursor() + } + pub async fn sent(&mut self, id: Option) { self.0.lock().await.sent(id) } diff --git a/src/ui/euph.rs b/src/ui/euph.rs index b18bd8b..40c2778 100644 --- a/src/ui/euph.rs +++ b/src/ui/euph.rs @@ -1,5 +1,6 @@ mod account; mod auth; +mod links; mod nick; mod nick_list; mod popup; diff --git a/src/ui/euph/links.rs b/src/ui/euph/links.rs new file mode 100644 index 0000000..0b71681 --- /dev/null +++ b/src/ui/euph/links.rs @@ -0,0 +1,69 @@ +use std::io; +use std::sync::Arc; + +use crossterm::event::KeyCode; +use crossterm::style::{ContentStyle, Stylize}; +use parking_lot::FairMutex; +use toss::terminal::Terminal; + +use crate::euph::Room; +use crate::ui::input::{key, InputEvent, KeyBindingsList, KeyEvent}; +use crate::ui::widgets::list::ListState; +use crate::ui::widgets::popup::Popup; +use crate::ui::widgets::text::Text; +use crate::ui::widgets::BoxedWidget; + +pub struct LinksState { + links: Vec, + list: ListState, +} + +pub enum EventResult { + NotHandled, + Handled, + Close, + ErrorOpeningLink { link: String, error: io::Error }, +} + +impl LinksState { + pub fn new(content: &str) -> Self { + // TODO Extract links + Self { + links: vec![ + "https://example.com/".to_string(), + "https://plugh.de/".to_string(), + ], + list: ListState::new(), + } + } + + pub fn widget(&self) -> BoxedWidget { + let mut list = self.list.widget(); + for (id, link) in self.links.iter().enumerate() { + list.add_sel( + id, + Text::new((link,)), + Text::new((link, ContentStyle::default().black().on_white())), + ); + } + + Popup::new(list).title("Links").build() + } + + pub fn list_key_bindings(&self, bindings: &mut KeyBindingsList) { + bindings.binding("esc", "close links popup") + } + + pub fn handle_input_event( + &mut self, + terminal: &mut Terminal, + crossterm_lock: &Arc>, + event: &InputEvent, + room: &Option, + ) -> EventResult { + match event { + key!(Esc) => EventResult::Close, + _ => EventResult::NotHandled, + } + } +} diff --git a/src/ui/euph/popup.rs b/src/ui/euph/popup.rs index 878177a..8bc8c6c 100644 --- a/src/ui/euph/popup.rs +++ b/src/ui/euph/popup.rs @@ -7,7 +7,7 @@ use crate::ui::widgets::text::Text; use crate::ui::widgets::BoxedWidget; pub enum RoomPopup { - ServerError { description: String, reason: String }, + Error { description: String, reason: String }, } impl RoomPopup { @@ -26,7 +26,7 @@ impl RoomPopup { pub fn widget(&self) -> BoxedWidget { let widget = match self { - Self::ServerError { + Self::Error { description, reason, } => Self::server_error_widget(description, reason), diff --git a/src/ui/euph/room.rs b/src/ui/euph/room.rs index 3825690..9b780ed 100644 --- a/src/ui/euph/room.rs +++ b/src/ui/euph/room.rs @@ -29,6 +29,7 @@ use crate::ui::UiEvent; use crate::vault::EuphVault; use super::account::{self, AccountUiState}; +use super::links::{self, LinksState}; use super::popup::RoomPopup; use super::{auth, nick, nick_list}; @@ -37,6 +38,7 @@ enum State { Auth(EditorState), Nick(EditorState), Account(AccountUiState), + Links(LinksState), } #[allow(clippy::large_enum_variant)] @@ -222,6 +224,7 @@ impl EuphRoom { State::Auth(editor) => layers.push(auth::widget(editor)), State::Nick(editor) => layers.push(nick::widget(editor)), State::Account(account) => layers.push(account.widget()), + State::Links(links) => layers.push(links.widget()), } for popup in &self.popups { @@ -316,6 +319,8 @@ impl EuphRoom { false }; + bindings.binding("I", "show message links"); + bindings.empty(); self.chat.list_key_bindings(bindings, can_compose).await; } @@ -326,6 +331,15 @@ impl EuphRoom { crossterm_lock: &Arc>, event: &InputEvent, ) -> bool { + if let key!('I') = event { + if let Some(id) = self.chat.cursor().await { + if let Some(msg) = self.vault.msg(&id).await { + self.state = State::Links(LinksState::new(&msg.content)); + } + } + return true; + } + if let Some(room) = &self.room { let status = room.status().await; let can_compose = matches!(status, Ok(Some(Status::Joined(_)))); @@ -395,6 +409,7 @@ impl EuphRoom { State::Auth(_) => auth::list_key_bindings(bindings), State::Nick(_) => nick::list_key_bindings(bindings), State::Account(account) => account.list_key_bindings(bindings), + State::Links(links) => links.list_key_bindings(bindings), } } @@ -449,6 +464,23 @@ impl EuphRoom { } } } + State::Links(links) => { + match links.handle_input_event(terminal, crossterm_lock, event, &self.room) { + links::EventResult::NotHandled => false, + links::EventResult::Handled => true, + links::EventResult::Close => { + self.state = State::Normal; + true + } + links::EventResult::ErrorOpeningLink { link, error } => { + self.popups.push_front(RoomPopup::Error { + description: format!("Failed to open link: {link}"), + reason: format!("{error}"), + }); + true + } + } + } } } @@ -489,7 +521,7 @@ impl EuphRoom { if let Some((action, reason)) = error { let description = format!("Failed to {action}."); let reason = reason.unwrap_or_else(|| "no idea, the server wouldn't say".to_string()); - self.popups.push_front(RoomPopup::ServerError { + self.popups.push_front(RoomPopup::Error { description, reason, }); @@ -522,7 +554,7 @@ impl EuphRoom { _ => return false, }; let description = format!("Failed to {action}."); - self.popups.push_front(RoomPopup::ServerError { + self.popups.push_front(RoomPopup::Error { description, reason, });