feat(beta): External Beta — passwords, channel join, mute, bookmarks, encrypted identity
The v0.3 client could only ever connect to a hardcoded default
channel with no password and offered no controls mid-call.
External Beta closes those gaps and tightens identity-at-rest.
User-facing additions
---------------------
* **Server password** on the connect form. Plumbed through
`BridgeError`-aware `connect(host, nickname, password)`. Empty
string means "no password" — no behaviour change for open
servers.
* **Channel join**: tapping a row (or its login icon) in the
channel tree issues a `client_move`. Names containing "🔒" or
"password" prompt for a channel password first.
* **Self-mute** for both microphone (`client_input_muted`) and
speaker (`client_output_muted`) via FilterChips. Output mute
also flips the audio engine's local output-muted flag so
playback silences immediately, before the server acknowledges.
* **Master output gain** slider (0–200%). Plumbed through an
`AtomicU32` (f32 bits) on the engine that the cpal output
callback multiplies into every sample.
* **Bookmarks**: SQLite-backed list with Save / Connect / Delete
actions. Bookmarks persist across app restarts; tapping one
pre-fills the form and dials immediately.
Hardening
---------
* **Encrypted identity at rest** (RISK-PoC-002 closure for the
file-only threat model). ChaCha20-Poly1305 envelope: nonce +
ciphertext written atomically with mode 0600; 32-byte DEK in a
separate `identity.dek` file. Legacy plaintext identity files
are auto-detected, read, and upgraded on the next save. Full OS-
keyring integration is still v0.4 work — documented in the
store's doc comment.
* **Mobile voice-comm routing**: on Android, `AudioEngine::start`
uses JNI to set `AudioManager.setMode(MODE_IN_COMMUNICATION)`
when `cfg.mobile_voice_preset` is true (default). This engages
the device-side AEC/NS pipeline on most Pixel/Moto/Samsung
hardware even though cpal still opens the AAudio default input
preset. Full `setInputPreset(VOICE_COMMUNICATION)` switch is
still RISK-AUDIO-MOBILE-001 (needs cpal upstream or an Oboe
fork).
* **Log noise**: bridge default `EnvFilter` now silences
`tsproto::resend=error` and `tsproto::packet_codec=error` so
the redacted diagnostic export is human-readable. Still
overridable via `RUST_LOG=...`.
Engineering
-----------
* **`chanora_storage`** gains `BookmarkRepository` (rusqlite
bundled) with `add` / `update` / `delete` / `list`. The
identity store now layers on `chacha20poly1305` + `rand` +
`zeroize` for the envelope.
* **`chanora_protocol`** exposes `move_to_channel` and
`set_muted` on `ProtocolClient`, dispatched through the
existing `connection_task` request channel onto tsclientlib's
generated `client.client_move(...)` and
`state.client_update().set_input_muted/set_output_muted(...)`
paths.
* **`chanora_core::ChanoraSession`** wires the bookmark store
next to the identity store inside `init_storage`, and adds
`list_bookmarks` / `add_bookmark` / `update_bookmark` /
`delete_bookmark` / `move_to_channel` / `set_self_muted` /
`set_output_gain`.
* **`chanora_audio::AudioEngine`** carries `output_gain` and
`output_muted` atomics; the output callback consults both. The
Android branch of `start()` engages MODE_IN_COMMUNICATION via
a small JNI helper that reuses the `ndk_context` global set by
the bridge's `android_init` hook.
* **`chanora_bridge::api`** adds `set_input_muted`,
`set_output_muted`, `set_output_gain`, `move_to_channel`,
`list_bookmarks`, `add_bookmark`, `update_bookmark`,
`delete_bookmark`, and the `BridgeBookmark` DTO. FRB v2.12
codegen regenerated.
Tests + CI
----------
* `chanora_storage` test count rises from 3 to 8 — bookmark CRUD
round-trip, missing-row → `NotFound`, encrypted round-trip
(verifies ciphertext is not the plaintext on disk), and the
legacy plaintext upgrade path.
* New `.github/workflows/ci.yml`: `cargo check --workspace`,
`cargo test --workspace --no-fail-fast`, `cargo clippy`
(advisory), `flutter analyze`, and `flutter test` excluding
the live-server `e2e` tag.
Live-verified on Moto G Stylus 5G against cn.teamspeak.app:
saved a bookmark, reconnected via it, joined a non-default
channel via tap, toggled both mutes, slid the volume, and the
redacted diagnostic export confirmed `AudioManager mode set to
MODE_IN_COMMUNICATION`, `client_move sent`, and `client_update
sent` lines.
This commit is contained in:
@@ -8,6 +8,10 @@
|
||||
|
||||
"fieldServerHost": "Server address",
|
||||
"fieldNickname": "Nickname",
|
||||
"fieldServerPassword": "Server password",
|
||||
"fieldServerPasswordHelp": "Leave blank if the server does not require one.",
|
||||
"fieldPassword": "Password",
|
||||
"fieldDisplayName": "Display name",
|
||||
|
||||
"connectAction": "Connect",
|
||||
"disconnectAction": "Disconnect",
|
||||
@@ -18,6 +22,17 @@
|
||||
"startAudioAction": "Start audio",
|
||||
"pttHoldToTalk": "Hold to talk",
|
||||
"pttTransmitting": "Transmitting…",
|
||||
"inputMuteAction": "Mute mic",
|
||||
"inputUnmuteAction": "Unmute mic",
|
||||
"outputMuteAction": "Mute speaker",
|
||||
"outputUnmuteAction": "Unmute speaker",
|
||||
"joinChannelAction": "Join channel",
|
||||
"channelPasswordTitle": "Channel password",
|
||||
"bookmarksHeading": "Bookmarks",
|
||||
"bookmarksEmpty": "No bookmarks yet. Enter a server above and tap \"Save bookmark\".",
|
||||
"bookmarkAddAction": "Save bookmark",
|
||||
"bookmarkAddTitle": "Save bookmark",
|
||||
"bookmarkDeleteAction": "Delete bookmark",
|
||||
|
||||
"statusIdle": "Not connected",
|
||||
"statusConnecting": "Connecting…",
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
|
||||
"fieldServerHost": "服务器地址",
|
||||
"fieldNickname": "昵称",
|
||||
"fieldServerPassword": "服务器密码",
|
||||
"fieldServerPasswordHelp": "如果服务器无需密码,留空即可。",
|
||||
"fieldPassword": "密码",
|
||||
"fieldDisplayName": "显示名称",
|
||||
|
||||
"connectAction": "连接",
|
||||
"disconnectAction": "断开连接",
|
||||
@@ -17,6 +21,17 @@
|
||||
"startAudioAction": "启动语音",
|
||||
"pttHoldToTalk": "按住说话",
|
||||
"pttTransmitting": "正在发送…",
|
||||
"inputMuteAction": "静音麦克风",
|
||||
"inputUnmuteAction": "取消麦克风静音",
|
||||
"outputMuteAction": "静音扬声器",
|
||||
"outputUnmuteAction": "取消扬声器静音",
|
||||
"joinChannelAction": "加入频道",
|
||||
"channelPasswordTitle": "频道密码",
|
||||
"bookmarksHeading": "书签",
|
||||
"bookmarksEmpty": "尚无书签。先在上方填写服务器,然后点击“保存书签”。",
|
||||
"bookmarkAddAction": "保存书签",
|
||||
"bookmarkAddTitle": "保存书签",
|
||||
"bookmarkDeleteAction": "删除书签",
|
||||
|
||||
"statusIdle": "未连接",
|
||||
"statusConnecting": "正在连接…",
|
||||
|
||||
@@ -121,6 +121,30 @@ abstract class AppL10n {
|
||||
/// **'Nickname'**
|
||||
String get fieldNickname;
|
||||
|
||||
/// No description provided for @fieldServerPassword.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Server password'**
|
||||
String get fieldServerPassword;
|
||||
|
||||
/// No description provided for @fieldServerPasswordHelp.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Leave blank if the server does not require one.'**
|
||||
String get fieldServerPasswordHelp;
|
||||
|
||||
/// No description provided for @fieldPassword.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Password'**
|
||||
String get fieldPassword;
|
||||
|
||||
/// No description provided for @fieldDisplayName.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Display name'**
|
||||
String get fieldDisplayName;
|
||||
|
||||
/// No description provided for @connectAction.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -175,6 +199,72 @@ abstract class AppL10n {
|
||||
/// **'Transmitting…'**
|
||||
String get pttTransmitting;
|
||||
|
||||
/// No description provided for @inputMuteAction.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Mute mic'**
|
||||
String get inputMuteAction;
|
||||
|
||||
/// No description provided for @inputUnmuteAction.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Unmute mic'**
|
||||
String get inputUnmuteAction;
|
||||
|
||||
/// No description provided for @outputMuteAction.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Mute speaker'**
|
||||
String get outputMuteAction;
|
||||
|
||||
/// No description provided for @outputUnmuteAction.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Unmute speaker'**
|
||||
String get outputUnmuteAction;
|
||||
|
||||
/// No description provided for @joinChannelAction.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Join channel'**
|
||||
String get joinChannelAction;
|
||||
|
||||
/// No description provided for @channelPasswordTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Channel password'**
|
||||
String get channelPasswordTitle;
|
||||
|
||||
/// No description provided for @bookmarksHeading.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Bookmarks'**
|
||||
String get bookmarksHeading;
|
||||
|
||||
/// No description provided for @bookmarksEmpty.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No bookmarks yet. Enter a server above and tap \"Save bookmark\".'**
|
||||
String get bookmarksEmpty;
|
||||
|
||||
/// No description provided for @bookmarkAddAction.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Save bookmark'**
|
||||
String get bookmarkAddAction;
|
||||
|
||||
/// No description provided for @bookmarkAddTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Save bookmark'**
|
||||
String get bookmarkAddTitle;
|
||||
|
||||
/// No description provided for @bookmarkDeleteAction.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Delete bookmark'**
|
||||
String get bookmarkDeleteAction;
|
||||
|
||||
/// No description provided for @statusIdle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
||||
@@ -21,6 +21,19 @@ class AppL10nEn extends AppL10n {
|
||||
@override
|
||||
String get fieldNickname => 'Nickname';
|
||||
|
||||
@override
|
||||
String get fieldServerPassword => 'Server password';
|
||||
|
||||
@override
|
||||
String get fieldServerPasswordHelp =>
|
||||
'Leave blank if the server does not require one.';
|
||||
|
||||
@override
|
||||
String get fieldPassword => 'Password';
|
||||
|
||||
@override
|
||||
String get fieldDisplayName => 'Display name';
|
||||
|
||||
@override
|
||||
String get connectAction => 'Connect';
|
||||
|
||||
@@ -48,6 +61,40 @@ class AppL10nEn extends AppL10n {
|
||||
@override
|
||||
String get pttTransmitting => 'Transmitting…';
|
||||
|
||||
@override
|
||||
String get inputMuteAction => 'Mute mic';
|
||||
|
||||
@override
|
||||
String get inputUnmuteAction => 'Unmute mic';
|
||||
|
||||
@override
|
||||
String get outputMuteAction => 'Mute speaker';
|
||||
|
||||
@override
|
||||
String get outputUnmuteAction => 'Unmute speaker';
|
||||
|
||||
@override
|
||||
String get joinChannelAction => 'Join channel';
|
||||
|
||||
@override
|
||||
String get channelPasswordTitle => 'Channel password';
|
||||
|
||||
@override
|
||||
String get bookmarksHeading => 'Bookmarks';
|
||||
|
||||
@override
|
||||
String get bookmarksEmpty =>
|
||||
'No bookmarks yet. Enter a server above and tap \"Save bookmark\".';
|
||||
|
||||
@override
|
||||
String get bookmarkAddAction => 'Save bookmark';
|
||||
|
||||
@override
|
||||
String get bookmarkAddTitle => 'Save bookmark';
|
||||
|
||||
@override
|
||||
String get bookmarkDeleteAction => 'Delete bookmark';
|
||||
|
||||
@override
|
||||
String get statusIdle => 'Not connected';
|
||||
|
||||
|
||||
@@ -20,6 +20,18 @@ class AppL10nZh extends AppL10n {
|
||||
@override
|
||||
String get fieldNickname => '昵称';
|
||||
|
||||
@override
|
||||
String get fieldServerPassword => '服务器密码';
|
||||
|
||||
@override
|
||||
String get fieldServerPasswordHelp => '如果服务器无需密码,留空即可。';
|
||||
|
||||
@override
|
||||
String get fieldPassword => '密码';
|
||||
|
||||
@override
|
||||
String get fieldDisplayName => '显示名称';
|
||||
|
||||
@override
|
||||
String get connectAction => '连接';
|
||||
|
||||
@@ -47,6 +59,39 @@ class AppL10nZh extends AppL10n {
|
||||
@override
|
||||
String get pttTransmitting => '正在发送…';
|
||||
|
||||
@override
|
||||
String get inputMuteAction => '静音麦克风';
|
||||
|
||||
@override
|
||||
String get inputUnmuteAction => '取消麦克风静音';
|
||||
|
||||
@override
|
||||
String get outputMuteAction => '静音扬声器';
|
||||
|
||||
@override
|
||||
String get outputUnmuteAction => '取消扬声器静音';
|
||||
|
||||
@override
|
||||
String get joinChannelAction => '加入频道';
|
||||
|
||||
@override
|
||||
String get channelPasswordTitle => '频道密码';
|
||||
|
||||
@override
|
||||
String get bookmarksHeading => '书签';
|
||||
|
||||
@override
|
||||
String get bookmarksEmpty => '尚无书签。先在上方填写服务器,然后点击“保存书签”。';
|
||||
|
||||
@override
|
||||
String get bookmarkAddAction => '保存书签';
|
||||
|
||||
@override
|
||||
String get bookmarkAddTitle => '保存书签';
|
||||
|
||||
@override
|
||||
String get bookmarkDeleteAction => '删除书签';
|
||||
|
||||
@override
|
||||
String get statusIdle => '未连接';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user