From 7254f81c653c2bbb07702f38a218ae99fbc9ccab Mon Sep 17 00:00:00 2001 From: Edison Jwa Date: Wed, 20 May 2026 08:17:22 +0900 Subject: [PATCH] fix(core): complete voice joins on server accept --- core/chanora_core/src/lib.rs | 92 +++++------------------------------- 1 file changed, 13 insertions(+), 79 deletions(-) diff --git a/core/chanora_core/src/lib.rs b/core/chanora_core/src/lib.rs index aea9179..cf6335d 100644 --- a/core/chanora_core/src/lib.rs +++ b/core/chanora_core/src/lib.rs @@ -642,13 +642,6 @@ impl ChanoraSession { if let Some(projection) = projection { self.emit_voice_state(projection).await; } - // Bring the audio engine up so the user can immediately - // hear other speakers + transmit on PTT. Tolerates - // failure the same way voice_join does: server-side - // we're in the channel regardless; if audio fails - // (missing mic permission, no device), the UI will - // still expose the controls and the user can resolve - // the underlying issue. if let Err(audio_err) = self.ensure_audio_running().await { warn!( target: "chanora_core", @@ -1241,6 +1234,18 @@ impl ChanoraSession { if let Some(pw) = requested_password { state.channel_passwords.insert(channel_id, pw); } + let epoch = state.join_state.authoritative.confirmed_epoch; + let _ = channel_join::reduce( + &mut state.join_state, + ChannelJoinEvent::AuthoritativeSelfMove { + channel: Some(JoinChannelId(channel_id)), + epoch, + source: AuthoritativeSource::LiveDelta, + }, + ); + let projection = channel_join::project(&state.join_state); + self.voice_selector.set_in_channel(true); + self.emit_voice_state(projection).await; drop(guard); // 2. Bring the audio engine up. Tolerate failure: the // server-side channel move has ALREADY succeeded (step @@ -1272,78 +1277,7 @@ impl ChanoraSession { // 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 - // it. Keeps the move/confirm contract honest even when - // upstream tsclientlib changes shape. - let deadline = std::time::Instant::now() + std::time::Duration::from_millis(1500); - let confirmed = loop { - if let Ok(snap) = self.snapshot().await { - if let Some((my_id, my_channel)) = self.find_own_in(&snap).await { - if my_channel == channel_id { - info!( - target: "chanora_core", - client_id = my_id, - channel_id, - "voice_join confirmed by snapshot" - ); - break true; - } - } - } - if std::time::Instant::now() >= deadline { - break false; - } - tokio::time::sleep(std::time::Duration::from_millis(80)).await; - }; - if !confirmed { - let mut guard = self.inner.lock().await; - if let Some(state) = guard.as_mut() { - let _ = channel_join::reduce( - &mut state.join_state, - ChannelJoinEvent::JoinTimeout { - key: pending_key, - now: std::time::Instant::now(), - }, - ); - let projection = channel_join::project(&state.join_state); - let still_in_channel = projection.current_channel.is_some(); - self.voice_selector.set_in_channel(still_in_channel); - self.emit_voice_state(projection).await; - } - return Err(CoreError::Protocol( - chanora_protocol::ProtocolError::ServerRejected { - // Use a sentinel "unknown" code (the canonical - // TS3 error catalogue uses 0x0001 for `undefined`). - code: 0x0001, - message: "channel move did not take effect server-side within timeout" - .to_string(), - }, - )); - } - let mut guard = self.inner.lock().await; - if let Some(state) = guard.as_mut() { - let epoch = state.join_state.authoritative.confirmed_epoch; - let _ = channel_join::reduce( - &mut state.join_state, - ChannelJoinEvent::AuthoritativeSelfMove { - channel: Some(JoinChannelId(channel_id)), - epoch, - source: AuthoritativeSource::Snapshot, - }, - ); - } - self.voice_selector.set_in_channel(true); - let projection = { - let guard = self.inner.lock().await; - guard - .as_ref() - .map(|state| channel_join::project(&state.join_state)) - }; - if let Some(projection) = projection { - self.emit_voice_state(projection).await; - } + info!(target: "chanora_core", channel_id, "voice_join accepted by server"); Ok(()) }