diff --git a/core/common/src/main/kotlin/dev/meloda/fast/common/model/LogLevel.kt b/core/common/src/main/kotlin/dev/meloda/fast/common/model/LogLevel.kt new file mode 100644 index 00000000..43b38035 --- /dev/null +++ b/core/common/src/main/kotlin/dev/meloda/fast/common/model/LogLevel.kt @@ -0,0 +1,13 @@ +package dev.meloda.fast.common.model + +enum class LogLevel(val value: Int) { + NONE(0), + BASIC(1), + HEADERS(2), + BODY(3); + + companion object { + fun parse(value: Int): LogLevel = entries.firstOrNull { it.value == value } + ?: throw IllegalArgumentException("Unknown log level with value: $value") + } +} diff --git a/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/AppSettings.kt b/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/AppSettings.kt index fff17db4..e74d501f 100644 --- a/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/AppSettings.kt +++ b/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/AppSettings.kt @@ -3,6 +3,7 @@ package dev.meloda.fast.datastore import android.content.SharedPreferences import androidx.core.content.edit import dev.meloda.fast.common.model.DarkMode +import dev.meloda.fast.common.model.LogLevel import kotlin.properties.Delegates import kotlin.reflect.KClass @@ -187,6 +188,20 @@ object AppSettings { ) set(value) = put(SettingsKeys.KEY_APPEARANCE_SHOW_TIME_IN_ACTION_MESSAGES, value) + var enableHaptic: Boolean + get() = get( + SettingsKeys.KEY_DEBUG_ENABLE_HAPTIC, + true + ) + set(value) = put(SettingsKeys.KEY_DEBUG_ENABLE_HAPTIC, value) + + var networkLogLevel: LogLevel + get() = get( + SettingsKeys.KEY_DEBUG_NETWORK_LOG_LEVEL, + SettingsKeys.DEFAULT_NETWORK_LOG_LEVEL + ).let(LogLevel::parse) + set(level) = put(SettingsKeys.KEY_DEBUG_NETWORK_LOG_LEVEL, level.value) + var showDebugCategory: Boolean get() = get( SettingsKeys.KEY_SHOW_DEBUG_CATEGORY, diff --git a/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/SettingsKeys.kt b/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/SettingsKeys.kt index cd0f34a4..ac05441b 100644 --- a/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/SettingsKeys.kt +++ b/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/SettingsKeys.kt @@ -45,6 +45,9 @@ object SettingsKeys { const val KEY_DEBUG_SHOW_CRASH_ALERT = "debug_show_crash_alert" const val KEY_DEBUG_HIDE_DEBUG_LIST = "debug_hide_debug_list" const val KEY_ENABLE_ANIMATIONS_IN_MESSAGES = "debug_enable_animations_in_messages" + const val KEY_DEBUG_ENABLE_HAPTIC = "debug_enable_haptic" + const val KEY_DEBUG_NETWORK_LOG_LEVEL = "debug_network_log_level" + const val DEFAULT_NETWORK_LOG_LEVEL = 0 const val KEY_SHOW_DEBUG_CATEGORY = "show_debug_category" diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts index 167d47b7..53bdfd6e 100644 --- a/core/network/build.gradle.kts +++ b/core/network/build.gradle.kts @@ -14,6 +14,7 @@ android { dependencies { api(projects.core.common) api(projects.core.model) + api(projects.core.datastore) implementation(libs.moshi.kotlin) implementation(libs.koin.android) diff --git a/core/network/src/main/kotlin/dev/meloda/fast/network/di/NetworkModule.kt b/core/network/src/main/kotlin/dev/meloda/fast/network/di/NetworkModule.kt index 51dab6f8..88501b6e 100644 --- a/core/network/src/main/kotlin/dev/meloda/fast/network/di/NetworkModule.kt +++ b/core/network/src/main/kotlin/dev/meloda/fast/network/di/NetworkModule.kt @@ -6,6 +6,8 @@ import com.slack.eithernet.ApiResultCallAdapterFactory import com.slack.eithernet.ApiResultConverterFactory import com.squareup.moshi.Moshi import dev.meloda.fast.common.AppConstants +import dev.meloda.fast.common.model.LogLevel +import dev.meloda.fast.datastore.AppSettings import dev.meloda.fast.network.JsonConverter import dev.meloda.fast.network.MoshiConverter import dev.meloda.fast.network.OAuthResultCallFactory @@ -55,7 +57,12 @@ val networkModule = module { .followSslRedirects(true) .addInterceptor( HttpLoggingInterceptor().apply { - level = HttpLoggingInterceptor.Level.BODY + level = when (AppSettings.Debug.networkLogLevel) { + LogLevel.NONE -> HttpLoggingInterceptor.Level.NONE + LogLevel.BASIC -> HttpLoggingInterceptor.Level.BASIC + LogLevel.HEADERS -> HttpLoggingInterceptor.Level.HEADERS + LogLevel.BODY -> HttpLoggingInterceptor.Level.BODY + } } ) .build() diff --git a/core/ui/src/main/kotlin/dev/meloda/fast/ui/util/ImmutableList.kt b/core/ui/src/main/kotlin/dev/meloda/fast/ui/util/ImmutableList.kt index a9a2b631..65128318 100644 --- a/core/ui/src/main/kotlin/dev/meloda/fast/ui/util/ImmutableList.kt +++ b/core/ui/src/main/kotlin/dev/meloda/fast/ui/util/ImmutableList.kt @@ -47,6 +47,8 @@ class ImmutableList(val values: List) : Iterable { return single } + val size: Int get() = values.size + companion object { fun copyOf(collection: Collection): ImmutableList = ImmutableList(collection.toList()) diff --git a/feature/auth/build.gradle.kts b/feature/auth/build.gradle.kts index 02b0a99a..a65db56e 100644 --- a/feature/auth/build.gradle.kts +++ b/feature/auth/build.gradle.kts @@ -4,6 +4,8 @@ import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties val sdkPackage: String = getLocalProperty("sdkPackage", "\"\"") val sdkFingerprint: String = getLocalProperty("sdkFingerprint", "\"\"") +val debugToken: String = getLocalProperty("debugToken", "\"\"") + fun getLocalProperty(key: String, defValue: String): String { return gradleLocalProperties(rootDir, providers).getProperty(key, defValue) } @@ -32,6 +34,14 @@ androidComponents { comment = "sdkFingerprint for VK" ) ) + put( + "debugToken", + BuildConfigField( + type = "String", + value = debugToken, + comment = "debug token for authorization" + ) + ) } } } diff --git a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/presentation/LogoScreen.kt b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/presentation/LogoScreen.kt index 6db9ac34..d777b0fc 100644 --- a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/presentation/LogoScreen.kt +++ b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/presentation/LogoScreen.kt @@ -203,7 +203,7 @@ fun SignInAlert( onConfirmClick: (token: String) -> Unit ) { var tokenText by rememberSaveable { - mutableStateOf("") + mutableStateOf(BuildConfig.debugToken) } val maxWidthModifier = Modifier.fillMaxWidth() diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/ConversationsViewModel.kt b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/ConversationsViewModel.kt index 78cc31d3..2967eb9d 100644 --- a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/ConversationsViewModel.kt +++ b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/ConversationsViewModel.kt @@ -18,6 +18,7 @@ import dev.meloda.fast.data.State import dev.meloda.fast.data.processState import dev.meloda.fast.datastore.UserSettings import dev.meloda.fast.domain.ConversationsUseCase +import dev.meloda.fast.domain.LoadUserByIdUseCase import dev.meloda.fast.domain.LongPollUpdatesParser import dev.meloda.fast.domain.MessagesUseCase import dev.meloda.fast.model.BaseError @@ -65,6 +66,7 @@ interface ConversationsViewModel { class ConversationsViewModelImpl( updatesParser: LongPollUpdatesParser, private val conversationsUseCase: ConversationsUseCase, + private val loadUserByIdUseCase: LoadUserByIdUseCase, private val messagesUseCase: MessagesUseCase, private val resources: Resources, private val userSettings: UserSettings @@ -97,6 +99,8 @@ class ConversationsViewModelImpl( updatesParser.onConversationPinStateChanged(::handlePinStateChanged) updatesParser.onInteractions(::handleInteraction) + loadProfile() + loadConversations() } @@ -223,6 +227,24 @@ class ConversationsViewModelImpl( screenState.setValue { old -> old.copy(showOptions = newShowOptions) } } + private fun loadProfile() { + loadUserByIdUseCase(userId = null) + .listenValue(viewModelScope) { state -> + state.processState( + error = { error -> + + }, + success = { response -> + val user = response ?: return@listenValue + + screenState.setValue { old -> + old.copy(profileImageUrl = user.photo100) + } + } + ) + } + } + private fun loadConversations( offset: Int = currentOffset.value ) { diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/model/ConversationsScreenState.kt b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/model/ConversationsScreenState.kt index 1af63362..38e0733e 100644 --- a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/model/ConversationsScreenState.kt +++ b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/model/ConversationsScreenState.kt @@ -8,7 +8,8 @@ data class ConversationsScreenState( val conversations: List, val isLoading: Boolean, val isPaginating: Boolean, - val isPaginationExhausted: Boolean + val isPaginationExhausted: Boolean, + val profileImageUrl: String? ) { companion object { @@ -17,7 +18,8 @@ data class ConversationsScreenState( conversations = emptyList(), isLoading = true, isPaginating = false, - isPaginationExhausted = false + isPaginationExhausted = false, + profileImageUrl = null ) } } diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/navigation/ConversationsNavigation.kt b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/navigation/ConversationsNavigation.kt index e53b9f73..66805ed8 100644 --- a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/navigation/ConversationsNavigation.kt +++ b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/navigation/ConversationsNavigation.kt @@ -26,7 +26,7 @@ fun NavGraphBuilder.conversationsScreen( ConversationsRoute( onError = onError, onConversationItemClicked = onConversationItemClicked, - onPhotoClicked = onPhotoClicked, + onConversationPhotoClicked = onPhotoClicked, viewModel = viewModel ) } diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsScreen.kt b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsScreen.kt index b6bb91fb..bb9eec82 100644 --- a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsScreen.kt +++ b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsScreen.kt @@ -9,6 +9,7 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.slideIn import androidx.compose.animation.slideOut +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets @@ -19,17 +20,17 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.MoreVert import androidx.compose.material.icons.rounded.Refresh import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold @@ -51,6 +52,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.rotate import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView @@ -63,6 +65,7 @@ import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.core.view.HapticFeedbackConstantsCompat import androidx.lifecycle.compose.collectAsStateWithLifecycle +import coil.compose.AsyncImage import coil.imageLoader import coil.request.ImageRequest import dev.chrisbanes.haze.haze @@ -73,6 +76,7 @@ import dev.meloda.fast.conversations.ConversationsViewModel import dev.meloda.fast.conversations.model.ConversationOption import dev.meloda.fast.conversations.model.ConversationsScreenState import dev.meloda.fast.conversations.model.UiConversation +import dev.meloda.fast.datastore.AppSettings import dev.meloda.fast.datastore.UserSettings import dev.meloda.fast.model.BaseError import dev.meloda.fast.ui.components.ErrorView @@ -90,7 +94,7 @@ import dev.meloda.fast.ui.R as UiR fun ConversationsRoute( onError: (BaseError) -> Unit, onConversationItemClicked: (conversationId: Int) -> Unit, - onPhotoClicked: (url: String) -> Unit, + onConversationPhotoClicked: (url: String) -> Unit, viewModel: ConversationsViewModel ) { val context = LocalContext.current @@ -129,7 +133,7 @@ fun ConversationsRoute( onPaginationConditionsMet = viewModel::onPaginationConditionsMet, onRefreshDropdownItemClicked = viewModel::onRefresh, onRefresh = viewModel::onRefresh, - onPhotoClicked = onPhotoClicked + onConversationPhotoClicked = onConversationPhotoClicked ) @@ -156,7 +160,7 @@ fun ConversationsScreen( onPaginationConditionsMet: () -> Unit = {}, onRefreshDropdownItemClicked: () -> Unit = {}, onRefresh: () -> Unit = {}, - onPhotoClicked: (url: String) -> Unit = {} + onConversationPhotoClicked: (url: String) -> Unit = {} ) { val view = LocalView.current val currentTheme = LocalThemeConfig.current @@ -220,23 +224,32 @@ fun ConversationsScreen( ) }, actions = { - IconButton( - onClick = { - dropDownMenuExpanded = true - } - ) { - Icon( - imageVector = Icons.Outlined.MoreVert, - contentDescription = "Options button" - ) - } + AsyncImage( + model = screenState.profileImageUrl, + contentDescription = "Profile Image", + modifier = Modifier + .padding(end = 12.dp) + .size(32.dp) + .clip(CircleShape) + .clickable { dropDownMenuExpanded = true }, + placeholder = painterResource(id = UiR.drawable.ic_account_circle_cut) + ) + +// IconButton( +// onClick = { +// dropDownMenuExpanded = true +// } +// ) { +// Icon( +// imageVector = Icons.Outlined.MoreVert, +// contentDescription = "Options button" +// ) +// } DropdownMenu( modifier = Modifier.defaultMinSize(minWidth = 140.dp), expanded = dropDownMenuExpanded, - onDismissRequest = { - dropDownMenuExpanded = false - }, + onDismissRequest = { dropDownMenuExpanded = false }, offset = DpOffset(x = (-4).dp, y = (-60).dp) ) { DropdownMenuItem( @@ -293,8 +306,9 @@ fun ConversationsScreen( ) { FloatingActionButton( onClick = { - view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT) - + if (AppSettings.Debug.enableHaptic) { + view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT) + } scope.launch { for (i in 20 downTo 0 step 4) { rotation.animateTo( @@ -372,7 +386,7 @@ fun ConversationsScreen( }.fillMaxSize(), onOptionClicked = onOptionClicked, padding = padding, - onPhotoClicked = onPhotoClicked + onPhotoClicked = onConversationPhotoClicked ) } } diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt index 0fa197f6..f2004a86 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt @@ -2,6 +2,7 @@ package dev.meloda.fast.messageshistory import android.content.SharedPreferences import android.util.Log +import androidx.compose.ui.text.input.TextFieldValue import androidx.core.content.edit import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel @@ -52,7 +53,7 @@ interface MessagesHistoryViewModel { fun onRefresh() fun onAttachmentButtonClicked() - fun onMessageInputChanged(newText: String) + fun onMessageInputChanged(newText: TextFieldValue) fun onEmojiButtonClicked() fun onActionButtonClicked() @@ -110,11 +111,11 @@ class MessagesHistoryViewModelImpl( } - override fun onMessageInputChanged(newText: String) { + override fun onMessageInputChanged(newText: TextFieldValue) { screenState.setValue { old -> old.copy( message = newText, - actionMode = if (newText.isEmptyOrBlank()) ActionMode.Record + actionMode = if (newText.text.isEmptyOrBlank()) ActionMode.Record else ActionMode.Send ) } @@ -317,7 +318,7 @@ class MessagesHistoryViewModelImpl( } private fun sendMessage() { - lastMessageText = screenState.value.message + lastMessageText = screenState.value.message.text val newMessage = VkMessage( id = -1 - sendingMessages.size, @@ -363,7 +364,7 @@ class MessagesHistoryViewModelImpl( screenState.setValue { old -> old.copy( - message = "", + message = TextFieldValue(), actionMode = ActionMode.Record, messages = listOf(newUiMessage).plus(old.messages) ) diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/MessagesHistoryScreenState.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/MessagesHistoryScreenState.kt index 7264fcd6..349f4301 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/MessagesHistoryScreenState.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/MessagesHistoryScreenState.kt @@ -1,6 +1,7 @@ package dev.meloda.fast.messageshistory.model import androidx.compose.runtime.Immutable +import androidx.compose.ui.text.input.TextFieldValue import dev.meloda.fast.common.model.UiImage import dev.meloda.fast.model.api.domain.VkAttachment @@ -11,7 +12,7 @@ data class MessagesHistoryScreenState( val status: String?, val avatar: UiImage, val messages: List, - val message: String, + val message: TextFieldValue, val attachments: List, val isLoading: Boolean, val isPaginating: Boolean, @@ -26,7 +27,7 @@ data class MessagesHistoryScreenState( status = null, avatar = UiImage.Color(0), messages = emptyList(), - message = "", + message = TextFieldValue(), attachments = emptyList(), isLoading = true, isPaginating = false, diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryScreen.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryScreen.kt index 426bf734..aaaf4e71 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryScreen.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryScreen.kt @@ -72,6 +72,11 @@ import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.core.view.HapticFeedbackConstantsCompat import androidx.lifecycle.compose.collectAsStateWithLifecycle +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.hazeChild +import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi +import dev.chrisbanes.haze.materials.HazeMaterials +import dev.meloda.fast.datastore.AppSettings import dev.meloda.fast.datastore.SettingsKeys import dev.meloda.fast.datastore.UserSettings import dev.meloda.fast.messageshistory.MessagesHistoryViewModel @@ -83,10 +88,6 @@ import dev.meloda.fast.messageshistory.util.indexOfMessageByCmId import dev.meloda.fast.model.BaseError import dev.meloda.fast.ui.theme.LocalThemeConfig import dev.meloda.fast.ui.util.ImmutableList -import dev.chrisbanes.haze.HazeState -import dev.chrisbanes.haze.hazeChild -import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi -import dev.chrisbanes.haze.materials.HazeMaterials import kotlinx.coroutines.launch import org.koin.androidx.compose.koinViewModel import org.koin.compose.koinInject @@ -138,7 +139,7 @@ fun MessagesHistoryScreen( onRefreshDropdownItemClicked: () -> Unit = {}, onToggleAnimationsDropdownItemClicked: (Boolean) -> Unit = {}, onPaginationConditionsMet: () -> Unit = {}, - onMessageInputChanged: (String) -> Unit = {}, + onMessageInputChanged: (TextFieldValue) -> Unit = {}, onAttachmentButtonClicked: () -> Unit = {}, onActionButtonClicked: () -> Unit = {} ) { @@ -367,8 +368,9 @@ fun MessagesHistoryScreen( Column(verticalArrangement = Arrangement.Bottom) { IconButton( onClick = { - view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT) - + if (AppSettings.Debug.enableHaptic) { + view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT) + } scope.launch { for (i in 20 downTo 0 step 4) { rotation.animateTo( @@ -397,15 +399,10 @@ fun MessagesHistoryScreen( } } - var message by remember { mutableStateOf(TextFieldValue(screenState.message)) } - TextField( modifier = Modifier.weight(1f), - value = message, - onValueChange = { newText -> - message = newText - onMessageInputChanged(newText.text) - }, + value = screenState.message, + onValueChange = onMessageInputChanged, colors = TextFieldDefaults.colors( unfocusedContainerColor = Color.Transparent, focusedContainerColor = Color.Transparent, @@ -421,36 +418,59 @@ fun MessagesHistoryScreen( } ) + val scope = rememberCoroutineScope() + val attachmentRotation = remember { Animatable(0f) } + Column(verticalArrangement = Arrangement.Bottom) { - IconButton(onClick = onAttachmentButtonClicked) { + IconButton( + onClick = { + if (AppSettings.Debug.enableHaptic) { + view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT) + } + scope.launch { + for (i in 20 downTo 0 step 4) { + attachmentRotation.animateTo( + targetValue = i.toFloat(), + animationSpec = tween(50) + ) + if (i > 0) { + attachmentRotation.animateTo( + targetValue = -i.toFloat(), + animationSpec = tween(50) + ) + } + } + } + } + ) { Icon( painter = painterResource(id = UiR.drawable.round_attach_file_24), contentDescription = "Add attachment button", tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.rotate(30f) + modifier = Modifier.rotate(30f + attachmentRotation.value) ) } Spacer(modifier = Modifier.height(4.dp)) } - val scope = rememberCoroutineScope() - val rotation = remember { Animatable(0f) } + val micRotation = remember { Animatable(0f) } Column(verticalArrangement = Arrangement.Bottom) { IconButton( onClick = { if (screenState.actionMode == ActionMode.Record) { - view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT) - + if (AppSettings.Debug.enableHaptic) { + view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT) + } scope.launch { for (i in 20 downTo 0 step 4) { - rotation.animateTo( + micRotation.animateTo( targetValue = i.toFloat(), animationSpec = tween(50) ) if (i > 0) { - rotation.animateTo( + micRotation.animateTo( targetValue = -i.toFloat(), animationSpec = tween(50) ) @@ -461,7 +481,7 @@ fun MessagesHistoryScreen( onActionButtonClicked() } }, - modifier = Modifier.rotate(rotation.value) + modifier = Modifier.rotate(micRotation.value) ) { Icon( painter = painterResource( diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/validation/MessagesHistoryValidator.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/validation/MessagesHistoryValidator.kt index 41f2f81d..cb6b386d 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/validation/MessagesHistoryValidator.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/validation/MessagesHistoryValidator.kt @@ -10,7 +10,7 @@ class MessagesHistoryValidator { val results = mutableListOf() results.addIf(MessagesHistoryValidationResult.MessageEmpty) { - screenState.message.isBlank() + screenState.message.text.isBlank() } results.addIf(MessagesHistoryValidationResult.AttachmentsEmpty) { diff --git a/feature/settings/src/main/kotlin/dev/meloda/fast/settings/SettingsViewModel.kt b/feature/settings/src/main/kotlin/dev/meloda/fast/settings/SettingsViewModel.kt index 0942ac86..6ef57d9e 100644 --- a/feature/settings/src/main/kotlin/dev/meloda/fast/settings/SettingsViewModel.kt +++ b/feature/settings/src/main/kotlin/dev/meloda/fast/settings/SettingsViewModel.kt @@ -10,8 +10,10 @@ import dev.meloda.fast.common.LongPollController import dev.meloda.fast.common.extensions.findWithIndex import dev.meloda.fast.common.extensions.setValue import dev.meloda.fast.common.model.DarkMode +import dev.meloda.fast.common.model.LogLevel import dev.meloda.fast.common.model.LongPollState import dev.meloda.fast.common.model.UiText +import dev.meloda.fast.common.model.parseString import dev.meloda.fast.data.UserConfig import dev.meloda.fast.data.db.AccountsRepository import dev.meloda.fast.datastore.AppSettings @@ -405,6 +407,33 @@ class SettingsViewModelImpl( defaultValue = SettingsKeys.DEFAULT_VALUE_APPEARANCE_SHOW_TIME_IN_ACTION_MESSAGES, title = UiText.Simple("Show time in action messages") ) + val debugEnableHaptic = SettingsItem.Switch( + key = SettingsKeys.KEY_DEBUG_ENABLE_HAPTIC, + defaultValue = true, + title = UiText.Simple("Enable haptic") + ) + + val logLevelValues = listOf( + LogLevel.NONE to UiText.Simple("None"), + LogLevel.BASIC to UiText.Simple("Basic"), + LogLevel.HEADERS to UiText.Simple("Headers"), + LogLevel.BODY to UiText.Simple("Body") + ).toMap() + + val debugNetworkLogLevel = SettingsItem.ListItem( + key = SettingsKeys.KEY_DEBUG_NETWORK_LOG_LEVEL, + title = UiText.Simple("Network log level"), + valueClass = Int::class, + defaultValue = SettingsKeys.DEFAULT_NETWORK_LOG_LEVEL, + titles = logLevelValues.values.toList(), + values = logLevelValues.keys.toList().map(LogLevel::value) + ).apply { + textProvider = TextProvider { item -> + val textValue = logLevelValues[LogLevel.parse(item.value)].parseString(resources) + + UiText.Simple("Current value: $textValue") + } + } val debugHideDebugList = SettingsItem.TitleText( key = SettingsKeys.KEY_DEBUG_HIDE_DEBUG_LIST, @@ -444,7 +473,9 @@ class SettingsViewModelImpl( debugLongPollBackground, debugUseBlur, debugShowEmojiButton, - debugShowTimeInActionMessages + debugShowTimeInActionMessages, + debugEnableHaptic, + debugNetworkLogLevel ).forEach(debugList::add) debugList += debugHideDebugList diff --git a/feature/settings/src/main/kotlin/dev/meloda/fast/settings/presentation/SettingsScreen.kt b/feature/settings/src/main/kotlin/dev/meloda/fast/settings/presentation/SettingsScreen.kt index ac48a49f..cc6c17d3 100644 --- a/feature/settings/src/main/kotlin/dev/meloda/fast/settings/presentation/SettingsScreen.kt +++ b/feature/settings/src/main/kotlin/dev/meloda/fast/settings/presentation/SettingsScreen.kt @@ -36,6 +36,7 @@ import dev.chrisbanes.haze.hazeChild import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.materials.HazeMaterials import dev.meloda.fast.data.UserConfig +import dev.meloda.fast.datastore.AppSettings import dev.meloda.fast.datastore.SettingsKeys import dev.meloda.fast.settings.HapticType import dev.meloda.fast.settings.SettingsViewModel @@ -111,7 +112,9 @@ fun SettingsScreen( LaunchedEffect(hapticType) { if (hapticType != null) { - view.performHapticFeedback(hapticType.getHaptic()) + if (AppSettings.Debug.enableHaptic) { + view.performHapticFeedback(hapticType.getHaptic()) + } onHapticPerformed() } }