diff --git a/apps/chanora_flutter/ios/Runner/Info.plist b/apps/chanora_flutter/ios/Runner/Info.plist index b1ba0bb..590b676 100644 --- a/apps/chanora_flutter/ios/Runner/Info.plist +++ b/apps/chanora_flutter/ios/Runner/Info.plist @@ -72,5 +72,14 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + + UIFileSharingEnabled + + LSSupportsOpeningDocumentsInPlace + diff --git a/core/chanora_core/src/lib.rs b/core/chanora_core/src/lib.rs index f5d7858..52e2523 100644 --- a/core/chanora_core/src/lib.rs +++ b/core/chanora_core/src/lib.rs @@ -918,8 +918,36 @@ impl ChanoraSession { self.emit_voice_state(false).await; return Err(e); } - // 2. Bring the audio engine up. - self.ensure_audio_running().await?; + // 2. Bring the audio engine up. Tolerate failure: the + // server-side channel move has ALREADY succeeded (step + // 1), so the user is in the channel from every other + // peer's perspective. Failing voice_join hard here would + // leave the UI in a phantom "you're in the channel + // visually but no controls" state because the calling + // Dart code wouldn't receive the VoiceState(true) event + // that gates the AppBar mute icons + the on-screen PTT + // button. Honest behaviour: surface the audio error + // once on the event channel (via the AudioStopped event + // consumers already handle), then continue so the UI + // matches reality — user is in the channel, but mic / + // speakers may be silent until they resolve the audio + // error (e.g. grant mic permission, plug in a working + // device). + if let Err(audio_err) = self.ensure_audio_running().await { + warn!( + target: "chanora_core", + error = %audio_err, + channel_id, + "voice_join: server move succeeded but audio engine \ + failed to start; continuing with no-audio in-channel \ + state so the UI matches the server-side state" + ); + // Tell subscribers the audio engine is not running so + // any audio-stats poll / level meter renders correctly. + // The voice_join itself still resolves Ok below so the + // UI gains the channel + mute + PTT controls. + let _ = self.events_tx.send(SessionEvent::AudioStopped); + } // 3. Belt-and-braces: if the server replied Ok but never // actually moved us (legacy server, command processed // but rolled back later, etc.) the snapshot poll catches diff --git a/crates/chanora_bridge/src/api.rs b/crates/chanora_bridge/src/api.rs index 7bf429d..cc4ebdf 100644 --- a/crates/chanora_bridge/src/api.rs +++ b/crates/chanora_bridge/src/api.rs @@ -180,8 +180,31 @@ fn log_file_path() -> Option { .join("chanora.log"), ) } - #[cfg(any(target_os = "android", target_os = "ios"))] + #[cfg(target_os = "ios")] { + // iOS sandbox: write the log to the app's Documents + // directory so it persists across launches and can be + // pulled via Xcode -> Devices and Simulators -> Download + // Container, OR via Files.app on the device (the app + // appears under "On My iPhone" once we declare + // UIFileSharingEnabled + LSSupportsOpeningDocumentsInPlace + // in Info.plist — done in a follow-up). + // + // HOME on iOS resolves to the app sandbox root; Documents + // is the standard user-visible subdirectory. + let home = std::env::var_os("HOME")?; + Some( + std::path::PathBuf::from(home) + .join("Documents") + .join("chanora.log"), + ) + } + #[cfg(target_os = "android")] + { + // Android log file location is set up via the bridge's + // Java-side init that writes the chosen path into an env + // var (not currently wired; P1 follow-up). For now we + // return None and rely on logcat. None } }