mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-08-29 07:16:08 +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 {
|
} else {
|
||||||
permissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
|
permissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
|
||||||
}
|
}
|
||||||
tryAwaitRelease()
|
// Only a clean release stops here. If the scroll parent steals
|
||||||
voice.stop(send = true)
|
// 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)
|
modifier = Modifier.padding(top = 10.dp)
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "Release to send",
|
text = "Lift finger to send",
|
||||||
style = ChatVisualTokens.SystemActionStyle,
|
style = ChatVisualTokens.SystemActionStyle,
|
||||||
color = palette.textTertiary,
|
color = palette.textTertiary,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
|
|||||||
@ -22,8 +22,10 @@ import androidx.compose.runtime.snapshotFlow
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
import androidx.compose.ui.input.pointer.PointerEventPass
|
||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
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.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.wear.compose.foundation.lazy.TransformingLazyColumn
|
import androidx.wear.compose.foundation.lazy.TransformingLazyColumn
|
||||||
@ -61,16 +63,16 @@ fun ChatScaffold(
|
|||||||
header: @Composable (expanded: Boolean) -> Unit,
|
header: @Composable (expanded: Boolean) -> Unit,
|
||||||
actionBar: @Composable () -> Unit
|
actionBar: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
val haptics = LocalHapticFeedback.current
|
|
||||||
val columnState = rememberTransformingLazyColumnState()
|
val columnState = rememberTransformingLazyColumnState()
|
||||||
|
|
||||||
// Haptics on incoming messages.
|
// Haptics on incoming messages.
|
||||||
|
val context = LocalContext.current
|
||||||
var previousCount by remember { mutableStateOf(messages.size) }
|
var previousCount by remember { mutableStateOf(messages.size) }
|
||||||
LaunchedEffect(messages.size) {
|
LaunchedEffect(messages.size) {
|
||||||
if (messages.size > previousCount) {
|
if (messages.size > previousCount) {
|
||||||
val last = messages.lastOrNull()
|
val last = messages.lastOrNull()
|
||||||
if (last != null && last.senderPeerID != myPeerID) {
|
if (last != null && last.senderPeerID != myPeerID) {
|
||||||
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
|
WearHaptics.knock(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
previousCount = messages.size
|
previousCount = messages.size
|
||||||
@ -139,7 +141,24 @@ private fun ChatBody(
|
|||||||
) {
|
) {
|
||||||
val palette = LocalBitchatPalette.current
|
val palette = LocalBitchatPalette.current
|
||||||
val transformationSpec = rememberTransformationSpec()
|
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) {
|
ScreenScaffold(scrollState = columnState) {
|
||||||
TransformingLazyColumn(
|
TransformingLazyColumn(
|
||||||
state = columnState,
|
state = columnState,
|
||||||
|
|||||||
@ -49,6 +49,7 @@ import com.bitchat.watch.ui.theme.LocalBitchatPalette
|
|||||||
@Composable
|
@Composable
|
||||||
fun TextInputScreen(onSend: (String) -> Unit) {
|
fun TextInputScreen(onSend: (String) -> Unit) {
|
||||||
val palette = LocalBitchatPalette.current
|
val palette = LocalBitchatPalette.current
|
||||||
|
val context = androidx.compose.ui.platform.LocalContext.current
|
||||||
var text by remember { mutableStateOf("") }
|
var text by remember { mutableStateOf("") }
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
val keyboardController = androidx.compose.ui.platform.LocalSoftwareKeyboardController.current
|
val keyboardController = androidx.compose.ui.platform.LocalSoftwareKeyboardController.current
|
||||||
@ -61,6 +62,7 @@ fun TextInputScreen(onSend: (String) -> Unit) {
|
|||||||
?.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)
|
?.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)
|
||||||
?.firstOrNull()
|
?.firstOrNull()
|
||||||
if (!spoken.isNullOrBlank()) {
|
if (!spoken.isNullOrBlank()) {
|
||||||
|
WearHaptics.tick(context)
|
||||||
onSend(spoken.trim())
|
onSend(spoken.trim())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -70,6 +72,7 @@ fun TextInputScreen(onSend: (String) -> Unit) {
|
|||||||
val trimmed = text.trim()
|
val trimmed = text.trim()
|
||||||
if (trimmed.isNotEmpty()) {
|
if (trimmed.isNotEmpty()) {
|
||||||
keyboardController?.hide()
|
keyboardController?.hide()
|
||||||
|
WearHaptics.tick(context)
|
||||||
onSend(trimmed)
|
onSend(trimmed)
|
||||||
text = ""
|
text = ""
|
||||||
}
|
}
|
||||||
|
|||||||
@ -50,6 +50,7 @@ class VoiceNoteController(
|
|||||||
elapsedMs = 0L
|
elapsedMs = 0L
|
||||||
liveSamples = FloatArray(LIVE_BARS)
|
liveSamples = FloatArray(LIVE_BARS)
|
||||||
recording = true
|
recording = true
|
||||||
|
WearHaptics.knock(context)
|
||||||
pollJob = scope.launch {
|
pollJob = scope.launch {
|
||||||
while (true) {
|
while (true) {
|
||||||
delay(AMPLITUDE_POLL_MS)
|
delay(AMPLITUDE_POLL_MS)
|
||||||
@ -68,6 +69,7 @@ class VoiceNoteController(
|
|||||||
fun stop(send: Boolean) {
|
fun stop(send: Boolean) {
|
||||||
if (!recording) return
|
if (!recording) return
|
||||||
recording = false
|
recording = false
|
||||||
|
WearHaptics.click(context)
|
||||||
pollJob?.cancel()
|
pollJob?.cancel()
|
||||||
pollJob = null
|
pollJob = null
|
||||||
val file = recorder.stop()
|
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