diff --git a/src/chat.rs b/src/chat.rs index d1a0a36..b695cb4 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -52,6 +52,10 @@ impl> Chat { tree: TreeView::new(), } } + + pub fn store(&self) -> &S { + &self.store + } } impl> Chat { diff --git a/src/euph.rs b/src/euph.rs index 105916d..b2336a2 100644 --- a/src/euph.rs +++ b/src/euph.rs @@ -2,4 +2,5 @@ pub mod api; mod conn; mod room; +pub use conn::{Joined, Joining, Status}; pub use room::Room; diff --git a/src/euph/conn.rs b/src/euph/conn.rs index aefd419..6daf407 100644 --- a/src/euph/conn.rs +++ b/src/euph/conn.rs @@ -63,9 +63,9 @@ impl Event { #[derive(Debug, Clone, Default)] pub struct Joining { - hello: Option, - snapshot: Option, - bounce: Option, + pub hello: Option, + pub snapshot: Option, + pub bounce: Option, } impl Joining { @@ -107,9 +107,9 @@ impl Joining { #[derive(Debug, Clone)] pub struct Joined { - session: SessionView, - account: Option, - listing: HashMap, + pub session: SessionView, + pub account: Option, + pub listing: HashMap, } impl Joined { diff --git a/src/euph/room.rs b/src/euph/room.rs index 30938ef..418851a 100644 --- a/src/euph/room.rs +++ b/src/euph/room.rs @@ -13,7 +13,7 @@ use tokio_tungstenite::tungstenite; use crate::ui::UiEvent; use crate::vault::EuphVault; -use super::api::{Data, Log, Snowflake}; +use super::api::{Data, Log, Nick, Send, Snowflake}; use super::conn::{self, ConnRx, ConnTx, Status}; #[derive(Debug, thiserror::Error)] @@ -29,6 +29,8 @@ enum Event { Data(Data), Status(oneshot::Sender>), RequestLogs, + Nick(String), + Send(Option, String), } #[derive(Debug)] @@ -120,6 +122,8 @@ impl State { Event::Data(data) => self.on_data(data).await?, Event::Status(reply_tx) => self.on_status(reply_tx).await, Event::RequestLogs => self.on_request_logs(), + Event::Nick(name) => self.on_nick(name), + Event::Send(parent, content) => self.on_send(parent, content), } } Ok(()) @@ -164,7 +168,6 @@ impl State { let id = d.0.id; self.vault.add_message(d.0, *last_msg_id); *last_msg_id = Some(id); - let _ = self.ui_event_tx.send(UiEvent::Redraw); } else { bail!("send event before snapshot event"); } @@ -174,24 +177,22 @@ impl State { self.vault.join(Utc::now()); self.last_msg_id = Some(d.log.last().map(|m| m.id)); self.vault.add_messages(d.log, None); - let _ = self.ui_event_tx.send(UiEvent::Redraw); } Data::LogReply(d) => { self.vault.add_messages(d.log, d.before); - let _ = self.ui_event_tx.send(UiEvent::Redraw); } Data::SendReply(d) => { if let Some(last_msg_id) = &mut self.last_msg_id { let id = d.0.id; self.vault.add_message(d.0, *last_msg_id); *last_msg_id = Some(id); - let _ = self.ui_event_tx.send(UiEvent::Redraw); } else { bail!("send reply before snapshot event"); } } _ => {} } + let _ = self.ui_event_tx.send(UiEvent::Redraw); Ok(()) } @@ -242,6 +243,24 @@ impl State { Ok(()) } + + fn on_nick(&self, name: String) { + if let Some(conn_tx) = &self.conn_tx { + let conn_tx = conn_tx.clone(); + task::spawn(async move { + let _ = conn_tx.send(Nick { name }).await; + }); + } + } + + fn on_send(&self, parent: Option, content: String) { + if let Some(conn_tx) = &self.conn_tx { + let conn_tx = conn_tx.clone(); + task::spawn(async move { + let _ = conn_tx.send(Send { content, parent }).await; + }); + } + } } #[derive(Debug)] @@ -252,16 +271,12 @@ pub struct Room { } impl Room { - pub fn new( - name: String, - vault: EuphVault, - ui_event_tx: mpsc::UnboundedSender, - ) -> Self { + pub fn new(vault: EuphVault, ui_event_tx: mpsc::UnboundedSender) -> Self { let (canary_tx, canary_rx) = oneshot::channel(); let (event_tx, event_rx) = mpsc::unbounded_channel(); let state = State { - name, + name: vault.room().to_string(), vault, ui_event_tx, conn_tx: None, @@ -294,4 +309,16 @@ impl Room { .send(Event::RequestLogs) .map_err(|_| Error::Stopped) } + + pub fn nick(&self, name: String) -> Result<(), Error> { + self.event_tx + .send(Event::Nick(name)) + .map_err(|_| Error::Stopped) + } + + pub fn send(&self, parent: Option, content: String) -> Result<(), Error> { + self.event_tx + .send(Event::Send(parent, content)) + .map_err(|_| Error::Stopped) + } } diff --git a/src/ui.rs b/src/ui.rs index 7f7e7d9..2ed32c2 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,3 +1,4 @@ +mod room; mod rooms; mod util; @@ -75,7 +76,7 @@ impl Ui { let mut ui = Self { event_tx: event_tx.clone(), mode: Mode::Main, - rooms: Rooms::new(vault), + rooms: Rooms::new(vault, event_tx.clone()), log_chat: Chat::new(logger), }; tokio::select! { @@ -195,7 +196,7 @@ impl Ui { match self.mode { Mode::Main => { self.rooms - .handle_key_event(terminal, size, &self.event_tx, crossterm_lock, event) + .handle_key_event(terminal, size, crossterm_lock, event) .await } Mode::Log => self.log_chat.handle_navigation(terminal, size, event).await, diff --git a/src/ui/room.rs b/src/ui/room.rs new file mode 100644 index 0000000..d41a008 --- /dev/null +++ b/src/ui/room.rs @@ -0,0 +1,135 @@ +use std::sync::Arc; + +use crossterm::event::{KeyCode, KeyEvent}; +use crossterm::style::ContentStyle; +use parking_lot::FairMutex; +use tokio::sync::mpsc; +use toss::frame::{Frame, Pos, Size}; +use toss::terminal::Terminal; + +use crate::chat::Chat; +use crate::euph::{self, Status}; +use crate::vault::{EuphMsg, EuphVault}; + +use super::{util, UiEvent}; + +pub struct EuphRoom { + ui_event_tx: mpsc::UnboundedSender, + room: Option, + chat: Chat, +} + +impl EuphRoom { + pub fn new(vault: EuphVault, ui_event_tx: mpsc::UnboundedSender) -> Self { + Self { + ui_event_tx, + room: None, + chat: Chat::new(vault), + } + } + + pub fn connect(&mut self) { + if self.room.is_none() { + self.room = Some(euph::Room::new( + self.chat.store().clone(), + self.ui_event_tx.clone(), + )); + } + } + + pub fn disconnect(&mut self) { + self.room = None; + } + + pub fn retain(&mut self) { + if let Some(room) = &self.room { + if room.stopped() { + self.room = None; + } + } + } + + pub async fn render(&mut self, frame: &mut Frame) { + let size = frame.size(); + + let chat_pos = Pos::new(0, 2); + let chat_size = Size { + height: size.height - 2, + ..size + }; + self.chat.render(frame, chat_pos, chat_size).await; + + let room = self.chat.store().room(); + let status = if let Some(room) = &self.room { + room.status().await.ok() + } else { + None + }; + Self::render_top_bar(frame, room, status); + } + + fn render_top_bar(frame: &mut Frame, room: &str, status: Option>) { + // Clear area in case something accidentally wrote on it already + let size = frame.size(); + for x in 0..size.width as i32 { + frame.write(Pos::new(x, 0), " ", ContentStyle::default()); + frame.write(Pos::new(x, 1), "─", ContentStyle::default()); + } + + // Write status + let status = match status { + None => format!("&{room}, archive"), + Some(None) => format!("&{room}, connecting..."), + Some(Some(Status::Joining(j))) => { + if j.bounce.is_none() { + format!("&{room}, joining...") + } else { + format!("&{room}, auth required") + } + } + Some(Some(Status::Joined(j))) => { + let nick = &j.session.name; + if nick.is_empty() { + format!("&{room}, present without nick") + } else { + format!("&{room}, present as {nick}",) + } + } + }; + frame.write(Pos::new(0, 0), &status, ContentStyle::default()); + } + + pub async fn handle_key_event( + &mut self, + terminal: &mut Terminal, + size: Size, + crossterm_lock: &Arc>, + event: KeyEvent, + ) { + let chat_size = Size { + height: size.height - 2, + ..size + }; + self.chat + .handle_navigation(terminal, chat_size, event) + .await; + + if let Some(room) = &self.room { + if let Ok(Some(Status::Joined(_))) = room.status().await { + if let KeyCode::Char('n' | 'N') = event.code { + if let Some(new_nick) = util::prompt(terminal, crossterm_lock) { + let _ = room.nick(new_nick); + } + } + + if let Some((parent, content)) = self + .chat + .handle_messaging(terminal, crossterm_lock, event) + .await + { + let _ = room.send(parent, content); + } + } + } + } +} diff --git a/src/ui/rooms.rs b/src/ui/rooms.rs index 17817d8..03841f4 100644 --- a/src/ui/rooms.rs +++ b/src/ui/rooms.rs @@ -11,6 +11,7 @@ use crate::chat::Chat; use crate::euph; use crate::vault::{EuphMsg, EuphVault, Vault}; +use super::room::EuphRoom; use super::{util, UiEvent}; mod style { @@ -33,6 +34,7 @@ struct Cursor { pub struct Rooms { vault: Vault, + ui_event_tx: mpsc::UnboundedSender, /// Cursor position inside the room list. /// @@ -42,18 +44,17 @@ pub struct Rooms { /// If set, a single room is displayed in full instead of the room list. focus: Option, - euph_rooms: HashMap, - euph_chats: HashMap>, + euph_rooms: HashMap, } impl Rooms { - pub fn new(vault: Vault) -> Self { + pub fn new(vault: Vault, ui_event_tx: mpsc::UnboundedSender) -> Self { Self { vault, + ui_event_tx, cursor: None, focus: None, euph_rooms: HashMap::new(), - euph_chats: HashMap::new(), } } @@ -105,9 +106,10 @@ impl Rooms { .map(|n| n.to_string()) .collect::>(); - self.euph_rooms - .retain(|n, r| rooms.contains(n) && !r.stopped()); - self.euph_chats.retain(|n, _| rooms.contains(n)); + self.euph_rooms.retain(|n, r| rooms.contains(n)); + for room in self.euph_rooms.values_mut() { + room.retain(); + } } fn make_consistent(&mut self, rooms: &[String], height: i32) { @@ -117,11 +119,10 @@ impl Rooms { pub async fn render(&mut self, frame: &mut Frame) { if let Some(room) = &self.focus { - let chat = self - .euph_chats - .entry(room.clone()) - .or_insert_with(|| Chat::new(self.vault.euph(room.clone()))); - chat.render(frame, Pos::new(0, 0), frame.size()).await; + let actual_room = self.euph_rooms.entry(room.clone()).or_insert_with(|| { + EuphRoom::new(self.vault.euph(room.clone()), self.ui_event_tx.clone()) + }); + actual_room.render(frame).await; } else { self.render_rooms(frame).await; } @@ -160,13 +161,19 @@ impl Rooms { &mut self, terminal: &mut Terminal, size: Size, - ui_event_tx: &mpsc::UnboundedSender, crossterm_lock: &Arc>, event: KeyEvent, ) { - if let Some(focus) = &self.focus { + if let Some(room) = &self.focus { if event.code == KeyCode::Esc { self.focus = None; + } else { + let actual_room = self.euph_rooms.entry(room.clone()).or_insert_with(|| { + EuphRoom::new(self.vault.euph(room.clone()), self.ui_event_tx.clone()) + }); + actual_room + .handle_key_event(terminal, size, crossterm_lock, event) + .await; } } else { let rooms = self.rooms().await; @@ -196,26 +203,28 @@ impl Rooms { if let Some(cursor) = &self.cursor { if let Some(room) = rooms.get(cursor.index) { let room = room.clone(); - self.euph_rooms.entry(room.clone()).or_insert_with(|| { - euph::Room::new( - room.clone(), - self.vault.euph(room), - ui_event_tx.clone(), - ) - }); + let actual_room = + self.euph_rooms.entry(room.clone()).or_insert_with(|| { + EuphRoom::new( + self.vault.euph(room.clone()), + self.ui_event_tx.clone(), + ) + }); + actual_room.connect(); } } } KeyCode::Char('C') => { if let Some(room) = util::prompt(terminal, crossterm_lock) { let room = room.trim().to_string(); - self.euph_rooms.entry(room.clone()).or_insert_with(|| { - euph::Room::new( - room.clone(), - self.vault.euph(room), - ui_event_tx.clone(), - ) - }); + let actual_room = + self.euph_rooms.entry(room.clone()).or_insert_with(|| { + EuphRoom::new( + self.vault.euph(room.clone()), + self.ui_event_tx.clone(), + ) + }); + actual_room.connect(); } } KeyCode::Char('d') => { @@ -229,7 +238,6 @@ impl Rooms { if let Some(cursor) = &self.cursor { if let Some(room) = rooms.get(cursor.index) { self.euph_rooms.remove(room); - self.euph_chats.remove(room); self.vault.euph(room.clone()).delete(); } } diff --git a/src/vault/euph.rs b/src/vault/euph.rs index 16b5181..4121e7e 100644 --- a/src/vault/euph.rs +++ b/src/vault/euph.rs @@ -93,6 +93,10 @@ pub struct EuphVault { } impl EuphVault { + pub fn room(&self) -> &str { + &self.room + } + pub fn join(&self, time: DateTime) { let request = EuphRequest::Join { room: self.room.clone(),