feat: event-driven UI updates for instant channel switching (#15)
* chore: regenerate Cargo.lock after rebase * fix(ui): add 1s cool-down to prevent double-tap channel join voiceJoin returns instantly (fire-and-forget protocol), so the pending-join guard clears before a second tap lands. The cool-down prevents the rapid channel oscillation and ClientIsFlooding (524) that results from double-tapping. * fix(ui): handle ChannelAlreadyIn as success, ClientIsFlooding with backoff - ChannelAlreadyIn (0x0302): treat as silent success, update UI state - ClientIsFlooding (0x020c): show localized snackbar, extend cooldown 5s - Add l10n strings for flooding error (en + zh) * fix(proto): use Windows TS3 client version for broadest compatibility Matches Qint's default (Windows_3_X_X__1). Avoids server-side behavioral differences with TS5 version strings. * fix(proto): patch tsproto-types to handle short P-256 coordinates BigInt::to_bytes_be() strips leading zeros, causing WrongPublicKeyLength when a server's ephemeral key coordinate starts with 0x00. Patch from EdisonJwa/tsclientlib fix/p256-short-coordinate-pad branch left-pads coordinates to the P-256 field size instead of rejecting them. * refactor(core): stop watchdog from emitting SnapshotChanged The watchdog now serves only as a liveness probe (miss counting for reconnection). UI updates are handled entirely by the event-driven delta path (ProtocolDelta → SessionEvent → BridgeEvent → Flutter). Removes signature tracking and SnapshotChanged emission from the supervisor loop. The initial snapshot is still fetched via the Connected event handler in Flutter. * refactor(ui): remove channel-join cooldown guard With event-driven deltas the UI updates instantly on channel moves, so the 1-second cooldown is no longer needed. Double-taps are handled by the server (ChannelAlreadyIn → success) and the pending-channel-id guard prevents overlapping requests. Also removes the _lastJoinCompletedAt field entirely. * fix(core): reattach event forwarders after reconnect The reconnect path swapped in a new ProtocolClient but never took chat_rx, activity_rx, or delta_rx from it. After the first reconnect, the event-driven UI pipeline was dead. Fix by extracting spawn_event_forwarders() helper called on both initial connect and reconnect. Also replaces lossy try_recv+sleep polling with proper recv().await for push-based delivery. * feat(protocol): enrich delta schema with all snapshot-visible fields ClientJoined now carries input_muted, output_muted, is_server_query, talk_power, talk_power_granted. ChannelAdded/ChannelUpdated now carry has_password and needed_talk_power. ClientUpdated also carries is_server_query, talk_power, talk_power_granted. This prevents local snapshot drift where fabricated defaults could hide password requirements, talk-power restrictions, or client type. * refactor: remove dead SnapshotChanged variant end-to-end SnapshotChanged is no longer emitted since the watchdog was refactored to liveness-only. Removes the variant from SessionEvent, BridgeEvent, and the Flutter switch statement. FRB bindings regenerated. * fix(ci): regenerate license inventory and fix iOS submodule fetch - Regenerate docs/security/license-inventory.md to match current lockfile - Remove submodules: true from checkout (causes hard fail on private submodule) - Add explicit git submodule update --init --depth=1 with || true fallback - Check silero-coreml/Package.swift instead of directory existence
This commit is contained in:
@@ -9,7 +9,8 @@ import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart' hide protected;
|
||||
part 'api.freezed.dart';
|
||||
|
||||
// These functions are ignored because they are not marked as `pub`: `install_panic_diagnostic_hook`, `log_file_path`, `log_sink`, `map_join_error_code`, `map_join_sync_state`, `open_log_file`, `permission_events`, `publish_permission_state`, `runtime`, `session`, `task_join_error`, `transmit_mode_from_u8`
|
||||
// These functions are ignored because they are not marked as `pub`: `dispatch_platform_audio_event`, `install_panic_diagnostic_hook`, `log_file_path`, `log_sink`, `map_join_error_code`, `map_join_sync_state`, `open_log_file`, `permission_events`, `platform_audio_events`, `process`, `publish_permission_state`, `runtime`, `session`, `task_join_error`, `transmit_mode_from_u8`
|
||||
// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `PlatformAudioEvent`
|
||||
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`
|
||||
// These functions are ignored (category: IgnoreBecauseExplicitAttribute): `from_kotlin_str`, `to_permission_gate`
|
||||
|
||||
@@ -1090,17 +1091,6 @@ sealed class BridgeEvent with _$BridgeEvent {
|
||||
/// Audio engine stopped.
|
||||
const factory BridgeEvent.audioStopped() = BridgeEvent_AudioStopped;
|
||||
|
||||
/// Snapshot probe observed a change in channel/client counts.
|
||||
/// UI uses this to drive an auto-refresh without active
|
||||
/// polling.
|
||||
const factory BridgeEvent.snapshotChanged({
|
||||
/// Latest channel count.
|
||||
required int channels,
|
||||
|
||||
/// Latest client count.
|
||||
required int clients,
|
||||
}) = BridgeEvent_SnapshotChanged;
|
||||
|
||||
/// Detected desktop Push-to-Talk capability (gen2 v0.9.3,
|
||||
/// DEC-023..028). The fields carry only privacy-safe values per
|
||||
/// DEC-027: the capability level, a stable backend identifier,
|
||||
@@ -1209,9 +1199,50 @@ sealed class BridgeEvent with _$BridgeEvent {
|
||||
|
||||
/// Audio route changed (speaker/earpiece/BT/wired).
|
||||
const factory BridgeEvent.audioRouteChanged({
|
||||
/// The new audio route.
|
||||
required BridgeAudioRoute route,
|
||||
}) = BridgeEvent_AudioRouteChanged;
|
||||
const factory BridgeEvent.clientMoved({
|
||||
required BigInt clientId,
|
||||
required BigInt newChannelId,
|
||||
}) = BridgeEvent_ClientMoved;
|
||||
const factory BridgeEvent.clientJoined({
|
||||
required BigInt clientId,
|
||||
required BigInt channelId,
|
||||
required String name,
|
||||
required bool inputMuted,
|
||||
required bool outputMuted,
|
||||
required bool isServerQuery,
|
||||
required int talkPower,
|
||||
required bool talkPowerGranted,
|
||||
}) = BridgeEvent_ClientJoined;
|
||||
const factory BridgeEvent.clientLeft({
|
||||
required BigInt clientId,
|
||||
required String name,
|
||||
}) = BridgeEvent_ClientLeft;
|
||||
const factory BridgeEvent.clientUpdated({
|
||||
required BigInt clientId,
|
||||
required bool inputMuted,
|
||||
required bool outputMuted,
|
||||
required bool isServerQuery,
|
||||
required int talkPower,
|
||||
required bool talkPowerGranted,
|
||||
}) = BridgeEvent_ClientUpdated;
|
||||
const factory BridgeEvent.channelAdded({
|
||||
required BigInt id,
|
||||
required BigInt parent,
|
||||
required String name,
|
||||
required PlatformInt64 order,
|
||||
required bool hasPassword,
|
||||
int? neededTalkPower,
|
||||
}) = BridgeEvent_ChannelAdded;
|
||||
const factory BridgeEvent.channelRemoved({required BigInt id}) =
|
||||
BridgeEvent_ChannelRemoved;
|
||||
const factory BridgeEvent.channelUpdated({
|
||||
required BigInt id,
|
||||
required String name,
|
||||
required bool hasPassword,
|
||||
int? neededTalkPower,
|
||||
}) = BridgeEvent_ChannelUpdated;
|
||||
}
|
||||
|
||||
/// Bridge iOS voice-processing mode.
|
||||
|
||||
Reference in New Issue
Block a user