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 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,
@@ -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"
+1
View File
@@ -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)
@@ -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()
@@ -47,6 +47,8 @@ class ImmutableList<T>(val values: List<T>) : Iterable<T> {
return single
}
val size: Int get() = values.size
companion object {
fun <T> copyOf(collection: Collection<T>): ImmutableList<T> =
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 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"
)
)
}
}
}
@@ -203,7 +203,7 @@ fun SignInAlert(
onConfirmClick: (token: String) -> Unit
) {
var tokenText by rememberSaveable {
mutableStateOf("")
mutableStateOf(BuildConfig.debugToken)
}
val maxWidthModifier = Modifier.fillMaxWidth()
@@ -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
) {
@@ -8,7 +8,8 @@ data class ConversationsScreenState(
val conversations: List<UiConversation>,
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
)
}
}
@@ -26,7 +26,7 @@ fun NavGraphBuilder.conversationsScreen(
ConversationsRoute(
onError = onError,
onConversationItemClicked = onConversationItemClicked,
onPhotoClicked = onPhotoClicked,
onConversationPhotoClicked = onPhotoClicked,
viewModel = viewModel
)
}
@@ -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
)
}
}
@@ -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)
)
@@ -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<UiItem>,
val message: String,
val message: TextFieldValue,
val attachments: List<VkAttachment>,
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,
@@ -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(
@@ -10,7 +10,7 @@ class MessagesHistoryValidator {
val results = mutableListOf<MessagesHistoryValidationResult>()
results.addIf(MessagesHistoryValidationResult.MessageEmpty) {
screenState.message.isBlank()
screenState.message.text.isBlank()
}
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.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
@@ -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()
}
}