voice improvments

This commit is contained in:
callebtc 2026-07-29 02:12:55 +02:00
parent b3b478be10
commit 2e3f7bf827
5 changed files with 69 additions and 8 deletions

View File

@ -94,8 +94,13 @@ fun ChatActionBar(onKeyboard: () -> Unit, voice: VoiceNoteController, modifier:
} else {
permissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
}
tryAwaitRelease()
voice.stop(send = true)
// Only a clean release stops here. If the scroll parent steals
// the pointer mid-drag (cancel), keep recording — the
// screen-level release watcher in ChatScaffold stops when the
// finger actually lifts, anywhere on the screen.
if (tryAwaitRelease()) {
voice.stop(send = true)
}
}
)
},
@ -167,7 +172,7 @@ fun VoiceRecordOverlay(voice: VoiceNoteController) {
modifier = Modifier.padding(top = 10.dp)
)
Text(
text = "Release to send",
text = "Lift finger to send",
style = ChatVisualTokens.SystemActionStyle,
color = palette.textTertiary,
textAlign = TextAlign.Center,

View File

@ -22,8 +22,10 @@ import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.changedToUp
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.wear.compose.foundation.lazy.TransformingLazyColumn
@ -61,16 +63,16 @@ fun ChatScaffold(
header: @Composable (expanded: Boolean) -> Unit,
actionBar: @Composable () -> Unit
) {
val haptics = LocalHapticFeedback.current
val columnState = rememberTransformingLazyColumnState()
// Haptics on incoming messages.
val context = LocalContext.current
var previousCount by remember { mutableStateOf(messages.size) }
LaunchedEffect(messages.size) {
if (messages.size > previousCount) {
val last = messages.lastOrNull()
if (last != null && last.senderPeerID != myPeerID) {
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
WearHaptics.knock(context)
}
}
previousCount = messages.size
@ -139,7 +141,24 @@ private fun ChatBody(
) {
val palette = LocalBitchatPalette.current
val transformationSpec = rememberTransformationSpec()
Box(modifier = modifier.fillMaxSize()) {
Box(
modifier = modifier
.fillMaxSize()
// Push-to-talk release is tracked globally: once recording, lifting the finger
// ANYWHERE on the screen stops and sends. On a 1.4" round screen it is too easy
// to drift off the small mic button (the scrollable parent steals the pointer
// mid-drag), so the button alone must not own the release.
.pointerInput(voice) {
awaitPointerEventScope {
while (true) {
val event = awaitPointerEvent(PointerEventPass.Initial)
if (voice.recording && event.changes.any { it.changedToUp() }) {
voice.stop(send = true)
}
}
}
}
) {
ScreenScaffold(scrollState = columnState) {
TransformingLazyColumn(
state = columnState,

View File

@ -49,6 +49,7 @@ import com.bitchat.watch.ui.theme.LocalBitchatPalette
@Composable
fun TextInputScreen(onSend: (String) -> Unit) {
val palette = LocalBitchatPalette.current
val context = androidx.compose.ui.platform.LocalContext.current
var text by remember { mutableStateOf("") }
val focusRequester = remember { FocusRequester() }
val keyboardController = androidx.compose.ui.platform.LocalSoftwareKeyboardController.current
@ -61,6 +62,7 @@ fun TextInputScreen(onSend: (String) -> Unit) {
?.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)
?.firstOrNull()
if (!spoken.isNullOrBlank()) {
WearHaptics.tick(context)
onSend(spoken.trim())
}
}
@ -70,6 +72,7 @@ fun TextInputScreen(onSend: (String) -> Unit) {
val trimmed = text.trim()
if (trimmed.isNotEmpty()) {
keyboardController?.hide()
WearHaptics.tick(context)
onSend(trimmed)
text = ""
}

View File

@ -50,6 +50,7 @@ class VoiceNoteController(
elapsedMs = 0L
liveSamples = FloatArray(LIVE_BARS)
recording = true
WearHaptics.knock(context)
pollJob = scope.launch {
while (true) {
delay(AMPLITUDE_POLL_MS)
@ -68,6 +69,7 @@ class VoiceNoteController(
fun stop(send: Boolean) {
if (!recording) return
recording = false
WearHaptics.click(context)
pollJob?.cancel()
pollJob = null
val file = recorder.stop()

View File

@ -0,0 +1,32 @@
package com.bitchat.watch.ui
import android.content.Context
import android.os.Build
import android.os.VibrationEffect
import android.os.Vibrator
import android.os.VibratorManager
/**
* Tactile accents for the watch's important moments. Uses predefined vibration effects so
* the feel stays consistent with the rest of Wear OS.
*/
object WearHaptics {
/** Firm knock: recording started, message received. */
fun knock(context: Context) = vibrate(context, VibrationEffect.EFFECT_HEAVY_CLICK)
/** Crisp click: recording stopped, message sent. */
fun click(context: Context) = vibrate(context, VibrationEffect.EFFECT_CLICK)
/** Light tick: small confirmations. */
fun tick(context: Context) = vibrate(context, VibrationEffect.EFFECT_TICK)
private fun vibrate(context: Context, effect: Int) {
val vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
context.getSystemService(VibratorManager::class.java)?.defaultVibrator
} else {
@Suppress("DEPRECATION")
context.getSystemService(Vibrator::class.java)
} ?: return
vibrator.vibrate(VibrationEffect.createPredefined(effect))
}
}