fix: improve voice and chat controls

This commit is contained in:
Edison Jwa
2026-05-25 18:26:56 +09:00
parent d03ea937e6
commit 487aff4e7c
13 changed files with 351 additions and 147 deletions
+73 -29
View File
@@ -174,6 +174,7 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
bool _hardMute = false;
bool _hardMuteByPermission = false;
bool _hardMuteByTalkPower = false;
bool _hardMuteByOutputMute = false;
bool _permissionHardMuteClearInFlight = false;
int _releaseTailMs = 200;
BigInt? _currentVoiceChannelId;
@@ -212,6 +213,7 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
final ValueNotifier<int> _chatFeedRevision = ValueNotifier(0);
int _chatUnread = 0;
bool _chatOpen = false;
bool _chatPreviewSnackBarVisible = false;
final ValueNotifier<List<_ReceivedPoke>> _pokeSnackBarPokes = ValueNotifier(
const [],
);
@@ -484,8 +486,10 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
await rust.setHardMute(muted: false);
if (!mounted || !_hardMuteByPermission) return;
setState(() {
_hardMute = false;
_hardMuteByPermission = false;
if (!_inputMuted && !_hardMuteByTalkPower && !_hardMuteByOutputMute) {
_hardMute = false;
}
});
} catch (e) {
if (!mounted) return;
@@ -1010,6 +1014,14 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
if (!mounted) return;
setState(() {
_outputMuted = next;
_hardMuteByOutputMute = next;
if (next) {
_hardMute = true;
} else if (!_inputMuted &&
!_hardMuteByPermission &&
!_hardMuteByTalkPower) {
_hardMute = false;
}
});
} catch (e) {
if (!mounted) return;
@@ -1104,8 +1116,9 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
}
Future<void> _onToggleHardMute() async {
// Don't allow manual unmute when talk-power-muted.
if (_hardMuteByTalkPower && _hardMute) {
// Don't allow manual unmute while another policy owns the local
// transmit clamp.
if ((_hardMuteByTalkPower || _hardMuteByOutputMute) && _hardMute) {
return;
}
final next = !_hardMute;
@@ -1200,6 +1213,8 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
pttBoundInputClass: _pttBoundInputClass,
isTouchOnly: isTouchOnlyPttHost,
initialAudioConfig: audioConfig,
onnxRuntimeAvailable:
StartupDependencyScope.maybeOf(context)?.hasOnnxRuntime ?? true,
onModeChanged: (mode) async {
try {
await rust.setTransmitMode(mode: mode);
@@ -1241,6 +1256,8 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
initialMode: _transmitMode,
initialReleaseTailMs: _releaseTailMs,
initialAudioConfig: audioConfig,
onnxRuntimeAvailable:
StartupDependencyScope.maybeOf(ctx)?.hasOnnxRuntime ?? true,
pttLevel: _pttLevel,
pttBackendId: _pttBackendId,
pttBoundInputClass: _pttBoundInputClass,
@@ -1399,6 +1416,10 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
_audioStats = null;
_inputMuted = false;
_outputMuted = false;
_hardMute = false;
_hardMuteByPermission = false;
_hardMuteByTalkPower = false;
_hardMuteByOutputMute = false;
_inChannel = false;
_currentVoiceChannelId = null;
_pendingVoiceChannelId = null;
@@ -1418,6 +1439,7 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
String clientName = '',
}) async {
final initialSnapshot = _snapshot!;
_dismissChatPreviewSnackBar();
setState(() {
_chatUnread = 0;
_chatOpen = true;
@@ -1500,36 +1522,50 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
if (_pokeSnackBarPokes.value.isNotEmpty) return;
final messenger = ScaffoldMessenger.of(context);
messenger.hideCurrentSnackBar();
messenger.showSnackBar(
_chatPreviewSnackBarVisible = true;
void openChatFromPreview() {
_dismissChatPreviewSnackBar();
unawaited(
_onOpenChat(
target: target,
clientName: _chatClientNameForTarget(target, senderName),
),
);
}
final controller = messenger.showSnackBar(
SnackBar(
behavior: SnackBarBehavior.floating,
margin: _chatSnackBarMargin(),
duration: const Duration(seconds: 7),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'$senderName (${_chatTargetLabel(target)})',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w600),
),
const SizedBox(height: 4),
Text(message, maxLines: 3, overflow: TextOverflow.ellipsis),
],
),
action: SnackBarAction(
label: 'Open',
onPressed: () => unawaited(
_onOpenChat(
target: target,
clientName: _chatClientNameForTarget(target, senderName),
),
duration: const Duration(seconds: 4),
content: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: openChatFromPreview,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'$senderName (${_chatTargetLabel(target)})',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w600),
),
const SizedBox(height: 4),
Text(message, maxLines: 3, overflow: TextOverflow.ellipsis),
],
),
),
action: SnackBarAction(label: 'Open', onPressed: openChatFromPreview),
),
);
controller.closed.then((_) => _chatPreviewSnackBarVisible = false);
}
void _dismissChatPreviewSnackBar() {
if (!_chatPreviewSnackBarVisible || !mounted) return;
_chatPreviewSnackBarVisible = false;
ScaffoldMessenger.of(context).removeCurrentSnackBar();
}
EdgeInsetsGeometry _chatSnackBarMargin() {
@@ -1589,6 +1625,14 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
if (own == null) return;
_inputMuted = own.inputMuted;
_outputMuted = own.outputMuted;
_hardMuteByOutputMute = own.outputMuted;
if (own.outputMuted) {
_hardMute = true;
} else if (!_inputMuted &&
!_hardMuteByPermission &&
!_hardMuteByTalkPower) {
_hardMute = false;
}
if (!_voiceStateInitialized || _currentVoiceChannelId == null) {
_currentVoiceChannelId = own.channelId;
_pendingVoiceChannelId = null;
@@ -1607,7 +1651,7 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
rust.setInputMuted(muted: true);
} else if (own.talkPowerOk && _hardMuteByTalkPower) {
_hardMuteByTalkPower = false;
if (!_hardMuteByPermission) {
if (!_hardMuteByPermission && !_hardMuteByOutputMute && !_inputMuted) {
_hardMute = false;
rust.setHardMute(muted: false);
rust.setInputMuted(muted: false);
@@ -1622,12 +1666,12 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
Future<void> _onShowDiagnostics(BuildContext context) async {
final l10n = AppL10n.of(context);
final rustText = rust.exportDiagnostics();
final rustText = await rust.exportDiagnostics();
if (!context.mounted) return;
final uiText = _uiDiagnostics.isEmpty
? 'UI diagnostics: none'
: ['UI diagnostics:', ..._uiDiagnostics].join('\n');
final text = '$uiText\n\nRust diagnostics:\n$rustText';
if (!mounted) return;
await showDialog<void>(
context: context,
builder: (ctx) => AlertDialog(