From cf1ec623e482438738ac601f9b3391218cb23705 Mon Sep 17 00:00:00 2001 From: denis0001-dev Date: Sat, 7 Feb 2026 20:41:42 +0300 Subject: [PATCH] Add haptic feedback Signed-off-by: denis0001-dev --- app/android/src/main/AndroidManifest.xml | 1 + .../ru/fromchat/ui/BackHandler.android.kt | 19 +++++++++++++++++++ .../kotlin/ru/fromchat/ui/BackHandler.kt | 3 +++ .../kotlin/ru/fromchat/ui/HapticFeedback.kt | 10 ++++++++++ .../ru/fromchat/ui/HapticFeedbackEvent.kt | 7 +++++++ .../kotlin/ru/fromchat/ui/chat/ChatScreen.kt | 4 ++++ .../ru/fromchat/ui/dm/DmContainerScreen.kt | 19 ++++++++++++++++--- .../kotlin/ru/fromchat/ui/BackHandler.ios.kt | 3 +++ 8 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 app/shared/src/commonMain/kotlin/ru/fromchat/ui/HapticFeedback.kt create mode 100644 app/shared/src/commonMain/kotlin/ru/fromchat/ui/HapticFeedbackEvent.kt diff --git a/app/android/src/main/AndroidManifest.xml b/app/android/src/main/AndroidManifest.xml index 720e2ad..6474f42 100644 --- a/app/android/src/main/AndroidManifest.xml +++ b/app/android/src/main/AndroidManifest.xml @@ -19,6 +19,7 @@ diff --git a/app/shared/src/androidMain/kotlin/ru/fromchat/ui/BackHandler.android.kt b/app/shared/src/androidMain/kotlin/ru/fromchat/ui/BackHandler.android.kt index 08c562d..801a808 100644 --- a/app/shared/src/androidMain/kotlin/ru/fromchat/ui/BackHandler.android.kt +++ b/app/shared/src/androidMain/kotlin/ru/fromchat/ui/BackHandler.android.kt @@ -1,9 +1,28 @@ package ru.fromchat.ui +import android.view.HapticFeedbackConstants import androidx.activity.compose.BackHandler as AndroidBackHandler import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalView @Composable actual fun BackHandler(enabled: Boolean, onBack: () -> Unit) { AndroidBackHandler(enabled = enabled, onBack = onBack) } + +@Composable +actual fun rememberHapticFeedbackInternal(): (Int) -> Unit { + val view = LocalView.current + return remember(view) { + { ordinal -> + val constant = when (ordinal) { + HapticFeedbackEvent.ProfileOpened.ordinal -> HapticFeedbackConstants.CLOCK_TICK + HapticFeedbackEvent.ProfileClosed.ordinal -> HapticFeedbackConstants.CLOCK_TICK + HapticFeedbackEvent.MessageSent.ordinal -> HapticFeedbackConstants.CONFIRM + else -> HapticFeedbackConstants.CLOCK_TICK + } + view.performHapticFeedback(constant) + } + } +} diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/BackHandler.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/BackHandler.kt index 8f8e135..91eaa79 100644 --- a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/BackHandler.kt +++ b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/BackHandler.kt @@ -9,3 +9,6 @@ import androidx.compose.runtime.Composable */ @Composable expect fun BackHandler(enabled: Boolean, onBack: () -> Unit) + +@Composable +expect fun rememberHapticFeedbackInternal(): (Int) -> Unit diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/HapticFeedback.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/HapticFeedback.kt new file mode 100644 index 0000000..7031c6f --- /dev/null +++ b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/HapticFeedback.kt @@ -0,0 +1,10 @@ +package ru.fromchat.ui + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember + +@Composable +fun rememberHapticFeedback(): (HapticFeedbackEvent) -> Unit { + val h = rememberHapticFeedbackInternal() + return remember(h) { { event: HapticFeedbackEvent -> h(event.ordinal) } } +} diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/HapticFeedbackEvent.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/HapticFeedbackEvent.kt new file mode 100644 index 0000000..54bc06d --- /dev/null +++ b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/HapticFeedbackEvent.kt @@ -0,0 +1,7 @@ +package ru.fromchat.ui + +enum class HapticFeedbackEvent { + ProfileOpened, + ProfileClosed, + MessageSent +} diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/chat/ChatScreen.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/chat/ChatScreen.kt index ffb267f..aa2c87c 100644 --- a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/chat/ChatScreen.kt +++ b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/chat/ChatScreen.kt @@ -72,7 +72,9 @@ import ru.fromchat.api.WebSocketMessage import ru.fromchat.api.WebSocketUpdatesData import ru.fromchat.back import ru.fromchat.core.Logger +import ru.fromchat.ui.HapticFeedbackEvent import ru.fromchat.ui.LocalNavController +import ru.fromchat.ui.rememberHapticFeedback import ru.fromchat.ui.scaleOnPress @OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class) @@ -116,6 +118,7 @@ fun ChatScreen( val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() val listState = rememberLazyListState() val scope = rememberCoroutineScope() + val haptic = rememberHapticFeedback() val navController = LocalNavController.current val profileUserId = panelState.profileUserId val hazeState = rememberHazeState(blurEnabled = true) @@ -389,6 +392,7 @@ fun ChatScreen( scope.launch { panel.sendMessageWithImmediateDisplay(text, replyTo?.id) replyTo = null + haptic(HapticFeedbackEvent.MessageSent) } } inputText = "" diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/dm/DmContainerScreen.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/dm/DmContainerScreen.kt index c6507bb..c8cd055 100644 --- a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/dm/DmContainerScreen.kt +++ b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/dm/DmContainerScreen.kt @@ -15,6 +15,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import ru.fromchat.ui.BackHandler +import ru.fromchat.ui.HapticFeedbackEvent +import ru.fromchat.ui.rememberHapticFeedback import ru.fromchat.ui.profile.ProfileScreen private const val TRANSITION_MS = 320 @@ -27,11 +29,13 @@ fun DmContainerScreen( ) { var showProfile by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() + val haptic = rememberHapticFeedback() val panel = remember(otherUserId) { DmPanelCache.getOrCreate(otherUserId, scope) } BackHandler(enabled = showProfile) { + haptic(HapticFeedbackEvent.ProfileClosed) showProfile = false } @@ -51,7 +55,10 @@ fun DmContainerScreen( DmScreen( panel = panel, modifier = Modifier.fillMaxSize(), - onTitleClick = { showProfile = true }, + onTitleClick = { + haptic(HapticFeedbackEvent.ProfileOpened) + showProfile = true + }, sharedTransitionScope = this@SharedTransitionLayout, animatedVisibilityScope = this@AnimatedContent, sharedAvatarKey = sharedAvatarKey @@ -59,8 +66,14 @@ fun DmContainerScreen( } else { ProfileScreen( userId = otherUserId, - onBack = { showProfile = false }, - onChat = { _ -> showProfile = false }, + onBack = { + haptic(HapticFeedbackEvent.ProfileClosed) + showProfile = false + }, + onChat = { _ -> + haptic(HapticFeedbackEvent.ProfileClosed) + showProfile = false + }, modifier = Modifier.fillMaxSize(), sharedTransitionScope = this@SharedTransitionLayout, animatedVisibilityScope = this@AnimatedContent, diff --git a/app/shared/src/iosMain/kotlin/ru/fromchat/ui/BackHandler.ios.kt b/app/shared/src/iosMain/kotlin/ru/fromchat/ui/BackHandler.ios.kt index 657e09c..3a52341 100644 --- a/app/shared/src/iosMain/kotlin/ru/fromchat/ui/BackHandler.ios.kt +++ b/app/shared/src/iosMain/kotlin/ru/fromchat/ui/BackHandler.ios.kt @@ -6,3 +6,6 @@ import androidx.compose.runtime.Composable actual fun BackHandler(enabled: Boolean, onBack: () -> Unit) { // iOS has no system back button; back is handled by navigation. } + +@Composable +actual fun rememberHapticFeedbackInternal(): (Int) -> Unit = { }