mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-08-08 06:46:11 +00:00
voice improvments
This commit is contained in:
parent
b3b478be10
commit
2e3f7bf827
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 = ""
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
32
wear/src/main/java/com/bitchat/watch/ui/WearHaptics.kt
Normal file
32
wear/src/main/java/com/bitchat/watch/ui/WearHaptics.kt
Normal 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))
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user