return of debug token for auth; ability to disable haptic and set logging level; etc
This commit is contained in:
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
+22
@@ -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
|
||||||
) {
|
) {
|
||||||
|
|||||||
+4
-2
@@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-19
@@ -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 = {
|
||||||
|
if (AppSettings.Debug.enableHaptic) {
|
||||||
view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT)
|
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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-5
@@ -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)
|
||||||
)
|
)
|
||||||
|
|||||||
+3
-2
@@ -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,
|
||||||
|
|||||||
+41
-21
@@ -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 = {
|
||||||
|
if (AppSettings.Debug.enableHaptic) {
|
||||||
view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT)
|
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) {
|
||||||
|
if (AppSettings.Debug.enableHaptic) {
|
||||||
view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT)
|
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(
|
||||||
|
|||||||
+1
-1
@@ -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
|
||||||
|
|||||||
+3
@@ -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) {
|
||||||
|
if (AppSettings.Debug.enableHaptic) {
|
||||||
view.performHapticFeedback(hapticType.getHaptic())
|
view.performHapticFeedback(hapticType.getHaptic())
|
||||||
|
}
|
||||||
onHapticPerformed()
|
onHapticPerformed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user