use crate::client::api::{ApiClient, UreqTransport}; use crate::client::event_stream::{StreamItem, follow_session_events}; use crate::client::transcript_fold::{ TranscriptItem, fold_event, fold_page, group_tool_runs, raw_seq, }; use crate::ui::{self, TranscriptScreen}; use event_model::SeqEvent; use iris::android::{AndroidAppState, AndroidUiState}; use iris::prelude::*; use std::{ sync::{ Arc, atomic::{AtomicU64, Ordering}, }, thread, }; #[derive(AndroidUiState)] pub struct TranscriptClient { ui_state: AndroidUiState, /// The screen's own content -- everything under the fixed /// [`frame_report_controls`] bar, which is built once (`new`, below) /// and never touched by `show_message`/`rebuild_transcript`'s own /// `set` calls the way `desktop-app`'s `transcript_ptr` isn't touched /// by rebuilding the session list beside it. content: WeakWidget, screen: Option, items: Vec, session_id: Option, generation: Arc, } fn build_transport() -> Result { crate::android::enrollment::transport() } fn placeholder(rsc: &mut Rsc, message: &str) -> StrongWidget { wtext(message.to_string()) .color(PaintId::WHITE) .overflow(TextOverflow::Wrap) .pad(16) .add_strong(rsc) .any() } fn frame_report_controls(rsc: &mut StdRsc) -> WeakWidget { type Rsc = StdRsc; let report_rect = rect(Srgba8::rgb(50, 50, 60)) .on( CursorSense::click(), |ctx: EventIdCtx<'_, Rsc, _, _>, _rsc: &mut Rsc| match ctx .state .android_state() .frame_report .report() { Some(stats) => log::info!("iris frame report: {stats}"), None => log::info!( "iris frame report: no frames recorded -- scroll first, then press this" ), }, ) .label("Frame report"); let report = ( report_rect, wtext("Frame report").size(18).text_align(Align::CENTER), ) .stack() .pad(8) .add(rsc); let reset_rect = rect(Srgba8::rgb(70, 40, 40)) .on( CursorSense::click(), |ctx: EventIdCtx<'_, Rsc, _, _>, _rsc: &mut Rsc| { ctx.state.android_state_mut().frame_report.reset(); log::info!("iris frame report: reset"); }, ) .label("Reset frame report"); let reset = ( reset_rect, wtext("Reset").size(18).text_align(Align::CENTER), ) .stack() .pad(8) .add(rsc); (report, reset).span(Dir::RIGHT).height(56).add(rsc) } impl TranscriptClient { pub(super) fn new(mut ui_state: AndroidUiState, rsc: &mut StdRsc) -> Self { crate::ui::register_fonts(&mut rsc.ui); let content = WidgetPtr::new().add(rsc); let loading = placeholder(rsc, "Loading sessions..."); content(rsc).set(loading); let tree = (frame_report_controls(rsc), content.height(rest(1))) .span(Dir::DOWN) .add_strong(rsc) .any(); ui_state.set_root(rsc, tree); let mut client = Self { ui_state, content, screen: None, items: Vec::new(), session_id: None, generation: Arc::new(AtomicU64::new(0)), }; client.spawn_fetch_sessions(rsc); client } } impl AndroidAppState for TranscriptClient { type Resources = StdRsc; fn back_pressed(&mut self, _rsc: &mut Self::Resources) -> bool { // No screen stack of its own -- same "let the activity finish" // answer `iris-android-app`'s tabs `Client` already gives. false } } impl TranscriptClient { fn show_message(&mut self, rsc: &mut StdRsc, message: &str) { let widget = placeholder(rsc, message); (self.content)(rsc).set(widget); self.screen = None; } fn spawn_fetch_sessions(&mut self, rsc: &mut StdRsc) { let my_generation = self.generation.load(Ordering::SeqCst); let generation = self.generation.clone(); rsc.spawn_task(async move |mut ctx| { let outcome = match build_transport() { Ok(transport) => ApiClient::new(transport) .fetch_sessions() .map_err(|e| e.to_string()), Err(e) => Err(format!("couldn't set up TLS: {e}")), }; ctx.update(move |state: &mut TranscriptClient, rsc| { if generation.load(Ordering::SeqCst) != my_generation { return; } match outcome { Ok(sessions) => match sessions.into_iter().next() { Some(session) => state.select_session(rsc, session.id), None => state.show_message(rsc, "No sessions on the sandbox server."), }, Err(message) => { state.show_message(rsc, &format!("Couldn't list sessions: {message}")) } } }); }); } fn select_session(&mut self, rsc: &mut StdRsc, session_id: String) { let my_generation = self.generation.fetch_add(1, Ordering::SeqCst) + 1; self.items.clear(); self.session_id = Some(session_id.clone()); self.show_message(rsc, "Loading transcript..."); let live_generation = self.generation.clone(); rsc.spawn_task(async move |mut ctx| { let transports = build_transport().and_then(|rest| build_transport().map(|stream| (rest, stream))); let (rest, stream_transport) = match transports { Ok(pair) => pair, Err(e) => { let message = format!("couldn't set up TLS: {e}"); ctx.update(move |state: &mut TranscriptClient, rsc| { if live_generation.load(Ordering::SeqCst) == my_generation { state.show_message(rsc, &message); } }); return; } }; let api = ApiClient::new(rest); let page: Result, String> = api .fetch_transcript_page(&session_id, None, 200, true) .map_err(|e| e.to_string()); let after = page .as_ref() .ok() .and_then(|values| values.last()) .and_then(raw_seq) .unwrap_or(0); let result = page.and_then(|values| fold_page(&values)); { let live_generation = live_generation.clone(); ctx.update(move |state: &mut TranscriptClient, rsc| { if live_generation.load(Ordering::SeqCst) != my_generation { return; } match result { Ok(items) => { state.items = items; state.rebuild_transcript(rsc); } Err(message) => { state.show_message(rsc, &format!("Couldn't load transcript: {message}")) } } }); } if live_generation.load(Ordering::SeqCst) != my_generation { return; } let _ = follow_session_events( &stream_transport, &session_id, after, move |item| match item { StreamItem::Open | StreamItem::Reset => { live_generation.load(Ordering::SeqCst) == my_generation } StreamItem::Event { event, .. } => { if live_generation.load(Ordering::SeqCst) != my_generation { return false; } let live_generation = live_generation.clone(); ctx.update(move |state: &mut TranscriptClient, rsc| { if live_generation.load(Ordering::SeqCst) != my_generation { return; } state.apply_event(rsc, &event); }); true } }, ); }); } fn rebuild_transcript(&mut self, rsc: &mut StdRsc) { let in_progress = self .screen .as_ref() .map(|screen| (screen.composer.field)(rsc).text().to_string()) .filter(|t| !t.is_empty()); let rows = group_tool_runs(&self.items); let (screen, tree) = ui::build_tree(rsc, rows); if let Some(text) = in_progress { (screen.composer.field)(rsc).set(&text); } if let Some(session_id) = self.session_id.clone() { let field = screen.composer.field; rsc.register_event(field, Submit, move |ctx, rsc| { let text = field(rsc).take(); let text = text.trim().to_string(); if !text.is_empty() { ctx.state.send_message(session_id.clone(), text); } }); } (self.content)(rsc).set(tree); self.screen = Some(screen); } fn apply_event(&mut self, rsc: &mut StdRsc, event: &SeqEvent) { let old_items = self.items.clone(); self.items = fold_event(&self.items, event); match self.screen.as_mut() { Some(screen) => screen.apply(rsc, &old_items, &self.items), None => self.rebuild_transcript(rsc), } } fn send_message(&mut self, session_id: String, text: String) { thread::spawn(move || { if let Ok(transport) = build_transport() { let api = ApiClient::new(transport); let _ = api.send_message(&session_id, &text, &[]); } }); } }