Render and interact with individual rooms
This commit is contained in:
parent
9cd7ee008d
commit
6fbc0c5ff7
8 changed files with 228 additions and 48 deletions
|
|
@ -52,6 +52,10 @@ impl<M: Msg, S: MsgStore<M>> Chat<M, S> {
|
|||
tree: TreeView::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn store(&self) -> &S {
|
||||
&self.store
|
||||
}
|
||||
}
|
||||
|
||||
impl<M: Msg, S: MsgStore<M>> Chat<M, S> {
|
||||
|
|
|
|||
|
|
@ -2,4 +2,5 @@ pub mod api;
|
|||
mod conn;
|
||||
mod room;
|
||||
|
||||
pub use conn::{Joined, Joining, Status};
|
||||
pub use room::Room;
|
||||
|
|
|
|||
|
|
@ -63,9 +63,9 @@ impl Event {
|
|||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Joining {
|
||||
hello: Option<HelloEvent>,
|
||||
snapshot: Option<SnapshotEvent>,
|
||||
bounce: Option<BounceEvent>,
|
||||
pub hello: Option<HelloEvent>,
|
||||
pub snapshot: Option<SnapshotEvent>,
|
||||
pub bounce: Option<BounceEvent>,
|
||||
}
|
||||
|
||||
impl Joining {
|
||||
|
|
@ -107,9 +107,9 @@ impl Joining {
|
|||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Joined {
|
||||
session: SessionView,
|
||||
account: Option<PersonalAccountView>,
|
||||
listing: HashMap<UserId, SessionView>,
|
||||
pub session: SessionView,
|
||||
pub account: Option<PersonalAccountView>,
|
||||
pub listing: HashMap<UserId, SessionView>,
|
||||
}
|
||||
|
||||
impl Joined {
|
||||
|
|
|
|||
|
|
@ -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<Option<Status>>),
|
||||
RequestLogs,
|
||||
Nick(String),
|
||||
Send(Option<Snowflake>, 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<Snowflake>, 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<UiEvent>,
|
||||
) -> Self {
|
||||
pub fn new(vault: EuphVault, ui_event_tx: mpsc::UnboundedSender<UiEvent>) -> 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<Snowflake>, content: String) -> Result<(), Error> {
|
||||
self.event_tx
|
||||
.send(Event::Send(parent, content))
|
||||
.map_err(|_| Error::Stopped)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
135
src/ui/room.rs
Normal file
135
src/ui/room.rs
Normal file
|
|
@ -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<UiEvent>,
|
||||
room: Option<euph::Room>,
|
||||
chat: Chat<EuphMsg, EuphVault>,
|
||||
}
|
||||
|
||||
impl EuphRoom {
|
||||
pub fn new(vault: EuphVault, ui_event_tx: mpsc::UnboundedSender<UiEvent>) -> 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<Option<Status>>) {
|
||||
// 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<FairMutex<()>>,
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<UiEvent>,
|
||||
|
||||
/// 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<String>,
|
||||
|
||||
euph_rooms: HashMap<String, euph::Room>,
|
||||
euph_chats: HashMap<String, Chat<EuphMsg, EuphVault>>,
|
||||
euph_rooms: HashMap<String, EuphRoom>,
|
||||
}
|
||||
|
||||
impl Rooms {
|
||||
pub fn new(vault: Vault) -> Self {
|
||||
pub fn new(vault: Vault, ui_event_tx: mpsc::UnboundedSender<UiEvent>) -> 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::<HashSet<String>>();
|
||||
|
||||
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<UiEvent>,
|
||||
crossterm_lock: &Arc<FairMutex<()>>,
|
||||
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();
|
||||
let actual_room =
|
||||
self.euph_rooms.entry(room.clone()).or_insert_with(|| {
|
||||
euph::Room::new(
|
||||
room.clone(),
|
||||
self.vault.euph(room),
|
||||
ui_event_tx.clone(),
|
||||
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();
|
||||
let actual_room =
|
||||
self.euph_rooms.entry(room.clone()).or_insert_with(|| {
|
||||
euph::Room::new(
|
||||
room.clone(),
|
||||
self.vault.euph(room),
|
||||
ui_event_tx.clone(),
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,6 +93,10 @@ pub struct EuphVault {
|
|||
}
|
||||
|
||||
impl EuphVault {
|
||||
pub fn room(&self) -> &str {
|
||||
&self.room
|
||||
}
|
||||
|
||||
pub fn join(&self, time: DateTime<Utc>) {
|
||||
let request = EuphRequest::Join {
|
||||
room: self.room.clone(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue