diff --git a/examples/arsnova-client-tui.rs b/examples/arsnova-client-tui.rs index a632c48..85509c1 100644 --- a/examples/arsnova-client-tui.rs +++ b/examples/arsnova-client-tui.rs @@ -64,6 +64,8 @@ async fn main() -> Result<(), ()> { let (tx, rx) = channel::(10); + let (fb_tx, fb_rx) = channel::(10); + let _ = tx .clone() .send(client.get_feedback(&cli.room).await.unwrap()) @@ -81,25 +83,43 @@ async fn main() -> Result<(), ()> { let l2 = create_ui(&mut terminal, &title, rx); - let l3 = tokio::spawn(async { + let l3 = tokio::spawn(async move { loop { if event::poll(std::time::Duration::from_millis(16)) .map_err(|_| ()) .is_ok() { if let event::Event::Key(key) = event::read().map_err(|_| ()).unwrap() { - if key.kind == KeyEventKind::Press && key.code == KeyCode::Esc { - break; + if key.kind == KeyEventKind::Press { + match key.code { + KeyCode::Esc => break, + KeyCode::Char('a') | KeyCode::Char('1') => { + let _ = fb_tx.send(0).await; + } + KeyCode::Char('b') | KeyCode::Char('2') => { + let _ = fb_tx.send(1).await; + } + KeyCode::Char('c') | KeyCode::Char('3') => { + let _ = fb_tx.send(2).await; + } + KeyCode::Char('d') | KeyCode::Char('4') => { + let _ = fb_tx.send(3).await; + } + _ => {} + }; } } } } }); + let l4 = client.register_feedback_receiver(&cli.room, fb_rx); + select! { _ = l1 => {}, _ = l2 => {}, _ = l3 => {}, + _ = l4 => {} } let _ = stdout().execute(LeaveAlternateScreen).map_err(|_| ()); @@ -113,40 +133,48 @@ async fn create_ui( title: &str, mut rx: Receiver, ) -> Result<(), ()> { - fn feedback_paragraph(feedback: &Feedback, idx: usize, width: usize) -> Paragraph<'static> { - let value = match idx { - 0 => feedback.very_good, - 1 => feedback.good, - 2 => feedback.bad, - 3 => feedback.very_bad, - _ => 0, + const ICONS: [&str; 4] = ["Super", "Gut", "Nicht so gut", "Schlecht"]; + + let feedback_paragraph = + |feedback: &Feedback, idx: usize, width: usize| -> Paragraph<'static> { + let value = match idx { + 0 => feedback.very_good, + 1 => feedback.good, + 2 => feedback.bad, + 3 => feedback.very_bad, + _ => 0, + }; + + let icons = ICONS + .iter() + .map(|icon| format!("{: <12}", icon)) + .collect::>(); + + let icon = match idx { + 0 => &icons[0], + 1 => &icons[1], + 2 => &icons[2], + 3 => &icons[3], + _ => " ", + }; + + let width = width - 24; + + let l = ((value as f32 / feedback.count_votes() as f32) * width as f32) as usize; + + match idx { + 0..=3 => Paragraph::new(Line::from(vec![ + Span::raw(format!("{} : ", icon)), + Span::raw(format!("[{: >5}] ", value)).dim(), + Span::raw("■".to_string().repeat(l).to_string()) + .green() + .on_black(), + Span::raw(" ".to_string().repeat(width - l).to_string()).on_black(), + ])), + _ => Paragraph::default(), + } }; - let icon = match idx { - 0 => "Super ", - 1 => "Gut ", - 2 => "Nicht so gut", - 3 => "Schlecht ", - _ => " ", - }; - - let width = width - 24; - - let l = ((value as f32 / feedback.count_votes() as f32) * width as f32) as usize; - - match idx { - 0..=3 => Paragraph::new(Line::from(vec![ - Span::raw(format!("{} : ", icon)), - Span::raw(format!("[{: >5}] ", value)).dim(), - Span::raw("■".to_string().repeat(l).to_string()) - .green() - .on_black(), - Span::raw(" ".to_string().repeat(width - l).to_string()).on_black(), - ])), - _ => Paragraph::default(), - } - } - loop { let feedback = match rx.recv().await { Some(feedback) => feedback, @@ -156,9 +184,11 @@ async fn create_ui( let _ = terminal.draw(|frame| { let layout = Layout::default() .direction(Direction::Vertical) - .constraints(vec![ + .constraints([ Constraint::Max(1), Constraint::Max(6), + Constraint::Max(2), + Constraint::Max(1), Constraint::Min(1), Constraint::Max(1), ]) @@ -185,12 +215,12 @@ async fn create_ui( Paragraph::new("Beenden mit ") .on_blue() .alignment(Alignment::Left), - layout[3], + layout[5], ); let feedback_layout = Layout::default() .direction(Direction::Vertical) - .constraints(vec![ + .constraints([ Constraint::Max(1), Constraint::Max(1), Constraint::Max(1), @@ -205,6 +235,31 @@ async fn create_ui( feedback_layout[idx], ) }); + + let button_layout = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage(25), + Constraint::Percentage(25), + Constraint::Percentage(25), + Constraint::Percentage(25), + Constraint::Min(0), + ]) + .split(layout[3]); + + ICONS.iter().enumerate().for_each(|(idx, label)| { + frame.render_widget( + Paragraph::new(Line::from(vec![ + Span::raw(format!(" {} ", idx + 1)) + .white() + .on_magenta() + .bold(), + Span::raw(format!("{: ^14}", label)).white().on_black(), + ])) + .alignment(Alignment::Center), + button_layout[idx], + ) + }); }); } } diff --git a/examples/arsnova-client.gif b/examples/arsnova-client.gif index 58171fe..bf871b1 100644 Binary files a/examples/arsnova-client.gif and b/examples/arsnova-client.gif differ diff --git a/src/client.rs b/src/client.rs index 86e14b4..c7f72a2 100644 --- a/src/client.rs +++ b/src/client.rs @@ -29,7 +29,7 @@ use reqwest::{IntoUrl, StatusCode}; use serde::Deserialize; use serde_json::json; use tokio::join; -use tokio::sync::mpsc::Sender; +use tokio::sync::mpsc::{Receiver, Sender}; use tokio_tungstenite::connect_async; use tokio_tungstenite::tungstenite::Message; use url::Url; @@ -361,6 +361,66 @@ impl Client { } } + /// Register feedback channel receiver and send incoming feedback to service + /// + /// This method fails on connection or response errors and if + /// no room is available with given room ID. + pub async fn register_feedback_receiver( + &self, + short_id: &str, + mut receiver: Receiver, + ) -> Result<(), ClientError> { + let room_info = self.get_room_info(short_id).await?; + + let ws_url = self.api_url.replace("http", "ws"); + let (socket, _) = connect_async(Url::parse(&format!("{}/ws/websocket", ws_url)).unwrap()) + .await + .map_err(|_| ConnectionError)?; + + let (mut write, _) = socket.split(); + + let user_id = self.get_user_id().unwrap_or_default(); + + if write + .send(Message::Text( + WsConnectMessage::new(self.token.as_ref().unwrap()).to_string(), + )) + .await + .is_ok() + { + return match write + .send(Message::Text( + WsSubscribeMessage::new(&room_info.id).to_string(), + )) + .await + { + Ok(_) => loop { + if let Some(value) = receiver.recv().await { + let payload = json!({ + "type": "CreateFeedback", + "payload": { + "roomId": room_info.id, + "userId": user_id, + "value": value + } + }) + .to_string(); + + let _ = write + .send(Message::Text(format!( + "SEND\ndestination:/queue/feedback.command\ncontent-type:application/json\ncontent-length:{}\n\n{}\0", + payload.chars().count(), + payload, + ))).await; + }; + }, + Err(_) => Err(ConnectionError), + }; + } + + Err(ConnectionError) + } + /// Registers a handler to get notifications on feedback change. /// /// This is done by using websocket connections to ARSnova.