return of debug token for auth; ability to disable haptic and set logging level; etc

This commit is contained in:
2024-10-26 02:40:31 +03:00
parent ba43b6a940
commit babf20f62e
18 changed files with 204 additions and 59 deletions
@@ -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")
}
}
@@ -3,6 +3,7 @@ package dev.meloda.fast.datastore
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.core.content.edit import androidx.core.content.edit
import dev.meloda.fast.common.model.DarkMode import dev.meloda.fast.common.model.DarkMode
import dev.meloda.fast.common.model.LogLevel
import kotlin.properties.Delegates import kotlin.properties.Delegates
import kotlin.reflect.KClass import kotlin.reflect.KClass
@@ -187,6 +188,20 @@ object AppSettings {
) )
set(value) = put(SettingsKeys.KEY_APPEARANCE_SHOW_TIME_IN_ACTION_MESSAGES, value) 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 var showDebugCategory: Boolean
get() = get( get() = get(
SettingsKeys.KEY_SHOW_DEBUG_CATEGORY, SettingsKeys.KEY_SHOW_DEBUG_CATEGORY,
@@ -45,6 +45,9 @@ object SettingsKeys {
const val KEY_DEBUG_SHOW_CRASH_ALERT = "debug_show_crash_alert" 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_DEBUG_HIDE_DEBUG_LIST = "debug_hide_debug_list"
const val KEY_ENABLE_ANIMATIONS_IN_MESSAGES = "debug_enable_animations_in_messages" 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" const val KEY_SHOW_DEBUG_CATEGORY = "show_debug_category"
+1
View File
@@ -14,6 +14,7 @@ android {
dependencies { dependencies {
api(projects.core.common) api(projects.core.common)
api(projects.core.model) api(projects.core.model)
api(projects.core.datastore)
implementation(libs.moshi.kotlin) implementation(libs.moshi.kotlin)
implementation(libs.koin.android) implementation(libs.koin.android)
@@ -6,6 +6,8 @@ import com.slack.eithernet.ApiResultCallAdapterFactory
import com.slack.eithernet.ApiResultConverterFactory import com.slack.eithernet.ApiResultConverterFactory
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import dev.meloda.fast.common.AppConstants 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.JsonConverter
import dev.meloda.fast.network.MoshiConverter import dev.meloda.fast.network.MoshiConverter
import dev.meloda.fast.network.OAuthResultCallFactory import dev.meloda.fast.network.OAuthResultCallFactory
@@ -55,7 +57,12 @@ val networkModule = module {
.followSslRedirects(true) .followSslRedirects(true)
.addInterceptor( .addInterceptor(
HttpLoggingInterceptor().apply { 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() .build()
@@ -47,6 +47,8 @@ class ImmutableList<T>(val values: List<T>) : Iterable<T> {
return single return single
} }
val size: Int get() = values.size
companion object { companion object {
fun <T> copyOf(collection: Collection<T>): ImmutableList<T> = fun <T> copyOf(collection: Collection<T>): ImmutableList<T> =
ImmutableList(collection.toList()) ImmutableList(collection.toList())
+10
View File
@@ -4,6 +4,8 @@ import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
val sdkPackage: String = getLocalProperty("sdkPackage", "\"\"") val sdkPackage: String = getLocalProperty("sdkPackage", "\"\"")
val sdkFingerprint: String = getLocalProperty("sdkFingerprint", "\"\"") val sdkFingerprint: String = getLocalProperty("sdkFingerprint", "\"\"")
val debugToken: String = getLocalProperty("debugToken", "\"\"")
fun getLocalProperty(key: String, defValue: String): String { fun getLocalProperty(key: String, defValue: String): String {
return gradleLocalProperties(rootDir, providers).getProperty(key, defValue) return gradleLocalProperties(rootDir, providers).getProperty(key, defValue)
} }
@@ -32,6 +34,14 @@ androidComponents {
comment = "sdkFingerprint for VK" comment = "sdkFingerprint for VK"
) )
) )
put(
"debugToken",
BuildConfigField(
type = "String",
value = debugToken,
comment = "debug token for authorization"
)
)
} }
} }
} }
@@ -203,7 +203,7 @@ fun SignInAlert(
onConfirmClick: (token: String) -> Unit onConfirmClick: (token: String) -> Unit
) { ) {
var tokenText by rememberSaveable { var tokenText by rememberSaveable {
mutableStateOf("") mutableStateOf(BuildConfig.debugToken)
} }
val maxWidthModifier = Modifier.fillMaxWidth() val maxWidthModifier = Modifier.fillMaxWidth()
@@ -18,6 +18,7 @@ import dev.meloda.fast.data.State
import dev.meloda.fast.data.processState import dev.meloda.fast.data.processState
import dev.meloda.fast.datastore.UserSettings import dev.meloda.fast.datastore.UserSettings
import dev.meloda.fast.domain.ConversationsUseCase import dev.meloda.fast.domain.ConversationsUseCase
import dev.meloda.fast.domain.LoadUserByIdUseCase
import dev.meloda.fast.domain.LongPollUpdatesParser import dev.meloda.fast.domain.LongPollUpdatesParser
import dev.meloda.fast.domain.MessagesUseCase import dev.meloda.fast.domain.MessagesUseCase
import dev.meloda.fast.model.BaseError import dev.meloda.fast.model.BaseError
@@ -65,6 +66,7 @@ interface ConversationsViewModel {
class ConversationsViewModelImpl( class ConversationsViewModelImpl(
updatesParser: LongPollUpdatesParser, updatesParser: LongPollUpdatesParser,
private val conversationsUseCase: ConversationsUseCase, private val conversationsUseCase: ConversationsUseCase,
private val loadUserByIdUseCase: LoadUserByIdUseCase,
private val messagesUseCase: MessagesUseCase, private val messagesUseCase: MessagesUseCase,
private val resources: Resources, private val resources: Resources,
private val userSettings: UserSettings private val userSettings: UserSettings
@@ -97,6 +99,8 @@ class ConversationsViewModelImpl(
updatesParser.onConversationPinStateChanged(::handlePinStateChanged) updatesParser.onConversationPinStateChanged(::handlePinStateChanged)
updatesParser.onInteractions(::handleInteraction) updatesParser.onInteractions(::handleInteraction)
loadProfile()
loadConversations() loadConversations()
} }
@@ -223,6 +227,24 @@ class ConversationsViewModelImpl(
screenState.setValue { old -> old.copy(showOptions = newShowOptions) } 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( private fun loadConversations(
offset: Int = currentOffset.value offset: Int = currentOffset.value
) { ) {
@@ -8,7 +8,8 @@ data class ConversationsScreenState(
val conversations: List<UiConversation>, val conversations: List<UiConversation>,
val isLoading: Boolean, val isLoading: Boolean,
val isPaginating: Boolean, val isPaginating: Boolean,
val isPaginationExhausted: Boolean val isPaginationExhausted: Boolean,
val profileImageUrl: String?
) { ) {
companion object { companion object {
@@ -17,7 +18,8 @@ data class ConversationsScreenState(
conversations = emptyList(), conversations = emptyList(),
isLoading = true, isLoading = true,
isPaginating = false, isPaginating = false,
isPaginationExhausted = false isPaginationExhausted = false,
profileImageUrl = null
) )
} }
} }
@@ -26,7 +26,7 @@ fun NavGraphBuilder.conversationsScreen(
ConversationsRoute( ConversationsRoute(
onError = onError, onError = onError,
onConversationItemClicked = onConversationItemClicked, onConversationItemClicked = onConversationItemClicked,
onPhotoClicked = onPhotoClicked, onConversationPhotoClicked = onPhotoClicked,
viewModel = viewModel viewModel = viewModel
) )
} }
@@ -9,6 +9,7 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideIn import androidx.compose.animation.slideIn
import androidx.compose.animation.slideOut import androidx.compose.animation.slideOut
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets 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.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material.icons.rounded.Refresh import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
@@ -51,6 +52,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.rotate
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
@@ -63,6 +65,7 @@ import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.view.HapticFeedbackConstantsCompat import androidx.core.view.HapticFeedbackConstantsCompat
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil.compose.AsyncImage
import coil.imageLoader import coil.imageLoader
import coil.request.ImageRequest import coil.request.ImageRequest
import dev.chrisbanes.haze.haze 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.ConversationOption
import dev.meloda.fast.conversations.model.ConversationsScreenState import dev.meloda.fast.conversations.model.ConversationsScreenState
import dev.meloda.fast.conversations.model.UiConversation import dev.meloda.fast.conversations.model.UiConversation
import dev.meloda.fast.datastore.AppSettings
import dev.meloda.fast.datastore.UserSettings import dev.meloda.fast.datastore.UserSettings
import dev.meloda.fast.model.BaseError import dev.meloda.fast.model.BaseError
import dev.meloda.fast.ui.components.ErrorView import dev.meloda.fast.ui.components.ErrorView
@@ -90,7 +94,7 @@ import dev.meloda.fast.ui.R as UiR
fun ConversationsRoute( fun ConversationsRoute(
onError: (BaseError) -> Unit, onError: (BaseError) -> Unit,
onConversationItemClicked: (conversationId: Int) -> Unit, onConversationItemClicked: (conversationId: Int) -> Unit,
onPhotoClicked: (url: String) -> Unit, onConversationPhotoClicked: (url: String) -> Unit,
viewModel: ConversationsViewModel viewModel: ConversationsViewModel
) { ) {
val context = LocalContext.current val context = LocalContext.current
@@ -129,7 +133,7 @@ fun ConversationsRoute(
onPaginationConditionsMet = viewModel::onPaginationConditionsMet, onPaginationConditionsMet = viewModel::onPaginationConditionsMet,
onRefreshDropdownItemClicked = viewModel::onRefresh, onRefreshDropdownItemClicked = viewModel::onRefresh,
onRefresh = viewModel::onRefresh, onRefresh = viewModel::onRefresh,
onPhotoClicked = onPhotoClicked onConversationPhotoClicked = onConversationPhotoClicked
) )
@@ -156,7 +160,7 @@ fun ConversationsScreen(
onPaginationConditionsMet: () -> Unit = {}, onPaginationConditionsMet: () -> Unit = {},
onRefreshDropdownItemClicked: () -> Unit = {}, onRefreshDropdownItemClicked: () -> Unit = {},
onRefresh: () -> Unit = {}, onRefresh: () -> Unit = {},
onPhotoClicked: (url: String) -> Unit = {} onConversationPhotoClicked: (url: String) -> Unit = {}
) { ) {
val view = LocalView.current val view = LocalView.current
val currentTheme = LocalThemeConfig.current val currentTheme = LocalThemeConfig.current
@@ -220,23 +224,32 @@ fun ConversationsScreen(
) )
}, },
actions = { actions = {
IconButton( AsyncImage(
onClick = { model = screenState.profileImageUrl,
dropDownMenuExpanded = true contentDescription = "Profile Image",
} modifier = Modifier
) { .padding(end = 12.dp)
Icon( .size(32.dp)
imageVector = Icons.Outlined.MoreVert, .clip(CircleShape)
contentDescription = "Options button" .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( DropdownMenu(
modifier = Modifier.defaultMinSize(minWidth = 140.dp), modifier = Modifier.defaultMinSize(minWidth = 140.dp),
expanded = dropDownMenuExpanded, expanded = dropDownMenuExpanded,
onDismissRequest = { onDismissRequest = { dropDownMenuExpanded = false },
dropDownMenuExpanded = false
},
offset = DpOffset(x = (-4).dp, y = (-60).dp) offset = DpOffset(x = (-4).dp, y = (-60).dp)
) { ) {
DropdownMenuItem( DropdownMenuItem(
@@ -293,8 +306,9 @@ fun ConversationsScreen(
) { ) {
FloatingActionButton( FloatingActionButton(
onClick = { onClick = {
view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT) if (AppSettings.Debug.enableHaptic) {
view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT)
}
scope.launch { scope.launch {
for (i in 20 downTo 0 step 4) { for (i in 20 downTo 0 step 4) {
rotation.animateTo( rotation.animateTo(
@@ -372,7 +386,7 @@ fun ConversationsScreen(
}.fillMaxSize(), }.fillMaxSize(),
onOptionClicked = onOptionClicked, onOptionClicked = onOptionClicked,
padding = padding, padding = padding,
onPhotoClicked = onPhotoClicked onPhotoClicked = onConversationPhotoClicked
) )
} }
} }
@@ -2,6 +2,7 @@ package dev.meloda.fast.messageshistory
import android.content.SharedPreferences import android.content.SharedPreferences
import android.util.Log import android.util.Log
import androidx.compose.ui.text.input.TextFieldValue
import androidx.core.content.edit import androidx.core.content.edit
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@@ -52,7 +53,7 @@ interface MessagesHistoryViewModel {
fun onRefresh() fun onRefresh()
fun onAttachmentButtonClicked() fun onAttachmentButtonClicked()
fun onMessageInputChanged(newText: String) fun onMessageInputChanged(newText: TextFieldValue)
fun onEmojiButtonClicked() fun onEmojiButtonClicked()
fun onActionButtonClicked() fun onActionButtonClicked()
@@ -110,11 +111,11 @@ class MessagesHistoryViewModelImpl(
} }
override fun onMessageInputChanged(newText: String) { override fun onMessageInputChanged(newText: TextFieldValue) {
screenState.setValue { old -> screenState.setValue { old ->
old.copy( old.copy(
message = newText, message = newText,
actionMode = if (newText.isEmptyOrBlank()) ActionMode.Record actionMode = if (newText.text.isEmptyOrBlank()) ActionMode.Record
else ActionMode.Send else ActionMode.Send
) )
} }
@@ -317,7 +318,7 @@ class MessagesHistoryViewModelImpl(
} }
private fun sendMessage() { private fun sendMessage() {
lastMessageText = screenState.value.message lastMessageText = screenState.value.message.text
val newMessage = VkMessage( val newMessage = VkMessage(
id = -1 - sendingMessages.size, id = -1 - sendingMessages.size,
@@ -363,7 +364,7 @@ class MessagesHistoryViewModelImpl(
screenState.setValue { old -> screenState.setValue { old ->
old.copy( old.copy(
message = "", message = TextFieldValue(),
actionMode = ActionMode.Record, actionMode = ActionMode.Record,
messages = listOf(newUiMessage).plus(old.messages) messages = listOf(newUiMessage).plus(old.messages)
) )
@@ -1,6 +1,7 @@
package dev.meloda.fast.messageshistory.model package dev.meloda.fast.messageshistory.model
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.ui.text.input.TextFieldValue
import dev.meloda.fast.common.model.UiImage import dev.meloda.fast.common.model.UiImage
import dev.meloda.fast.model.api.domain.VkAttachment import dev.meloda.fast.model.api.domain.VkAttachment
@@ -11,7 +12,7 @@ data class MessagesHistoryScreenState(
val status: String?, val status: String?,
val avatar: UiImage, val avatar: UiImage,
val messages: List<UiItem>, val messages: List<UiItem>,
val message: String, val message: TextFieldValue,
val attachments: List<VkAttachment>, val attachments: List<VkAttachment>,
val isLoading: Boolean, val isLoading: Boolean,
val isPaginating: Boolean, val isPaginating: Boolean,
@@ -26,7 +27,7 @@ data class MessagesHistoryScreenState(
status = null, status = null,
avatar = UiImage.Color(0), avatar = UiImage.Color(0),
messages = emptyList(), messages = emptyList(),
message = "", message = TextFieldValue(),
attachments = emptyList(), attachments = emptyList(),
isLoading = true, isLoading = true,
isPaginating = false, isPaginating = false,
@@ -72,6 +72,11 @@ import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.view.HapticFeedbackConstantsCompat import androidx.core.view.HapticFeedbackConstantsCompat
import androidx.lifecycle.compose.collectAsStateWithLifecycle 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.SettingsKeys
import dev.meloda.fast.datastore.UserSettings import dev.meloda.fast.datastore.UserSettings
import dev.meloda.fast.messageshistory.MessagesHistoryViewModel 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.model.BaseError
import dev.meloda.fast.ui.theme.LocalThemeConfig import dev.meloda.fast.ui.theme.LocalThemeConfig
import dev.meloda.fast.ui.util.ImmutableList 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 kotlinx.coroutines.launch
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
import org.koin.compose.koinInject import org.koin.compose.koinInject
@@ -138,7 +139,7 @@ fun MessagesHistoryScreen(
onRefreshDropdownItemClicked: () -> Unit = {}, onRefreshDropdownItemClicked: () -> Unit = {},
onToggleAnimationsDropdownItemClicked: (Boolean) -> Unit = {}, onToggleAnimationsDropdownItemClicked: (Boolean) -> Unit = {},
onPaginationConditionsMet: () -> Unit = {}, onPaginationConditionsMet: () -> Unit = {},
onMessageInputChanged: (String) -> Unit = {}, onMessageInputChanged: (TextFieldValue) -> Unit = {},
onAttachmentButtonClicked: () -> Unit = {}, onAttachmentButtonClicked: () -> Unit = {},
onActionButtonClicked: () -> Unit = {} onActionButtonClicked: () -> Unit = {}
) { ) {
@@ -367,8 +368,9 @@ fun MessagesHistoryScreen(
Column(verticalArrangement = Arrangement.Bottom) { Column(verticalArrangement = Arrangement.Bottom) {
IconButton( IconButton(
onClick = { onClick = {
view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT) if (AppSettings.Debug.enableHaptic) {
view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT)
}
scope.launch { scope.launch {
for (i in 20 downTo 0 step 4) { for (i in 20 downTo 0 step 4) {
rotation.animateTo( rotation.animateTo(
@@ -397,15 +399,10 @@ fun MessagesHistoryScreen(
} }
} }
var message by remember { mutableStateOf(TextFieldValue(screenState.message)) }
TextField( TextField(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
value = message, value = screenState.message,
onValueChange = { newText -> onValueChange = onMessageInputChanged,
message = newText
onMessageInputChanged(newText.text)
},
colors = TextFieldDefaults.colors( colors = TextFieldDefaults.colors(
unfocusedContainerColor = Color.Transparent, unfocusedContainerColor = Color.Transparent,
focusedContainerColor = Color.Transparent, focusedContainerColor = Color.Transparent,
@@ -421,36 +418,59 @@ fun MessagesHistoryScreen(
} }
) )
val scope = rememberCoroutineScope()
val attachmentRotation = remember { Animatable(0f) }
Column(verticalArrangement = Arrangement.Bottom) { 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( Icon(
painter = painterResource(id = UiR.drawable.round_attach_file_24), painter = painterResource(id = UiR.drawable.round_attach_file_24),
contentDescription = "Add attachment button", contentDescription = "Add attachment button",
tint = MaterialTheme.colorScheme.primary, tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.rotate(30f) modifier = Modifier.rotate(30f + attachmentRotation.value)
) )
} }
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
} }
val scope = rememberCoroutineScope() val micRotation = remember { Animatable(0f) }
val rotation = remember { Animatable(0f) }
Column(verticalArrangement = Arrangement.Bottom) { Column(verticalArrangement = Arrangement.Bottom) {
IconButton( IconButton(
onClick = { onClick = {
if (screenState.actionMode == ActionMode.Record) { if (screenState.actionMode == ActionMode.Record) {
view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT) if (AppSettings.Debug.enableHaptic) {
view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT)
}
scope.launch { scope.launch {
for (i in 20 downTo 0 step 4) { for (i in 20 downTo 0 step 4) {
rotation.animateTo( micRotation.animateTo(
targetValue = i.toFloat(), targetValue = i.toFloat(),
animationSpec = tween(50) animationSpec = tween(50)
) )
if (i > 0) { if (i > 0) {
rotation.animateTo( micRotation.animateTo(
targetValue = -i.toFloat(), targetValue = -i.toFloat(),
animationSpec = tween(50) animationSpec = tween(50)
) )
@@ -461,7 +481,7 @@ fun MessagesHistoryScreen(
onActionButtonClicked() onActionButtonClicked()
} }
}, },
modifier = Modifier.rotate(rotation.value) modifier = Modifier.rotate(micRotation.value)
) { ) {
Icon( Icon(
painter = painterResource( painter = painterResource(
@@ -10,7 +10,7 @@ class MessagesHistoryValidator {
val results = mutableListOf<MessagesHistoryValidationResult>() val results = mutableListOf<MessagesHistoryValidationResult>()
results.addIf(MessagesHistoryValidationResult.MessageEmpty) { results.addIf(MessagesHistoryValidationResult.MessageEmpty) {
screenState.message.isBlank() screenState.message.text.isBlank()
} }
results.addIf(MessagesHistoryValidationResult.AttachmentsEmpty) { results.addIf(MessagesHistoryValidationResult.AttachmentsEmpty) {
@@ -10,8 +10,10 @@ import dev.meloda.fast.common.LongPollController
import dev.meloda.fast.common.extensions.findWithIndex import dev.meloda.fast.common.extensions.findWithIndex
import dev.meloda.fast.common.extensions.setValue import dev.meloda.fast.common.extensions.setValue
import dev.meloda.fast.common.model.DarkMode 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.LongPollState
import dev.meloda.fast.common.model.UiText 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.UserConfig
import dev.meloda.fast.data.db.AccountsRepository import dev.meloda.fast.data.db.AccountsRepository
import dev.meloda.fast.datastore.AppSettings import dev.meloda.fast.datastore.AppSettings
@@ -405,6 +407,33 @@ class SettingsViewModelImpl(
defaultValue = SettingsKeys.DEFAULT_VALUE_APPEARANCE_SHOW_TIME_IN_ACTION_MESSAGES, defaultValue = SettingsKeys.DEFAULT_VALUE_APPEARANCE_SHOW_TIME_IN_ACTION_MESSAGES,
title = UiText.Simple("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( val debugHideDebugList = SettingsItem.TitleText(
key = SettingsKeys.KEY_DEBUG_HIDE_DEBUG_LIST, key = SettingsKeys.KEY_DEBUG_HIDE_DEBUG_LIST,
@@ -444,7 +473,9 @@ class SettingsViewModelImpl(
debugLongPollBackground, debugLongPollBackground,
debugUseBlur, debugUseBlur,
debugShowEmojiButton, debugShowEmojiButton,
debugShowTimeInActionMessages debugShowTimeInActionMessages,
debugEnableHaptic,
debugNetworkLogLevel
).forEach(debugList::add) ).forEach(debugList::add)
debugList += debugHideDebugList debugList += debugHideDebugList
@@ -36,6 +36,7 @@ import dev.chrisbanes.haze.hazeChild
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import dev.chrisbanes.haze.materials.HazeMaterials import dev.chrisbanes.haze.materials.HazeMaterials
import dev.meloda.fast.data.UserConfig import dev.meloda.fast.data.UserConfig
import dev.meloda.fast.datastore.AppSettings
import dev.meloda.fast.datastore.SettingsKeys import dev.meloda.fast.datastore.SettingsKeys
import dev.meloda.fast.settings.HapticType import dev.meloda.fast.settings.HapticType
import dev.meloda.fast.settings.SettingsViewModel import dev.meloda.fast.settings.SettingsViewModel
@@ -111,7 +112,9 @@ fun SettingsScreen(
LaunchedEffect(hapticType) { LaunchedEffect(hapticType) {
if (hapticType != null) { if (hapticType != null) {
view.performHapticFeedback(hapticType.getHaptic()) if (AppSettings.Debug.enableHaptic) {
view.performHapticFeedback(hapticType.getHaptic())
}
onHapticPerformed() onHapticPerformed()
} }
} }