Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 797e966b65 | |||
| 2c8536a9da | |||
| 12ba4faade | |||
| 89e5d10bdf | |||
| 79e5823ff8 | |||
| 6995fc385c | |||
| bed1348d19 | |||
| 7fa2da8a2c | |||
| 842ffe9f5b | |||
| 72520863bd | |||
| b69273b043 | |||
| 81f9742120 | |||
| a8cd647d04 | |||
| 7e17738024 | |||
| 9983fa3752 | |||
| 5fd2fb0abb | |||
| 59280a0358 | |||
| e9f84cbdf4 | |||
| b80fc6c936 | |||
| 92a156f2f0 | |||
| a20b4e42ad | |||
| 124033fb9c | |||
| 11c394cc9f | |||
| 60d173e1f3 | |||
| e1a2cce08d | |||
| 9756a36650 | |||
| f363e2c547 | |||
| 1cddcd7e99 | |||
| 095aa20dcc | |||
| ce5a43aea9 | |||
| 8824775f68 | |||
| 7e5843759d | |||
| 7c14df1824 |
@@ -7,10 +7,10 @@ plugins {
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "dev.meloda.fast"
|
||||
namespace = "dev.meloda.fastvk"
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "dev.meloda.fast"
|
||||
applicationId = "dev.meloda.fastvk"
|
||||
|
||||
versionCode = libs.versions.versionCode.get().toInt()
|
||||
versionName = libs.versions.versionName.get()
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
tools:targetApi="tiramisu">
|
||||
|
||||
<activity
|
||||
android:name=".presentation.MainActivity"
|
||||
android:name="dev.meloda.fast.presentation.MainActivity"
|
||||
android:exported="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
|
||||
@@ -38,13 +38,13 @@
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".service.longpolling.LongPollingService"
|
||||
android:name="dev.meloda.fast.service.longpolling.LongPollingService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<service
|
||||
android:name=".service.OnlineService"
|
||||
android:name="dev.meloda.fast.service.OnlineService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
@@ -86,6 +86,8 @@ class MainViewModelImpl(
|
||||
BaseError.SessionExpired -> {
|
||||
isNeedToReplaceWithAuth.update { true }
|
||||
}
|
||||
|
||||
is BaseError.SimpleError -> Unit // TODO: 21-Mar-25, Danil Nikolaev: show error in ui
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,7 +211,7 @@ class MainViewModelImpl(
|
||||
}
|
||||
|
||||
longPollController.setStateToApply(
|
||||
if (AppSettings.Debug.longPollInBackground) {
|
||||
if (AppSettings.Experimental.longPollInBackground) {
|
||||
LongPollState.Background
|
||||
} else {
|
||||
LongPollState.InApp
|
||||
@@ -233,7 +235,7 @@ class MainViewModelImpl(
|
||||
}
|
||||
|
||||
private fun disableBackgroundLongPoll() {
|
||||
AppSettings.Debug.longPollInBackground = false
|
||||
AppSettings.Experimental.longPollInBackground = false
|
||||
longPollController.setStateToApply(LongPollState.InApp)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@ import dev.meloda.fast.provider.ApiLanguageProvider
|
||||
import dev.meloda.fast.service.longpolling.di.longPollModule
|
||||
import dev.meloda.fast.settings.di.settingsModule
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.androidx.viewmodel.dsl.viewModelOf
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.core.module.dsl.viewModelOf
|
||||
import org.koin.core.qualifier.qualifier
|
||||
import org.koin.dsl.bind
|
||||
import org.koin.dsl.module
|
||||
@@ -61,7 +61,7 @@ val applicationModule = module {
|
||||
qualifier = qualifier("main")
|
||||
}
|
||||
|
||||
single {
|
||||
single<ImageLoader> {
|
||||
ImageLoader.Builder(get())
|
||||
.crossfade(true)
|
||||
.build()
|
||||
|
||||
@@ -24,7 +24,6 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
@@ -85,6 +84,7 @@ class MainActivity : AppCompatActivity() {
|
||||
)
|
||||
|
||||
createNotificationChannels()
|
||||
requestNotificationPermissions()
|
||||
|
||||
setContent {
|
||||
KoinContext {
|
||||
@@ -250,12 +250,11 @@ class MainActivity : AppCompatActivity() {
|
||||
val noCategoryName = getString(UiR.string.notification_channel_no_category_name)
|
||||
val noCategoryDescriptionText =
|
||||
getString(UiR.string.notification_channel_no_category_description)
|
||||
val noCategoryImportance = NotificationManagerCompat.IMPORTANCE_HIGH
|
||||
val noCategoryChannel =
|
||||
NotificationChannel(
|
||||
AppConstants.NOTIFICATION_CHANNEL_UNCATEGORIZED,
|
||||
noCategoryName,
|
||||
noCategoryImportance
|
||||
NotificationManager.IMPORTANCE_HIGH
|
||||
).apply {
|
||||
description = noCategoryDescriptionText
|
||||
}
|
||||
@@ -263,12 +262,11 @@ class MainActivity : AppCompatActivity() {
|
||||
val longPollName = getString(UiR.string.notification_channel_long_polling_service_name)
|
||||
val longPollDescriptionText =
|
||||
getString(UiR.string.notification_channel_long_polling_service_description)
|
||||
val longPollImportance = NotificationManagerCompat.IMPORTANCE_NONE
|
||||
val longPollChannel =
|
||||
NotificationChannel(
|
||||
AppConstants.NOTIFICATION_CHANNEL_LONG_POLLING,
|
||||
longPollName,
|
||||
longPollImportance
|
||||
NotificationManager.IMPORTANCE_NONE
|
||||
).apply {
|
||||
description = longPollDescriptionText
|
||||
}
|
||||
@@ -285,9 +283,18 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestNotificationPermissions() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
requestPermissions(
|
||||
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||
REQUEST_NOTIFICATION_PERMISSION_CODE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleLongPollService(
|
||||
enable: Boolean,
|
||||
inBackground: Boolean = AppSettings.Debug.longPollInBackground
|
||||
inBackground: Boolean = AppSettings.Experimental.longPollInBackground
|
||||
) {
|
||||
if (enable) {
|
||||
val longPollIntent = Intent(this, LongPollingService::class.java)
|
||||
@@ -313,7 +320,7 @@ class MainActivity : AppCompatActivity() {
|
||||
private fun stopServices() {
|
||||
toggleOnlineService(enable = false)
|
||||
|
||||
val asForeground = AppSettings.Debug.longPollInBackground
|
||||
val asForeground = AppSettings.Experimental.longPollInBackground
|
||||
|
||||
if (!asForeground) {
|
||||
toggleLongPollService(enable = false)
|
||||
@@ -324,4 +331,8 @@ class MainActivity : AppCompatActivity() {
|
||||
super.onDestroy()
|
||||
stopServices()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val REQUEST_NOTIFICATION_PERMISSION_CODE = 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,8 @@ import dev.meloda.fast.ui.theme.LocalBottomPadding
|
||||
import dev.meloda.fast.ui.theme.LocalHazeState
|
||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||
import dev.meloda.fast.ui.util.ImmutableList
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
|
||||
@OptIn(ExperimentalHazeMaterialsApi::class)
|
||||
@Composable
|
||||
@@ -68,6 +70,14 @@ fun MainScreen(
|
||||
mutableIntStateOf(1)
|
||||
}
|
||||
|
||||
val sharedFlow = remember {
|
||||
MutableSharedFlow<Int>(
|
||||
replay = 0,
|
||||
extraBufferCapacity = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
bottomBar = {
|
||||
NavigationBar(
|
||||
@@ -98,6 +108,8 @@ fun MainScreen(
|
||||
inclusive = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sharedFlow.tryEmit(index)
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
@@ -156,7 +168,11 @@ fun MainScreen(
|
||||
enterTransition = { fadeIn(animationSpec = tween(200)) },
|
||||
exitTransition = { fadeOut(animationSpec = tween(200)) }
|
||||
) {
|
||||
navigation<MainGraph>(startDestination = navigationItems[selectedItemIndex].route) {
|
||||
navigation<MainGraph>(
|
||||
startDestination = navigationItems[selectedItemIndex].route,
|
||||
enterTransition = { fadeIn(animationSpec = tween(200)) },
|
||||
exitTransition = { fadeOut(animationSpec = tween(200)) }
|
||||
) {
|
||||
friendsScreen(
|
||||
onError = onError,
|
||||
navController = navController,
|
||||
@@ -165,8 +181,9 @@ fun MainScreen(
|
||||
conversationsScreen(
|
||||
onError = onError,
|
||||
onConversationItemClicked = onConversationItemClicked,
|
||||
onPhotoClicked = onPhotoClicked,
|
||||
scrollToTopFlow = sharedFlow,
|
||||
navController = navController,
|
||||
onPhotoClicked = onPhotoClicked
|
||||
)
|
||||
profileScreen(
|
||||
onError = onError,
|
||||
|
||||
@@ -95,11 +95,6 @@ class OnlineService : Service() {
|
||||
}.also { coroutine -> coroutine.invokeOnCompletion { onlineJob = null } }
|
||||
}
|
||||
|
||||
override fun onLowMemory() {
|
||||
Log.d(STATE_TAG, "onLowMemory")
|
||||
super.onLowMemory()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
Log.d(STATE_TAG, "onDestroy")
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ class LongPollingService : Service() {
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (startId > 1) return START_STICKY
|
||||
|
||||
val inBackground = AppSettings.Debug.longPollInBackground
|
||||
val inBackground = AppSettings.Experimental.longPollInBackground
|
||||
|
||||
Log.d(
|
||||
STATE_TAG,
|
||||
@@ -258,10 +258,10 @@ class LongPollingService : Service() {
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onLowMemory() {
|
||||
Log.d(STATE_TAG, "onLowMemory")
|
||||
override fun onTrimMemory(level: Int) {
|
||||
Log.d(STATE_TAG, "onTrimMemory")
|
||||
longPollController.updateCurrentState(LongPollState.Stopped)
|
||||
super.onLowMemory()
|
||||
super.onTrimMemory(level)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -9,8 +9,8 @@ import org.gradle.kotlin.dsl.configure
|
||||
import org.gradle.kotlin.dsl.provideDelegate
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension
|
||||
import org.jetbrains.kotlin.gradle.dsl.KotlinBaseExtension
|
||||
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension
|
||||
import org.jetbrains.kotlin.gradle.dsl.KotlinTopLevelExtension
|
||||
|
||||
internal fun Project.configureKotlinAndroid(
|
||||
commonExtension: CommonExtension<*, *, *, *, *, *>,
|
||||
@@ -40,7 +40,7 @@ internal fun Project.configureKotlinJvm() {
|
||||
configureKotlin<KotlinJvmProjectExtension>()
|
||||
}
|
||||
|
||||
private inline fun <reified T : KotlinTopLevelExtension> Project.configureKotlin() = configure<T> {
|
||||
private inline fun <reified T : KotlinBaseExtension> Project.configureKotlin() = configure<T> {
|
||||
// Treat all Kotlin warnings as errors (disabled by default)
|
||||
// Override by setting warningsAsErrors=true in your ~/.gradle/gradle.properties
|
||||
val warningsAsErrors: String? by project
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package dev.meloda.fast.common.extensions
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@@ -86,7 +86,7 @@ object AndroidUtils {
|
||||
action = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
Settings.ACTION_SECURITY_SETTINGS
|
||||
} else {
|
||||
data = Uri.parse("package:dev.meloda.fast")
|
||||
data = Uri.parse("package:dev.meloda.fastvk")
|
||||
Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES
|
||||
}
|
||||
})
|
||||
|
||||
@@ -20,18 +20,20 @@ sealed class State<out T> {
|
||||
|
||||
data object ConnectionError : Error()
|
||||
|
||||
data object Unknown : Error()
|
||||
data object UnknownError : Error()
|
||||
|
||||
data object InternalError : Error()
|
||||
|
||||
data class OAuthError(val error: OAuthErrorDomain) : Error()
|
||||
|
||||
data class TestError(val message: String) : Error()
|
||||
}
|
||||
|
||||
fun isLoading(): Boolean = this is Loading
|
||||
|
||||
companion object {
|
||||
|
||||
val UNKNOWN_ERROR = Error.Unknown
|
||||
val UNKNOWN_ERROR = Error.UnknownError
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,11 +75,12 @@ fun <T : Any> ApiResult<T, RestApiErrorDomain>.mapToState() = when (this) {
|
||||
is ApiResult.Failure.ApiFailure -> this.error.toStateApiError()
|
||||
}
|
||||
|
||||
fun <T : Any, N> ApiResult<T, RestApiErrorDomain>.mapToState(successMapper: (T) -> N) = when (this) {
|
||||
is ApiResult.Success -> State.Success(successMapper(this.value))
|
||||
fun <T : Any, N> ApiResult<T, RestApiErrorDomain>.mapToState(successMapper: (T) -> N) =
|
||||
when (this) {
|
||||
is ApiResult.Success -> State.Success(successMapper(this.value))
|
||||
|
||||
is ApiResult.Failure.NetworkFailure -> State.Error.ConnectionError
|
||||
is ApiResult.Failure.UnknownFailure -> State.UNKNOWN_ERROR
|
||||
is ApiResult.Failure.HttpFailure -> this.error.toStateApiError()
|
||||
is ApiResult.Failure.ApiFailure -> this.error.toStateApiError()
|
||||
}
|
||||
is ApiResult.Failure.NetworkFailure -> State.Error.ConnectionError
|
||||
is ApiResult.Failure.UnknownFailure -> State.UNKNOWN_ERROR
|
||||
is ApiResult.Failure.HttpFailure -> this.error.toStateApiError()
|
||||
is ApiResult.Failure.ApiFailure -> this.error.toStateApiError()
|
||||
}
|
||||
|
||||
+5
-1
@@ -1,8 +1,8 @@
|
||||
package dev.meloda.fast.data.api.conversations
|
||||
|
||||
import com.slack.eithernet.ApiResult
|
||||
import dev.meloda.fast.model.api.domain.VkConversation
|
||||
import dev.meloda.fast.network.RestApiErrorDomain
|
||||
import com.slack.eithernet.ApiResult
|
||||
|
||||
interface ConversationsRepository {
|
||||
|
||||
@@ -11,6 +11,10 @@ interface ConversationsRepository {
|
||||
offset: Int?
|
||||
): ApiResult<List<VkConversation>, RestApiErrorDomain>
|
||||
|
||||
suspend fun getConversationsById(
|
||||
peerIds: List<Int>
|
||||
): ApiResult<List<VkConversation>, RestApiErrorDomain>
|
||||
|
||||
suspend fun storeConversations(conversations: List<VkConversation>)
|
||||
suspend fun delete(peerId: Int): ApiResult<Int, RestApiErrorDomain>
|
||||
suspend fun pin(peerId: Int): ApiResult<Int, RestApiErrorDomain>
|
||||
|
||||
+40
-1
@@ -1,5 +1,6 @@
|
||||
package dev.meloda.fast.data.api.conversations
|
||||
|
||||
import com.slack.eithernet.ApiResult
|
||||
import dev.meloda.fast.common.VkConstants
|
||||
import dev.meloda.fast.data.VkGroupsMap
|
||||
import dev.meloda.fast.data.VkMemoryCache
|
||||
@@ -19,7 +20,6 @@ import dev.meloda.fast.network.RestApiErrorDomain
|
||||
import dev.meloda.fast.network.mapApiDefault
|
||||
import dev.meloda.fast.network.mapApiResult
|
||||
import dev.meloda.fast.network.service.conversations.ConversationsService
|
||||
import com.slack.eithernet.ApiResult
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@@ -79,6 +79,45 @@ class ConversationsRepositoryImpl(
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getConversationsById(
|
||||
peerIds: List<Int>
|
||||
): ApiResult<List<VkConversation>, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||
val requestParams = mapOf(
|
||||
"peer_ids" to peerIds.joinToString(separator = ","),
|
||||
"extended" to "1",
|
||||
"fields" to VkConstants.ALL_FIELDS
|
||||
)
|
||||
|
||||
conversationsService.getConversationsById(requestParams).mapApiResult(
|
||||
successMapper = { apiResponse ->
|
||||
val response = apiResponse.requireResponse()
|
||||
|
||||
val profilesList = response.profiles.orEmpty().map(VkUserData::mapToDomain)
|
||||
val groupsList = response.groups.orEmpty().map(VkGroupData::mapToDomain)
|
||||
val contactsList = response.contacts.orEmpty().map(VkContactData::mapToDomain)
|
||||
|
||||
val usersMap = VkUsersMap.forUsers(profilesList)
|
||||
val groupsMap = VkGroupsMap.forGroups(groupsList)
|
||||
|
||||
VkMemoryCache.appendUsers(profilesList)
|
||||
VkMemoryCache.appendGroups(groupsList)
|
||||
VkMemoryCache.appendContacts(contactsList)
|
||||
|
||||
response.items.map { item ->
|
||||
item.asDomain().let { conversation ->
|
||||
conversation.copy(
|
||||
user = usersMap.conversationUser(conversation),
|
||||
group = groupsMap.conversationGroup(conversation)
|
||||
).also { VkMemoryCache[conversation.id] = it }
|
||||
}
|
||||
}
|
||||
},
|
||||
errorMapper = { error ->
|
||||
error?.toDomain()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun storeConversations(conversations: List<VkConversation>) {
|
||||
conversationDao.insertAll(conversations.map(VkConversation::asEntity))
|
||||
}
|
||||
|
||||
@@ -89,12 +89,20 @@ object AppSettings {
|
||||
)
|
||||
set(value) = put(SettingsKeys.KEY_USE_CONTACT_NAMES, value)
|
||||
|
||||
var enablePullToRefresh: Boolean
|
||||
var showEmojiButton: Boolean
|
||||
get() = get(
|
||||
SettingsKeys.KEY_ENABLE_PULL_TO_REFRESH,
|
||||
SettingsKeys.DEFAULT_VALUE_ENABLE_PULL_TO_REFRESH
|
||||
SettingsKeys.KEY_SHOW_EMOJI_BUTTON,
|
||||
SettingsKeys.DEFAULT_VALUE_KEY_SHOW_EMOJI_BUTTON
|
||||
)
|
||||
set(value) = put(SettingsKeys.KEY_ENABLE_PULL_TO_REFRESH, value)
|
||||
set(value) = put(SettingsKeys.KEY_SHOW_EMOJI_BUTTON, value)
|
||||
|
||||
var enableHaptic: Boolean
|
||||
get() = get(
|
||||
SettingsKeys.KEY_ENABLE_HAPTIC,
|
||||
SettingsKeys.DEFAULT_ENABLE_HAPTIC
|
||||
)
|
||||
set(value) = put(SettingsKeys.KEY_ENABLE_HAPTIC, value)
|
||||
|
||||
}
|
||||
|
||||
object Appearance {
|
||||
@@ -126,6 +134,13 @@ object AppSettings {
|
||||
)
|
||||
set(value) = put(SettingsKeys.KEY_USE_DYNAMIC_COLORS, value)
|
||||
|
||||
var useSystemFont: Boolean
|
||||
get() = get(
|
||||
SettingsKeys.KEY_USE_SYSTEM_FONT,
|
||||
SettingsKeys.DEFAULT_USE_SYSTEM_FONT
|
||||
)
|
||||
set(value) = put(SettingsKeys.KEY_USE_SYSTEM_FONT, value)
|
||||
|
||||
var appLanguage: String
|
||||
get() = get(
|
||||
SettingsKeys.KEY_APPEARANCE_LANGUAGE,
|
||||
@@ -152,6 +167,36 @@ object AppSettings {
|
||||
set(value) = put(SettingsKeys.KEY_ACTIVITY_SEND_ONLINE_STATUS, value)
|
||||
}
|
||||
|
||||
object Experimental {
|
||||
var longPollInBackground: Boolean
|
||||
get() = get(
|
||||
SettingsKeys.KEY_LONG_POLL_IN_BACKGROUND,
|
||||
SettingsKeys.DEFAULT_LONG_POLL_IN_BACKGROUND
|
||||
)
|
||||
set(value) = put(SettingsKeys.KEY_LONG_POLL_IN_BACKGROUND, value)
|
||||
|
||||
var showTimeInActionMessages: Boolean
|
||||
get() = get(
|
||||
SettingsKeys.KEY_SHOW_TIME_IN_ACTION_MESSAGES,
|
||||
SettingsKeys.DEFAULT_SHOW_TIME_IN_ACTION_MESSAGES
|
||||
)
|
||||
set(value) = put(SettingsKeys.KEY_SHOW_TIME_IN_ACTION_MESSAGES, value)
|
||||
|
||||
var useBlur: Boolean
|
||||
get() = get(
|
||||
SettingsKeys.KEY_USE_BLUR,
|
||||
SettingsKeys.DEFAULT_USE_BLUR
|
||||
)
|
||||
set(value) = put(SettingsKeys.KEY_USE_BLUR, value)
|
||||
|
||||
var moreAnimations: Boolean
|
||||
get() = get(
|
||||
SettingsKeys.KEY_MORE_ANIMATIONS,
|
||||
SettingsKeys.DEFAULT_MORE_ANIMATIONS
|
||||
)
|
||||
set(value) = put(SettingsKeys.KEY_MORE_ANIMATIONS, value)
|
||||
}
|
||||
|
||||
object Debug {
|
||||
var showAlertAfterCrash: Boolean
|
||||
get() = get(
|
||||
@@ -160,41 +205,6 @@ object AppSettings {
|
||||
)
|
||||
set(value) = put(SettingsKeys.KEY_DEBUG_SHOW_CRASH_ALERT, value)
|
||||
|
||||
var longPollInBackground: Boolean
|
||||
get() = get(
|
||||
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND,
|
||||
SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND
|
||||
)
|
||||
set(value) = put(SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND, value)
|
||||
|
||||
var useBlur: Boolean
|
||||
get() = get(
|
||||
SettingsKeys.KEY_APPEARANCE_USE_BLUR,
|
||||
SettingsKeys.DEFAULT_VALUE_KEY_APPEARANCE_USE_BLUR
|
||||
)
|
||||
set(value) = put(SettingsKeys.KEY_APPEARANCE_USE_BLUR, value)
|
||||
|
||||
var showEmojiButton: Boolean
|
||||
get() = get(
|
||||
SettingsKeys.KEY_SHOW_EMOJI_BUTTON,
|
||||
SettingsKeys.DEFAULT_VALUE_KEY_SHOW_EMOJI_BUTTON
|
||||
)
|
||||
set(value) = put(SettingsKeys.KEY_SHOW_EMOJI_BUTTON, value)
|
||||
|
||||
var showTimeInActionMessages: Boolean
|
||||
get() = get(
|
||||
SettingsKeys.KEY_APPEARANCE_SHOW_TIME_IN_ACTION_MESSAGES,
|
||||
SettingsKeys.DEFAULT_VALUE_APPEARANCE_SHOW_TIME_IN_ACTION_MESSAGES
|
||||
)
|
||||
set(value) = put(SettingsKeys.KEY_APPEARANCE_SHOW_TIME_IN_ACTION_MESSAGES, value)
|
||||
|
||||
var enableHaptic: Boolean
|
||||
get() = get(
|
||||
SettingsKeys.KEY_DEBUG_ENABLE_HAPTIC,
|
||||
SettingsKeys.DEFAULT_DEBUG_ENABLE_HAPTIC
|
||||
)
|
||||
set(value) = put(SettingsKeys.KEY_DEBUG_ENABLE_HAPTIC, value)
|
||||
|
||||
var networkLogLevel: LogLevel
|
||||
get() = get(
|
||||
SettingsKeys.KEY_DEBUG_NETWORK_LOG_LEVEL,
|
||||
@@ -202,13 +212,6 @@ object AppSettings {
|
||||
).let(LogLevel::parse)
|
||||
set(level) = put(SettingsKeys.KEY_DEBUG_NETWORK_LOG_LEVEL, level.value)
|
||||
|
||||
var useSystemFont: Boolean
|
||||
get() = get(
|
||||
SettingsKeys.KEY_DEBUG_USE_SYSTEM_FONT,
|
||||
SettingsKeys.DEFAULT_DEBUG_USE_SYSTEM_FONT
|
||||
)
|
||||
set(value) = put(SettingsKeys.KEY_DEBUG_USE_SYSTEM_FONT, value)
|
||||
|
||||
var showDebugCategory: Boolean
|
||||
get() = get(
|
||||
SettingsKeys.KEY_SHOW_DEBUG_CATEGORY,
|
||||
|
||||
@@ -7,11 +7,9 @@ object SettingsKeys {
|
||||
const val KEY_ACCOUNT_LOGOUT = "account_logout"
|
||||
|
||||
const val KEY_GENERAL = "general"
|
||||
const val KEY_USE_CONTACT_NAMES = "general_use_contact_names"
|
||||
const val KEY_USE_CONTACT_NAMES = "use_contact_names"
|
||||
const val DEFAULT_VALUE_USE_CONTACT_NAMES = false
|
||||
const val KEY_ENABLE_PULL_TO_REFRESH = "general_pull_to_refresh"
|
||||
const val DEFAULT_VALUE_ENABLE_PULL_TO_REFRESH = false
|
||||
const val KEY_SHOW_EMOJI_BUTTON = "general_show_emoji_button"
|
||||
const val KEY_SHOW_EMOJI_BUTTON = "show_emoji_button"
|
||||
const val DEFAULT_VALUE_KEY_SHOW_EMOJI_BUTTON = false
|
||||
|
||||
const val KEY_APPEARANCE = "appearance"
|
||||
@@ -23,20 +21,20 @@ object SettingsKeys {
|
||||
const val DEFAULT_VALUE_APPEARANCE_AMOLED_THEME = false
|
||||
const val KEY_USE_DYNAMIC_COLORS = "appearance_use_dynamic_colors"
|
||||
const val DEFAULT_VALUE_USE_DYNAMIC_COLORS = false
|
||||
const val KEY_APPEARANCE_COLOR_SCHEME = "appearance_color_scheme"
|
||||
const val DEFAULT_VALUE_APPEARANCE_COLOR_SCHEME = 0
|
||||
const val KEY_COLOR_SCHEME = "appearance_color_scheme"
|
||||
const val DEFAULT_COLOR_SCHEME = 0
|
||||
const val KEY_APPEARANCE_LANGUAGE = "appearance_language"
|
||||
const val DEFAULT_APPEARANCE_LANGUAGE = ""
|
||||
const val KEY_APPEARANCE_USE_BLUR = "appearance_use_blur"
|
||||
const val DEFAULT_VALUE_KEY_APPEARANCE_USE_BLUR = false
|
||||
const val KEY_APPEARANCE_SHOW_TIME_IN_ACTION_MESSAGES =
|
||||
"appearance_show_time_in_action_messages"
|
||||
const val DEFAULT_VALUE_APPEARANCE_SHOW_TIME_IN_ACTION_MESSAGES = false
|
||||
const val KEY_USE_BLUR = "use_blur"
|
||||
const val DEFAULT_USE_BLUR = false
|
||||
const val KEY_SHOW_TIME_IN_ACTION_MESSAGES =
|
||||
"show_time_in_action_messages"
|
||||
const val DEFAULT_SHOW_TIME_IN_ACTION_MESSAGES = false
|
||||
|
||||
const val KEY_FEATURES_FAST_TEXT = "features_fast_text"
|
||||
const val DEFAULT_VALUE_FEATURES_FAST_TEXT = "¯\\_(ツ)_/¯"
|
||||
const val KEY_FEATURES_LONG_POLL_IN_BACKGROUND = "features_lp_background"
|
||||
const val DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND = false
|
||||
const val KEY_LONG_POLL_IN_BACKGROUND = "lp_background"
|
||||
const val DEFAULT_LONG_POLL_IN_BACKGROUND = false
|
||||
|
||||
const val KEY_ACTIVITY_SEND_ONLINE_STATUS = "activity_send_online_status"
|
||||
const val DEFAULT_VALUE_KEY_ACTIVITY_SEND_ONLINE_STATUS = false
|
||||
@@ -44,15 +42,16 @@ object SettingsKeys {
|
||||
const val KEY_DEBUG_PERFORM_CRASH = "debug_perform_crash"
|
||||
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 DEFAULT_DEBUG_ENABLE_HAPTIC = true
|
||||
const val KEY_ENABLE_HAPTIC = "enable_haptic"
|
||||
const val DEFAULT_ENABLE_HAPTIC = true
|
||||
const val KEY_DEBUG_NETWORK_LOG_LEVEL = "debug_network_log_level"
|
||||
const val DEFAULT_NETWORK_LOG_LEVEL = 0
|
||||
const val KEY_DEBUG_USE_SYSTEM_FONT = "debug_use_system_font"
|
||||
const val DEFAULT_DEBUG_USE_SYSTEM_FONT = false
|
||||
const val KEY_USE_SYSTEM_FONT = "use_system_font"
|
||||
const val DEFAULT_USE_SYSTEM_FONT = false
|
||||
const val KEY_MORE_ANIMATIONS = "more_animations"
|
||||
const val DEFAULT_MORE_ANIMATIONS = false
|
||||
|
||||
|
||||
const val KEY_SHOW_DEBUG_CATEGORY = "show_debug_category"
|
||||
|
||||
const val ID_DMITRY = 37610580
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
interface UserSettings {
|
||||
|
||||
val useContactNames: StateFlow<Boolean>
|
||||
val enablePullToRefresh: StateFlow<Boolean>
|
||||
|
||||
val enableMultiline: StateFlow<Boolean>
|
||||
val darkMode: StateFlow<DarkMode>
|
||||
@@ -28,7 +27,6 @@ interface UserSettings {
|
||||
val showDebugCategory: StateFlow<Boolean>
|
||||
|
||||
fun onUseContactNamesChanged(use: Boolean)
|
||||
fun onEnablePullToRefreshChanged(enable: Boolean)
|
||||
|
||||
fun onEnableMultilineChanged(enable: Boolean)
|
||||
fun onDarkModeChanged(mode: DarkMode)
|
||||
@@ -52,7 +50,6 @@ interface UserSettings {
|
||||
class UserSettingsImpl : UserSettings {
|
||||
|
||||
override val useContactNames = MutableStateFlow(AppSettings.General.useContactNames)
|
||||
override val enablePullToRefresh = MutableStateFlow(AppSettings.General.enablePullToRefresh)
|
||||
|
||||
override val enableMultiline = MutableStateFlow(AppSettings.Appearance.enableMultiline)
|
||||
override val darkMode = MutableStateFlow(AppSettings.Appearance.darkMode)
|
||||
@@ -65,22 +62,18 @@ class UserSettingsImpl : UserSettings {
|
||||
override val sendOnlineStatus = MutableStateFlow(AppSettings.Activity.sendOnlineStatus)
|
||||
|
||||
override val showAlertAfterCrash = MutableStateFlow(AppSettings.Debug.showAlertAfterCrash)
|
||||
override val longPollInBackground = MutableStateFlow(AppSettings.Debug.longPollInBackground)
|
||||
override val useBlur = MutableStateFlow(AppSettings.Debug.useBlur)
|
||||
override val showEmojiButton = MutableStateFlow(AppSettings.Debug.showEmojiButton)
|
||||
override val longPollInBackground = MutableStateFlow(AppSettings.Experimental.longPollInBackground)
|
||||
override val useBlur = MutableStateFlow(AppSettings.Experimental.useBlur)
|
||||
override val showEmojiButton = MutableStateFlow(AppSettings.General.showEmojiButton)
|
||||
override val showTimeInActionMessages =
|
||||
MutableStateFlow(AppSettings.Debug.showTimeInActionMessages)
|
||||
override val useSystemFont = MutableStateFlow(AppSettings.Debug.useSystemFont)
|
||||
MutableStateFlow(AppSettings.Experimental.showTimeInActionMessages)
|
||||
override val useSystemFont = MutableStateFlow(AppSettings.Appearance.useSystemFont)
|
||||
override val showDebugCategory = MutableStateFlow(AppSettings.Debug.showDebugCategory)
|
||||
|
||||
override fun onUseContactNamesChanged(use: Boolean) {
|
||||
useContactNames.value = use
|
||||
}
|
||||
|
||||
override fun onEnablePullToRefreshChanged(enable: Boolean) {
|
||||
enablePullToRefresh.value = enable
|
||||
}
|
||||
|
||||
override fun onEnableMultilineChanged(enable: Boolean) {
|
||||
enableMultiline.value = enable
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package dev.meloda.fast.domain
|
||||
|
||||
import dev.meloda.fast.data.State
|
||||
import dev.meloda.fast.data.api.conversations.ConversationsRepository
|
||||
import dev.meloda.fast.data.mapToState
|
||||
import dev.meloda.fast.model.api.domain.VkConversation
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
||||
class LoadConversationsByIdUseCase(
|
||||
private val conversationsRepository: ConversationsRepository
|
||||
) {
|
||||
|
||||
operator fun invoke(peerIds: List<Int>): Flow<State<List<VkConversation>>> = flow {
|
||||
emit(State.Loading)
|
||||
|
||||
val newState = conversationsRepository
|
||||
.getConversationsById(peerIds = peerIds)
|
||||
.mapToState()
|
||||
|
||||
emit(newState)
|
||||
}
|
||||
}
|
||||
@@ -33,92 +33,100 @@ class OAuthUseCaseImpl(
|
||||
forceSms = forceSms
|
||||
)
|
||||
|
||||
val error = response.error?.let(VkOAuthError::parse)
|
||||
val errorType = response.errorType?.let(VkOAuthErrorType::parse)
|
||||
kotlin.runCatching {
|
||||
val error = response.error?.let(VkOAuthError::parse)
|
||||
val errorType = response.errorType?.let(VkOAuthErrorType::parse)
|
||||
|
||||
val newState = when (error) {
|
||||
null -> {
|
||||
State.Success(
|
||||
AuthInfo(
|
||||
userId = response.userId,
|
||||
accessToken = response.accessToken,
|
||||
validationHash = response.validationHash
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
VkOAuthError.FLOOD_CONTROL -> {
|
||||
State.Error.OAuthError(OAuthErrorDomain.TooManyTriesError)
|
||||
}
|
||||
|
||||
VkOAuthError.NEED_VALIDATION -> {
|
||||
if (response.banInfo != null) {
|
||||
val info = requireNotNull(response.banInfo)
|
||||
|
||||
State.Error.OAuthError(
|
||||
OAuthErrorDomain.UserBannedError(
|
||||
memberName = info.memberName,
|
||||
message = info.message,
|
||||
accessToken = info.accessToken,
|
||||
restoreUrl = info.restoreUrl
|
||||
)
|
||||
)
|
||||
} else {
|
||||
State.Error.OAuthError(
|
||||
OAuthErrorDomain.ValidationRequiredError(
|
||||
description = response.errorDescription.orEmpty(),
|
||||
validationType = response.validationType.orEmpty()
|
||||
.let(ValidationType::parse),
|
||||
validationSid = response.validationSid.orEmpty(),
|
||||
phoneMask = response.phoneMask.orEmpty(),
|
||||
redirectUri = response.redirectUri.orEmpty(),
|
||||
validationResend = response.validationResend,
|
||||
restoreIfCannotGetCode = response.restoreIfCannotGetCode
|
||||
val newState = when (error) {
|
||||
null -> {
|
||||
State.Success(
|
||||
AuthInfo(
|
||||
userId = response.userId,
|
||||
accessToken = response.accessToken,
|
||||
validationHash = response.validationHash
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
VkOAuthError.NEED_CAPTCHA -> {
|
||||
State.Error.OAuthError(
|
||||
OAuthErrorDomain.CaptchaRequiredError(
|
||||
captchaSid = response.captchaSid.orEmpty(),
|
||||
captchaImageUrl = response.captchaImage.orEmpty()
|
||||
VkOAuthError.FLOOD_CONTROL -> {
|
||||
State.Error.OAuthError(OAuthErrorDomain.TooManyTriesError)
|
||||
}
|
||||
|
||||
VkOAuthError.NEED_VALIDATION -> {
|
||||
if (response.banInfo != null) {
|
||||
val info = requireNotNull(response.banInfo)
|
||||
|
||||
State.Error.OAuthError(
|
||||
OAuthErrorDomain.UserBannedError(
|
||||
memberName = info.memberName,
|
||||
message = info.message,
|
||||
accessToken = info.accessToken,
|
||||
restoreUrl = info.restoreUrl
|
||||
)
|
||||
)
|
||||
} else {
|
||||
State.Error.OAuthError(
|
||||
OAuthErrorDomain.ValidationRequiredError(
|
||||
description = response.errorDescription.orEmpty(),
|
||||
validationType = response.validationType.orEmpty()
|
||||
.let(ValidationType::parse),
|
||||
validationSid = response.validationSid.orEmpty(),
|
||||
phoneMask = response.phoneMask.orEmpty(),
|
||||
redirectUri = response.redirectUri.orEmpty(),
|
||||
validationResend = response.validationResend,
|
||||
restoreIfCannotGetCode = response.restoreIfCannotGetCode
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
VkOAuthError.NEED_CAPTCHA -> {
|
||||
State.Error.OAuthError(
|
||||
OAuthErrorDomain.CaptchaRequiredError(
|
||||
captchaSid = response.captchaSid.orEmpty(),
|
||||
captchaImageUrl = response.captchaImage.orEmpty()
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
VkOAuthError.INVALID_CLIENT -> {
|
||||
State.Error.OAuthError(OAuthErrorDomain.InvalidCredentialsError)
|
||||
}
|
||||
VkOAuthError.INVALID_CLIENT -> {
|
||||
State.Error.OAuthError(OAuthErrorDomain.InvalidCredentialsError)
|
||||
}
|
||||
|
||||
VkOAuthError.INVALID_REQUEST -> {
|
||||
when (errorType) {
|
||||
null -> State.Error.OAuthError(OAuthErrorDomain.UnknownError)
|
||||
VkOAuthError.INVALID_REQUEST -> {
|
||||
when (errorType) {
|
||||
null -> State.Error.OAuthError(OAuthErrorDomain.UnknownError)
|
||||
|
||||
VkOAuthErrorType.WRONG_OTP -> {
|
||||
State.Error.OAuthError(OAuthErrorDomain.WrongValidationCode)
|
||||
VkOAuthErrorType.WRONG_OTP -> {
|
||||
State.Error.OAuthError(OAuthErrorDomain.WrongValidationCode)
|
||||
}
|
||||
|
||||
VkOAuthErrorType.WRONG_OTP_FORMAT -> {
|
||||
State.Error.OAuthError(OAuthErrorDomain.WrongValidationCodeFormat)
|
||||
}
|
||||
|
||||
VkOAuthErrorType.PASSWORD_BRUTEFORCE_ATTEMPT -> {
|
||||
State.Error.OAuthError(OAuthErrorDomain.TooManyTriesError)
|
||||
}
|
||||
|
||||
VkOAuthErrorType.USERNAME_OR_PASSWORD_IS_INCORRECT -> {
|
||||
State.Error.OAuthError(OAuthErrorDomain.InvalidCredentialsError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VkOAuthErrorType.WRONG_OTP_FORMAT -> {
|
||||
State.Error.OAuthError(OAuthErrorDomain.WrongValidationCodeFormat)
|
||||
}
|
||||
|
||||
VkOAuthErrorType.PASSWORD_BRUTEFORCE_ATTEMPT -> {
|
||||
State.Error.OAuthError(OAuthErrorDomain.TooManyTriesError)
|
||||
}
|
||||
|
||||
VkOAuthErrorType.USERNAME_OR_PASSWORD_IS_INCORRECT -> {
|
||||
State.Error.OAuthError(OAuthErrorDomain.InvalidCredentialsError)
|
||||
}
|
||||
VkOAuthError.UNKNOWN -> {
|
||||
State.Error.OAuthError(OAuthErrorDomain.UnknownError)
|
||||
}
|
||||
}
|
||||
|
||||
VkOAuthError.UNKNOWN -> {
|
||||
State.Error.OAuthError(OAuthErrorDomain.UnknownError)
|
||||
emit(newState)
|
||||
}.fold(
|
||||
onSuccess = {
|
||||
},
|
||||
onFailure = {
|
||||
emit(State.Error.TestError(it.stackTraceToString()))
|
||||
}
|
||||
}
|
||||
|
||||
emit(newState)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import dev.meloda.fast.domain.AccountUseCaseImpl
|
||||
import dev.meloda.fast.domain.GetCurrentAccountUseCase
|
||||
import dev.meloda.fast.domain.GetLocalUserByIdUseCase
|
||||
import dev.meloda.fast.domain.GetLocalUsersByIdsUseCase
|
||||
import dev.meloda.fast.domain.LoadConversationsByIdUseCase
|
||||
import dev.meloda.fast.domain.LoadUserByIdUseCase
|
||||
import dev.meloda.fast.domain.LoadUsersByIdsUseCase
|
||||
import dev.meloda.fast.domain.StoreUsersUseCase
|
||||
@@ -24,4 +25,6 @@ val domainModule = module {
|
||||
|
||||
singleOf(::AccountUseCaseImpl) bind AccountUseCase::class
|
||||
singleOf(::GetCurrentAccountUseCase)
|
||||
|
||||
singleOf(::LoadConversationsByIdUseCase)
|
||||
}
|
||||
|
||||
@@ -6,4 +6,6 @@ import androidx.compose.runtime.Immutable
|
||||
sealed class BaseError {
|
||||
|
||||
data object SessionExpired : BaseError()
|
||||
|
||||
data class SimpleError(val message: String) : BaseError()
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package dev.meloda.fast.model.api.data
|
||||
|
||||
import dev.meloda.fast.model.api.domain.VkVideoDomain
|
||||
import com.squareup.moshi.JsonClass
|
||||
import dev.meloda.fast.model.api.domain.VkVideoDomain
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class VkVideoData(
|
||||
@@ -12,7 +12,7 @@ data class VkVideoData(
|
||||
val duration: Int,
|
||||
val date: Int,
|
||||
val comments: Int?,
|
||||
val description: String,
|
||||
val description: String?,
|
||||
val player: String?,
|
||||
val added: Int?,
|
||||
val type: String,
|
||||
@@ -20,9 +20,9 @@ data class VkVideoData(
|
||||
val access_key: String?,
|
||||
val owner_id: Int,
|
||||
val is_favorite: Boolean?,
|
||||
val image: List<Image>,
|
||||
val image: List<Image>?,
|
||||
val first_frame: List<FirstFrame>?,
|
||||
val files: File?,
|
||||
val files: File?
|
||||
) : VkAttachmentData {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@@ -67,7 +67,7 @@ data class VkVideoData(
|
||||
fun toDomain() = VkVideoDomain(
|
||||
id = id,
|
||||
ownerId = owner_id,
|
||||
images = image.map { it.asVideoImage() },
|
||||
images = image.orEmpty().map { it.asVideoImage() },
|
||||
firstFrames = first_frame,
|
||||
accessKey = access_key,
|
||||
title = title
|
||||
|
||||
+11
-2
@@ -1,12 +1,12 @@
|
||||
package dev.meloda.fast.model.api.responses
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import dev.meloda.fast.model.api.data.VkContactData
|
||||
import dev.meloda.fast.model.api.data.VkConversationData
|
||||
import dev.meloda.fast.model.api.data.VkGroupData
|
||||
import dev.meloda.fast.model.api.data.VkMessageData
|
||||
import dev.meloda.fast.model.api.data.VkUserData
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ConversationsGetResponse(
|
||||
@@ -18,6 +18,15 @@ data class ConversationsGetResponse(
|
||||
@Json(name = "contacts") val contacts: List<VkContactData>?
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ConversationsGetByIdResponse(
|
||||
@Json(name = "count") val count: Int,
|
||||
@Json(name = "items") val items: List<VkConversationData>,
|
||||
@Json(name = "profiles") val profiles: List<VkUserData>?,
|
||||
@Json(name = "groups") val groups: List<VkGroupData>?,
|
||||
@Json(name = "contacts") val contacts: List<VkContactData>?
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ConversationsResponseItem(
|
||||
@Json(name = "conversation") val conversation: VkConversationData,
|
||||
|
||||
@@ -46,8 +46,13 @@ class ResponseConverterFactory(private val converter: JsonConverter) : Converter
|
||||
return successModel
|
||||
},
|
||||
onFailure = { failure ->
|
||||
if(failure is JsonDataException) {
|
||||
throw failure
|
||||
if (failure is JsonDataException) {
|
||||
throw ApiException(
|
||||
RestApiError(
|
||||
errorCode = -1,
|
||||
errorMsg = failure.message.orEmpty()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val isUnit = successType == Unit::class.java
|
||||
|
||||
@@ -5,6 +5,8 @@ enum class ValidationType(val value: String) {
|
||||
SMS("2fa_sms");
|
||||
|
||||
companion object {
|
||||
fun parse(value: String): ValidationType = entries.first { it.value == value }
|
||||
fun parse(value: String): ValidationType =
|
||||
entries.firstOrNull { it.value == value }
|
||||
?: throw IllegalArgumentException("Unknown validation type $value")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package dev.meloda.fast.network
|
||||
|
||||
enum class VkErrorCode(val code: Int) {
|
||||
WTF(-1),
|
||||
UNKNOWN_ERROR(1),
|
||||
APP_DISABLED(2),
|
||||
UNKNOWN_METHOD(3),
|
||||
|
||||
@@ -6,7 +6,6 @@ import com.slack.eithernet.integration.retrofit.ApiResultCallAdapterFactory
|
||||
import com.slack.eithernet.integration.retrofit.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
|
||||
@@ -57,12 +56,8 @@ val networkModule = module {
|
||||
.followSslRedirects(true)
|
||||
.addInterceptor(
|
||||
HttpLoggingInterceptor().apply {
|
||||
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
|
||||
}
|
||||
level =
|
||||
HttpLoggingInterceptor.Level.entries[AppSettings.Debug.networkLogLevel.ordinal]
|
||||
}
|
||||
)
|
||||
.build()
|
||||
|
||||
+8
-1
@@ -1,10 +1,11 @@
|
||||
package dev.meloda.fast.network.service.conversations
|
||||
|
||||
import com.slack.eithernet.ApiResult
|
||||
import dev.meloda.fast.model.api.responses.ConversationsDeleteResponse
|
||||
import dev.meloda.fast.model.api.responses.ConversationsGetByIdResponse
|
||||
import dev.meloda.fast.model.api.responses.ConversationsGetResponse
|
||||
import dev.meloda.fast.network.ApiResponse
|
||||
import dev.meloda.fast.network.RestApiError
|
||||
import com.slack.eithernet.ApiResult
|
||||
import retrofit2.http.FieldMap
|
||||
import retrofit2.http.FormUrlEncoded
|
||||
import retrofit2.http.POST
|
||||
@@ -17,6 +18,12 @@ interface ConversationsService {
|
||||
@FieldMap params: Map<String, String>
|
||||
): ApiResult<ApiResponse<ConversationsGetResponse>, RestApiError>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST(ConversationsUrls.GET_BY_ID)
|
||||
suspend fun getConversationsById(
|
||||
@FieldMap params: Map<String, String>
|
||||
): ApiResult<ApiResponse<ConversationsGetByIdResponse>, RestApiError>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST(ConversationsUrls.DELETE)
|
||||
suspend fun delete(
|
||||
|
||||
+1
@@ -5,6 +5,7 @@ import dev.meloda.fast.common.AppConstants
|
||||
object ConversationsUrls {
|
||||
|
||||
const val GET = "${AppConstants.URL_API}/messages.getConversations"
|
||||
const val GET_BY_ID = "${AppConstants.URL_API}/messages.getConversationsById"
|
||||
const val DELETE = "${AppConstants.URL_API}/messages.deleteConversation"
|
||||
const val PIN = "${AppConstants.URL_API}/messages.pinConversation"
|
||||
const val UNPIN = "${AppConstants.URL_API}/messages.unpinConversation"
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
@@ -22,7 +23,9 @@ fun ErrorView(
|
||||
onButtonClick: (() -> Unit)? = null,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package dev.meloda.fast.ui.components
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Indication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.IconButtonColors
|
||||
import androidx.compose.material3.IconButtonDefaults
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.LocalUseFallbackRippleImplementation
|
||||
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun IconButton(
|
||||
onClick: () -> Unit = {},
|
||||
onLongClick: (() -> Unit)? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
colors: IconButtonColors = IconButtonDefaults.iconButtonColors(),
|
||||
interactionSource: MutableInteractionSource? = null,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
Box(
|
||||
modifier =
|
||||
modifier
|
||||
.minimumInteractiveComponentSize()
|
||||
.size(IconButtonTokens.StateLayerSize)
|
||||
.clip(IconButtonTokens.StateLayerShape)
|
||||
.background(color = colors.containerColor(enabled))
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
enabled = enabled,
|
||||
interactionSource = interactionSource,
|
||||
indication = rippleOrFallbackImplementation(
|
||||
bounded = false,
|
||||
radius = IconButtonTokens.StateLayerSize / 2
|
||||
)
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
val contentColor = colors.contentColor(enabled)
|
||||
CompositionLocalProvider(LocalContentColor provides contentColor, content = content)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION_ERROR")
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
internal fun rippleOrFallbackImplementation(
|
||||
bounded: Boolean = true,
|
||||
radius: Dp = Dp.Unspecified,
|
||||
color: Color = Color.Unspecified
|
||||
): Indication {
|
||||
return if (LocalUseFallbackRippleImplementation.current) {
|
||||
rememberRipple(bounded, radius, color)
|
||||
} else {
|
||||
ripple(bounded, radius, color)
|
||||
}
|
||||
}
|
||||
|
||||
internal object IconButtonTokens {
|
||||
val StateLayerShape = CircleShape
|
||||
val StateLayerSize = 40.0.dp
|
||||
}
|
||||
|
||||
@Stable
|
||||
internal fun IconButtonColors.containerColor(enabled: Boolean): Color =
|
||||
if (enabled) containerColor else disabledContainerColor
|
||||
|
||||
@Stable
|
||||
internal fun IconButtonColors.contentColor(enabled: Boolean): Color =
|
||||
if (enabled) contentColor else disabledContentColor
|
||||
@@ -133,6 +133,7 @@
|
||||
<string name="message_attachment_story_your_story">Ваша история</string>
|
||||
<string name="settings_dynamic_colors">Динамические цвета</string>
|
||||
<string name="settings_dynamic_colors_description">Цвета для приложения будут извлечены из ваших обоев на главном экране</string>
|
||||
<string name="settings_appearance_use_system_font_title">Использовать системный шрифт</string>
|
||||
<string name="settings_application_language">Язык приложения</string>
|
||||
<string name="settings_application_language_value">Текущий: %1$s</string>
|
||||
<string name="language_system">Системный</string>
|
||||
@@ -177,16 +178,19 @@
|
||||
<string name="settings_general_title">Основное</string>
|
||||
<string name="settings_general_contact_names_title">Использовать имена контактов</string>
|
||||
<string name="settings_general_contact_names_summary">Приложение будет использовать доступные имена контактов для пользователей</string>
|
||||
<string name="settings_general_enable_haptic_title">Включить тактильную отдачу</string>
|
||||
<string name="settings_appearance_title">Внешний вид</string>
|
||||
<string name="settings_appearance_multiline_title">Многострочные заголовки и сообщения</string>
|
||||
<string name="settings_appearance_multiline_summary">Заголовок чата и текст сообщения смогут занимать несколько строчек</string>
|
||||
<string name="settings_features_title">Фичи</string>
|
||||
<string name="settings_features_fast_text_title">Fast текст</string>
|
||||
<string name="settings_features_long_poll_in_background_title">[WIP] LongPoll в фоне</string>
|
||||
<string name="settings_features_long_poll_in_background_title">LongPoll в фоне</string>
|
||||
<string name="settings_features_long_poll_in_background_summary">Ваши сообщения будут обновляться, даже если приложение находится в фоне</string>
|
||||
<string name="settings_experimental_more_animations_summary">Использовать анимации везде, где возможно</string>
|
||||
<string name="settings_activity_title">Активность</string>
|
||||
<string name="settings_activity_send_online_title">Быть «в сети»</string>
|
||||
<string name="settings_activity_send_online_summary">Статус «в сети» будет отправляться каждые 5 минут</string>
|
||||
<string name="settings_experimental_title">Экспериментальные - ОЧЕНЬ нестабильные</string>
|
||||
<string name="settings_debug_title">Отладка</string>
|
||||
<string name="action_disable">Отключить</string>
|
||||
<string name="background_long_poll_rationale_text">Приложение не сможет обновлять сообщения в фоне без доступа к уведомлениям</string>
|
||||
@@ -198,4 +202,14 @@
|
||||
<string name="notification_channel_no_category_description">Уведомления без категории</string>
|
||||
<string name="notification_channel_long_polling_service_name">Сервис обновления сообщений</string>
|
||||
<string name="notification_channel_long_polling_service_description">Уведомления сервиса обновлений сообщений</string>
|
||||
<string name="settings_general_show_emoji_button_title">Показывать кнопку эмоджи</string>
|
||||
<string name="settings_general_show_emoji_button_summary">Показывать кнопку эмоджи на панели чата</string>
|
||||
<string name="settings_features_show_time_in_action_messages_title">Показывать время в сервисных сообщениях</string>
|
||||
<string name="settings_experimental_use_blur_title">Использовать размытие</string>
|
||||
<string name="settings_experimental_use_blur_summary">Добавлять размытие везде, где возможно.\\nРаботает только с 12 версии Android</string>
|
||||
<string name="settings_experimental_more_animations_title">Больше анимаций</string>
|
||||
<string name="warning_confirmation">Подтверждение</string>
|
||||
<string name="captcha_exit_warning">Вы уверены? Процесс ввода капчи будет отменён</string>
|
||||
<string name="validation_exit_warning">Вы уверены? Процесс ввода кода-подтверждения будет отменён</string>
|
||||
<string name="action_authorize">Авторизоваться</string>
|
||||
</resources>
|
||||
|
||||
@@ -204,6 +204,8 @@
|
||||
<string name="settings_dynamic_colors">Dynamic colors</string>
|
||||
<string name="settings_dynamic_colors_description">The colors for the app will be extracted from your home screen wallpaper</string>
|
||||
|
||||
<string name="settings_appearance_use_system_font_title">Use system font</string>
|
||||
|
||||
<string name="settings_application_language">Application Language</string>
|
||||
<string name="settings_application_language_value">Current: %1$s</string>
|
||||
|
||||
@@ -235,16 +237,28 @@
|
||||
<string name="settings_general_title">General</string>
|
||||
<string name="settings_general_contact_names_title">Use contact names</string>
|
||||
<string name="settings_general_contact_names_summary">App will use available contact names for users</string>
|
||||
<string name="settings_general_show_emoji_button_title">Show emoji button</string>
|
||||
<string name="settings_general_show_emoji_button_summary">Show emoji button in chat panel</string>
|
||||
<string name="settings_general_enable_haptic_title">Enable haptic</string>
|
||||
<string name="settings_appearance_title">Appearance</string>
|
||||
<string name="settings_appearance_multiline_title">Multiline titles and messages</string>
|
||||
<string name="settings_appearance_multiline_summary">The title of the conversation and the text of the message can take up multiple lines</string>
|
||||
<string name="settings_features_title">Features</string>
|
||||
<string name="settings_features_fast_text_title">Fast text</string>
|
||||
<string name="settings_features_long_poll_in_background_title">[WIP] LongPoll in background</string>
|
||||
<string name="settings_features_long_poll_in_background_summary">Your messages will be updates even when app is not on the screen</string>
|
||||
<string name="settings_features_long_poll_in_background_title">LongPoll in background</string>
|
||||
<string name="settings_features_long_poll_in_background_summary">Your messages will be updating even when app is not on the screen</string>
|
||||
<string name="settings_features_show_time_in_action_messages_title">Show time in action messages</string>
|
||||
<string name="settings_experimental_use_blur_title">Use blur</string>
|
||||
<string name="settings_experimental_use_blur_summary">Adds blur wherever possible.\nWorks on android 12 and newer</string>
|
||||
<string name="settings_experimental_more_animations_title">More animations</string>
|
||||
<string name="settings_experimental_more_animations_summary">Use animations wherever possible</string>
|
||||
|
||||
<string name="settings_activity_title">Activity</string>
|
||||
<string name="settings_activity_send_online_title">Send online status</string>
|
||||
<string name="settings_activity_send_online_summary">Online status will be sent every five minutes</string>
|
||||
|
||||
<string name="settings_experimental_title">Experimental - VERY unstable</string>
|
||||
|
||||
<string name="settings_debug_title">Debug</string>
|
||||
<string name="background_long_poll_rationale_text">The app won\'t be able to update messages in the background without access to notifications</string>
|
||||
<string name="action_disable">Disable</string>
|
||||
|
||||
@@ -87,6 +87,7 @@ fun NavGraphBuilder.authNavGraph(
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: 17.12.2024, Danil Nikolaev: check clearing backstack from main screen
|
||||
fun NavController.navigateToAuth(clearBackStack: Boolean = false) {
|
||||
val navController = this
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ package dev.meloda.fast.auth.captcha.di
|
||||
import dev.meloda.fast.auth.captcha.CaptchaViewModel
|
||||
import dev.meloda.fast.auth.captcha.CaptchaViewModelImpl
|
||||
import dev.meloda.fast.auth.captcha.validation.CaptchaValidator
|
||||
import org.koin.androidx.viewmodel.dsl.viewModelOf
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.core.module.dsl.viewModelOf
|
||||
import org.koin.dsl.bind
|
||||
import org.koin.dsl.module
|
||||
|
||||
|
||||
@@ -345,6 +345,13 @@ class LoginViewModelImpl(
|
||||
true
|
||||
}
|
||||
|
||||
is State.Error.TestError -> {
|
||||
val message = stateError.message
|
||||
val error = LoginError.SimpleError(message = message)
|
||||
loginError.update { error }
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
@@ -368,7 +375,7 @@ class LoginViewModelImpl(
|
||||
|
||||
private fun startLongPoll() {
|
||||
longPollController.setStateToApply(
|
||||
if (AppSettings.Debug.longPollInBackground) {
|
||||
if (AppSettings.Experimental.longPollInBackground) {
|
||||
LongPollState.Background
|
||||
} else {
|
||||
LongPollState.InApp
|
||||
|
||||
@@ -4,8 +4,8 @@ import dev.meloda.fast.auth.login.LoginViewModelImpl
|
||||
import dev.meloda.fast.domain.OAuthUseCase
|
||||
import dev.meloda.fast.domain.OAuthUseCaseImpl
|
||||
import dev.meloda.fast.auth.login.validation.LoginValidator
|
||||
import org.koin.androidx.viewmodel.dsl.viewModelOf
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.core.module.dsl.viewModelOf
|
||||
import org.koin.dsl.bind
|
||||
import org.koin.dsl.module
|
||||
|
||||
|
||||
@@ -9,4 +9,5 @@ sealed class LoginError {
|
||||
data object TooManyTries : LoginError()
|
||||
data object WrongValidationCode : LoginError()
|
||||
data object WrongValidationCodeFormat : LoginError()
|
||||
data class SimpleError(val message: String): LoginError()
|
||||
}
|
||||
|
||||
@@ -50,8 +50,6 @@ import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import dev.meloda.fast.auth.login.LoginViewModel
|
||||
import dev.meloda.fast.auth.login.LoginViewModelImpl
|
||||
import dev.meloda.fast.auth.login.model.CaptchaArguments
|
||||
import dev.meloda.fast.auth.login.model.LoginError
|
||||
import dev.meloda.fast.auth.login.model.LoginScreenState
|
||||
@@ -441,5 +439,14 @@ fun HandleError(
|
||||
confirmText = stringResource(id = UiR.string.ok)
|
||||
)
|
||||
}
|
||||
|
||||
is LoginError.SimpleError -> {
|
||||
MaterialDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = "Error",
|
||||
text = error.message,
|
||||
confirmText = stringResource(id = UiR.string.ok)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import dev.meloda.fast.domain.AuthUseCaseImpl
|
||||
import dev.meloda.fast.auth.validation.ValidationViewModel
|
||||
import dev.meloda.fast.auth.validation.ValidationViewModelImpl
|
||||
import dev.meloda.fast.auth.validation.validation.ValidationValidator
|
||||
import org.koin.androidx.viewmodel.dsl.viewModelOf
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.core.module.dsl.viewModelOf
|
||||
import org.koin.dsl.bind
|
||||
import org.koin.dsl.module
|
||||
|
||||
|
||||
+6
-14
@@ -62,19 +62,17 @@ import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import dev.chrisbanes.haze.HazeState
|
||||
import dev.chrisbanes.haze.haze
|
||||
import dev.chrisbanes.haze.hazeChild
|
||||
import dev.chrisbanes.haze.hazeEffect
|
||||
import dev.chrisbanes.haze.hazeSource
|
||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||
import dev.chrisbanes.haze.materials.HazeMaterials
|
||||
import dev.meloda.fast.chatmaterials.ChatMaterialsViewModel
|
||||
import dev.meloda.fast.chatmaterials.ChatMaterialsViewModelImpl
|
||||
import dev.meloda.fast.chatmaterials.model.ChatMaterialsScreenState
|
||||
import dev.meloda.fast.chatmaterials.model.UiChatMaterial
|
||||
import dev.meloda.fast.datastore.UserSettings
|
||||
import dev.meloda.fast.ui.R
|
||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import org.koin.compose.koinInject
|
||||
|
||||
@Composable
|
||||
fun ChatMaterialsRoute(
|
||||
@@ -82,15 +80,10 @@ fun ChatMaterialsRoute(
|
||||
onPhotoClicked: (url: String) -> Unit,
|
||||
viewModel: ChatMaterialsViewModel = koinViewModel<ChatMaterialsViewModelImpl>()
|
||||
) {
|
||||
val userSettings: UserSettings = koinInject()
|
||||
|
||||
val enablePullToRefresh by userSettings.enablePullToRefresh.collectAsStateWithLifecycle()
|
||||
|
||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||
|
||||
ChatMaterialsScreen(
|
||||
screenState = screenState,
|
||||
enablePullToRefresh = enablePullToRefresh,
|
||||
onBack = onBack,
|
||||
onTypeChanged = viewModel::onTypeChanged,
|
||||
onRefreshDropdownItemClicked = viewModel::onRefresh,
|
||||
@@ -107,7 +100,6 @@ fun ChatMaterialsRoute(
|
||||
@Composable
|
||||
fun ChatMaterialsScreen(
|
||||
screenState: ChatMaterialsScreenState = ChatMaterialsScreenState.EMPTY,
|
||||
enablePullToRefresh: Boolean = false,
|
||||
onBack: () -> Unit = {},
|
||||
onTypeChanged: (String) -> Unit = {},
|
||||
onRefreshDropdownItemClicked: () -> Unit = {},
|
||||
@@ -145,7 +137,7 @@ fun ChatMaterialsScreen(
|
||||
)
|
||||
}
|
||||
|
||||
val titles = listOf("Photos", "Videos", "Audios", "Files", "Links")
|
||||
val titles = listOf("Photos", "Videos", "Audios")//, "Files", "Links")
|
||||
|
||||
val listState = rememberLazyListState()
|
||||
val gridState = rememberLazyGridState()
|
||||
@@ -187,7 +179,7 @@ fun ChatMaterialsScreen(
|
||||
modifier = Modifier
|
||||
.then(
|
||||
if (currentTheme.enableBlur) {
|
||||
Modifier.hazeChild(
|
||||
Modifier.hazeEffect(
|
||||
state = hazeState,
|
||||
style = hazeStyle
|
||||
)
|
||||
@@ -319,7 +311,7 @@ fun ChatMaterialsScreen(
|
||||
modifier = Modifier
|
||||
.then(
|
||||
if (currentTheme.enableBlur) {
|
||||
Modifier.haze(state = hazeState)
|
||||
Modifier.hazeSource(state = hazeState)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
@@ -354,7 +346,7 @@ fun ChatMaterialsScreen(
|
||||
modifier = Modifier
|
||||
.then(
|
||||
if (currentTheme.enableBlur) {
|
||||
Modifier.haze(state = hazeState)
|
||||
Modifier.hazeSource(state = hazeState)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
|
||||
+249
-95
@@ -1,8 +1,11 @@
|
||||
package dev.meloda.fast.conversations
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import coil.ImageLoader
|
||||
import coil.request.ImageRequest
|
||||
import com.conena.nanokt.collections.indexOfFirstOrNull
|
||||
import dev.meloda.fast.common.extensions.createTimerFlow
|
||||
import dev.meloda.fast.common.extensions.findWithIndex
|
||||
@@ -18,6 +21,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.LoadConversationsByIdUseCase
|
||||
import dev.meloda.fast.domain.LongPollUpdatesParser
|
||||
import dev.meloda.fast.domain.MessagesUseCase
|
||||
import dev.meloda.fast.model.BaseError
|
||||
@@ -26,7 +30,9 @@ import dev.meloda.fast.model.LongPollEvent
|
||||
import dev.meloda.fast.model.api.domain.VkConversation
|
||||
import dev.meloda.fast.network.VkErrorCode
|
||||
import dev.meloda.fast.ui.util.ImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -34,15 +40,16 @@ import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
interface ConversationsViewModel {
|
||||
|
||||
val screenState: StateFlow<ConversationsScreenState>
|
||||
val baseError: StateFlow<BaseError?>
|
||||
val imagesToPreload: StateFlow<List<String>>
|
||||
val currentOffset: StateFlow<Int>
|
||||
val canPaginate: StateFlow<Boolean>
|
||||
val scrollToTop: StateFlow<Boolean>
|
||||
|
||||
fun onPaginationConditionsMet()
|
||||
|
||||
@@ -63,6 +70,10 @@ interface ConversationsViewModel {
|
||||
|
||||
fun setScrollIndex(index: Int)
|
||||
fun setScrollOffset(offset: Int)
|
||||
|
||||
|
||||
fun setScrollToTopFlow(scrollToTopFlow: Flow<Int>)
|
||||
fun onScrolledToTop()
|
||||
}
|
||||
|
||||
class ConversationsViewModelImpl(
|
||||
@@ -70,14 +81,22 @@ class ConversationsViewModelImpl(
|
||||
private val conversationsUseCase: ConversationsUseCase,
|
||||
private val messagesUseCase: MessagesUseCase,
|
||||
private val resources: Resources,
|
||||
private val userSettings: UserSettings
|
||||
private val userSettings: UserSettings,
|
||||
private val imageLoader: ImageLoader,
|
||||
private val applicationContext: Context,
|
||||
private val loadConversationsByIdUseCase: LoadConversationsByIdUseCase
|
||||
) : ConversationsViewModel, ViewModel() {
|
||||
|
||||
override val screenState = MutableStateFlow(ConversationsScreenState.EMPTY)
|
||||
override val baseError = MutableStateFlow<BaseError?>(null)
|
||||
override val imagesToPreload = MutableStateFlow<List<String>>(emptyList())
|
||||
override val currentOffset = MutableStateFlow(0)
|
||||
override val canPaginate = MutableStateFlow(false)
|
||||
override val scrollToTop = MutableStateFlow(false)
|
||||
|
||||
// TODO: 22-Dec-24, Danil Nikolaev: rewrite
|
||||
private val useContactNames = {
|
||||
userSettings.useContactNames.value
|
||||
}
|
||||
|
||||
override fun onPaginationConditionsMet() {
|
||||
currentOffset.update { screenState.value.conversations.size }
|
||||
@@ -115,6 +134,7 @@ class ConversationsViewModelImpl(
|
||||
}
|
||||
|
||||
override fun onRefresh() {
|
||||
baseError.setValue { null }
|
||||
loadConversations(offset = 0)
|
||||
}
|
||||
|
||||
@@ -217,6 +237,20 @@ class ConversationsViewModelImpl(
|
||||
screenState.setValue { old -> old.copy(scrollOffset = offset) }
|
||||
}
|
||||
|
||||
override fun setScrollToTopFlow(scrollToTopFlow: Flow<Int>) {
|
||||
scrollToTopFlow.listenValue(viewModelScope) { index ->
|
||||
if (index == 1) {
|
||||
scrollToTop.emit(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onScrolledToTop() {
|
||||
viewModelScope.launch(Dispatchers.Main) {
|
||||
scrollToTop.emit(false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideOptions(conversationId: Int) {
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
@@ -240,17 +274,7 @@ class ConversationsViewModelImpl(
|
||||
conversationsUseCase.getConversations(count = LOAD_COUNT, offset = offset)
|
||||
.listenValue(viewModelScope) { state ->
|
||||
state.processState(
|
||||
error = { error ->
|
||||
if (error is State.Error.ApiError) {
|
||||
when (error.errorCode) {
|
||||
VkErrorCode.USER_AUTHORIZATION_FAILED -> {
|
||||
baseError.setValue { BaseError.SessionExpired }
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
},
|
||||
error = ::handleError,
|
||||
success = { response ->
|
||||
val itemsCountSufficient = response.size == LOAD_COUNT
|
||||
canPaginate.setValue { itemsCountSufficient }
|
||||
@@ -258,9 +282,17 @@ class ConversationsViewModelImpl(
|
||||
val paginationExhausted = !itemsCountSufficient &&
|
||||
screenState.value.conversations.isNotEmpty()
|
||||
|
||||
imagesToPreload.setValue {
|
||||
val imagesToPreload =
|
||||
response.mapNotNull { it.extractAvatar().extractUrl() }
|
||||
|
||||
imagesToPreload.forEach { url ->
|
||||
imageLoader.enqueue(
|
||||
ImageRequest.Builder(applicationContext)
|
||||
.data(url)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
conversationsUseCase.storeConversations(response)
|
||||
|
||||
val loadedConversations = response.map {
|
||||
@@ -298,6 +330,40 @@ class ConversationsViewModelImpl(
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleError(error: State.Error) {
|
||||
when (error) {
|
||||
is State.Error.ApiError -> {
|
||||
when (error.errorCode) {
|
||||
VkErrorCode.USER_AUTHORIZATION_FAILED -> {
|
||||
baseError.setValue { BaseError.SessionExpired }
|
||||
}
|
||||
|
||||
else -> {
|
||||
baseError.setValue {
|
||||
BaseError.SimpleError(message = error.errorMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
State.Error.ConnectionError -> {
|
||||
baseError.setValue {
|
||||
BaseError.SimpleError(message = "Connection error")
|
||||
}
|
||||
}
|
||||
State.Error.InternalError -> {
|
||||
baseError.setValue {
|
||||
BaseError.SimpleError(message = "Internal error")
|
||||
}
|
||||
}
|
||||
State.Error.UnknownError -> {
|
||||
baseError.setValue {
|
||||
BaseError.SimpleError(message = "Unknown error")
|
||||
}
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteConversation(peerId: Int) {
|
||||
conversationsUseCase.delete(peerId).listenValue(viewModelScope) { state ->
|
||||
state.processState(
|
||||
@@ -314,7 +380,12 @@ class ConversationsViewModelImpl(
|
||||
conversations.update { newConversations }
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
conversations = newConversations.map { it.asPresentation(resources) }
|
||||
conversations = newConversations.map {
|
||||
it.asPresentation(
|
||||
resources = resources,
|
||||
useContactName = useContactNames()
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -350,13 +421,40 @@ class ConversationsViewModelImpl(
|
||||
|
||||
private fun handleNewMessage(event: LongPollEvent.VkMessageNewEvent) {
|
||||
val message = event.message
|
||||
|
||||
val newConversations = conversations.value.toMutableList()
|
||||
val conversationIndex =
|
||||
newConversations.indexOfFirstOrNull { it.id == message.peerId }
|
||||
|
||||
if (conversationIndex == null) { // диалога нет в списке
|
||||
// pizdets
|
||||
// TODO: 04/07/2024, Danil Nikolaev: load conversation and store info
|
||||
if (conversationIndex == null) {
|
||||
loadConversationsByIdUseCase(peerIds = listOf(message.peerId))
|
||||
.listenValue(viewModelScope) { state ->
|
||||
state.processState(
|
||||
error = { error ->
|
||||
|
||||
},
|
||||
success = { response ->
|
||||
val conversation = (response.firstOrNull() ?: return@listenValue)
|
||||
.copy(lastMessage = message)
|
||||
|
||||
// TODO: 22-Dec-24, Danil Nikolaev: handle interactions and pinned state
|
||||
|
||||
newConversations.add(pinnedConversationsCount.value, conversation)
|
||||
conversations.update { newConversations }
|
||||
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
conversations = newConversations.map {
|
||||
it.asPresentation(
|
||||
resources = resources,
|
||||
useContactName = useContactNames()
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val conversation = newConversations[conversationIndex]
|
||||
var newConversation = conversation.copy(
|
||||
@@ -397,7 +495,12 @@ class ConversationsViewModelImpl(
|
||||
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
conversations = newConversations.map { it.asPresentation(resources) }
|
||||
conversations = newConversations.map {
|
||||
it.asPresentation(
|
||||
resources = resources,
|
||||
useContactName = useContactNames()
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -421,7 +524,12 @@ class ConversationsViewModelImpl(
|
||||
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
conversations = newConversations.map { it.asPresentation(resources) }
|
||||
conversations = newConversations.map {
|
||||
it.asPresentation(
|
||||
resources = resources,
|
||||
useContactName = useContactNames()
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -431,20 +539,29 @@ class ConversationsViewModelImpl(
|
||||
val newConversations = conversations.value.toMutableList()
|
||||
|
||||
val conversationIndex =
|
||||
newConversations.indexOfFirstOrNull { it.id == event.peerId } ?: return
|
||||
newConversations.indexOfFirstOrNull { it.id == event.peerId }
|
||||
|
||||
newConversations[conversationIndex] =
|
||||
newConversations[conversationIndex].copy(
|
||||
inRead = event.messageId,
|
||||
unreadCount = event.unreadCount
|
||||
)
|
||||
if (conversationIndex == null) { // диалога нет в списке
|
||||
// pizdets
|
||||
} else {
|
||||
newConversations[conversationIndex] =
|
||||
newConversations[conversationIndex].copy(
|
||||
inRead = event.messageId,
|
||||
unreadCount = event.unreadCount
|
||||
)
|
||||
|
||||
conversations.update { newConversations }
|
||||
conversations.update { newConversations }
|
||||
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
conversations = newConversations.map { it.asPresentation(resources) }
|
||||
)
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
conversations = newConversations.map {
|
||||
it.asPresentation(
|
||||
resources = resources,
|
||||
useContactName = useContactNames()
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -452,19 +569,28 @@ class ConversationsViewModelImpl(
|
||||
val newConversations = conversations.value.toMutableList()
|
||||
|
||||
val conversationIndex =
|
||||
newConversations.indexOfFirstOrNull { it.id == event.peerId } ?: return
|
||||
newConversations.indexOfFirstOrNull { it.id == event.peerId }
|
||||
|
||||
newConversations[conversationIndex] =
|
||||
newConversations[conversationIndex].copy(
|
||||
outRead = event.messageId,
|
||||
unreadCount = event.unreadCount
|
||||
)
|
||||
if (conversationIndex == null) { // диалога нет в списке
|
||||
// pizdets
|
||||
} else {
|
||||
newConversations[conversationIndex] =
|
||||
newConversations[conversationIndex].copy(
|
||||
outRead = event.messageId,
|
||||
unreadCount = event.unreadCount
|
||||
)
|
||||
|
||||
conversations.update { newConversations }
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
conversations = newConversations.map { it.asPresentation(resources) }
|
||||
)
|
||||
conversations.update { newConversations }
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
conversations = newConversations.map {
|
||||
it.asPresentation(
|
||||
resources = resources,
|
||||
useContactName = useContactNames()
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -473,34 +599,43 @@ class ConversationsViewModelImpl(
|
||||
val newConversations = conversations.value.toMutableList()
|
||||
|
||||
val conversationIndex =
|
||||
newConversations.indexOfFirstOrNull { it.id == event.peerId } ?: return
|
||||
newConversations.indexOfFirstOrNull { it.id == event.peerId }
|
||||
|
||||
val pin = event.majorId > 0
|
||||
|
||||
val conversation = newConversations[conversationIndex].copy(majorId = event.majorId)
|
||||
|
||||
newConversations.removeAt(conversationIndex)
|
||||
|
||||
if (pin) {
|
||||
newConversations.add(0, conversation)
|
||||
if (conversationIndex == null) { // диалога нет в списке
|
||||
// pizdets
|
||||
} else {
|
||||
pinnedCount -= 1
|
||||
val pin = event.majorId > 0
|
||||
|
||||
newConversations.add(conversation)
|
||||
val conversation = newConversations[conversationIndex].copy(majorId = event.majorId)
|
||||
|
||||
val pinnedSubList = newConversations.filter(VkConversation::isPinned)
|
||||
val unpinnedSubList = newConversations
|
||||
.filterNot(VkConversation::isPinned)
|
||||
.sortedByDescending { it.lastMessage?.date }
|
||||
newConversations.removeAt(conversationIndex)
|
||||
|
||||
newConversations.clear()
|
||||
newConversations += pinnedSubList + unpinnedSubList
|
||||
}
|
||||
if (pin) {
|
||||
newConversations.add(0, conversation)
|
||||
} else {
|
||||
pinnedCount -= 1
|
||||
|
||||
conversations.update { newConversations }
|
||||
newConversations.add(conversation)
|
||||
|
||||
screenState.setValue { old ->
|
||||
old.copy(conversations = newConversations.map { it.asPresentation(resources) })
|
||||
val pinnedSubList = newConversations.filter(VkConversation::isPinned)
|
||||
val unpinnedSubList = newConversations
|
||||
.filterNot(VkConversation::isPinned)
|
||||
.sortedByDescending { it.lastMessage?.date }
|
||||
|
||||
newConversations.clear()
|
||||
newConversations += pinnedSubList + unpinnedSubList
|
||||
}
|
||||
|
||||
conversations.update { newConversations }
|
||||
|
||||
screenState.setValue { old ->
|
||||
old.copy(conversations = newConversations.map {
|
||||
it.asPresentation(
|
||||
resources = resources,
|
||||
useContactName = useContactNames()
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -520,44 +655,53 @@ class ConversationsViewModelImpl(
|
||||
|
||||
val newConversations = conversations.value.toMutableList()
|
||||
val conversationAndIndex =
|
||||
newConversations.findWithIndex { it.id == peerId } ?: return
|
||||
newConversations.findWithIndex { it.id == peerId }
|
||||
|
||||
newConversations[conversationAndIndex.first] =
|
||||
conversationAndIndex.second.copy(
|
||||
interactionType = interactionType.value,
|
||||
interactionIds = userIds
|
||||
)
|
||||
if (conversationAndIndex == null) { // диалога нет в списке
|
||||
// pizdets
|
||||
} else {
|
||||
newConversations[conversationAndIndex.first] =
|
||||
conversationAndIndex.second.copy(
|
||||
interactionType = interactionType.value,
|
||||
interactionIds = userIds
|
||||
)
|
||||
|
||||
conversations.update { newConversations }
|
||||
conversations.update { newConversations }
|
||||
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
conversations = newConversations.map { it.asPresentation(resources) }
|
||||
)
|
||||
}
|
||||
|
||||
interactionsTimers[peerId]?.let { interactionJob ->
|
||||
if (interactionJob.interactionType == interactionType) {
|
||||
interactionJob.timerJob.cancel(NewInteractionException)
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
conversations = newConversations.map {
|
||||
it.asPresentation(
|
||||
resources = resources,
|
||||
useContactName = useContactNames()
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
var timeoutAction: (() -> Unit)? = null
|
||||
interactionsTimers[peerId]?.let { interactionJob ->
|
||||
if (interactionJob.interactionType == interactionType) {
|
||||
interactionJob.timerJob.cancel(NewInteractionException)
|
||||
}
|
||||
}
|
||||
|
||||
val timerJob = createTimerFlow(
|
||||
time = 5,
|
||||
onTimeoutAction = { timeoutAction?.invoke() }
|
||||
).launchIn(viewModelScope)
|
||||
var timeoutAction: (() -> Unit)? = null
|
||||
|
||||
val newInteractionJob = InteractionJob(
|
||||
interactionType = interactionType,
|
||||
timerJob = timerJob
|
||||
)
|
||||
val timerJob = createTimerFlow(
|
||||
time = 5,
|
||||
onTimeoutAction = { timeoutAction?.invoke() }
|
||||
).launchIn(viewModelScope)
|
||||
|
||||
interactionsTimers[peerId] = newInteractionJob
|
||||
val newInteractionJob = InteractionJob(
|
||||
interactionType = interactionType,
|
||||
timerJob = timerJob
|
||||
)
|
||||
|
||||
timeoutAction = {
|
||||
stopInteraction(peerId, newInteractionJob)
|
||||
interactionsTimers[peerId] = newInteractionJob
|
||||
|
||||
timeoutAction = {
|
||||
stopInteraction(peerId, newInteractionJob)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -577,7 +721,12 @@ class ConversationsViewModelImpl(
|
||||
conversations.update { newConversations }
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
conversations = newConversations.map { it.asPresentation(resources) }
|
||||
conversations = newConversations.map {
|
||||
it.asPresentation(
|
||||
resources = resources,
|
||||
useContactName = useContactNames()
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -606,7 +755,12 @@ class ConversationsViewModelImpl(
|
||||
conversations.update { newConversations }
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
conversations = newConversations.map { it.asPresentation(resources) }
|
||||
conversations = newConversations.map {
|
||||
it.asPresentation(
|
||||
resources = resources,
|
||||
useContactName = useContactNames()
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -1,14 +1,14 @@
|
||||
package dev.meloda.fast.conversations.di
|
||||
|
||||
import dev.meloda.fast.conversations.ConversationsViewModelImpl
|
||||
import dev.meloda.fast.domain.ConversationsUseCaseImpl
|
||||
import dev.meloda.fast.domain.ConversationsUseCase
|
||||
import org.koin.androidx.viewmodel.dsl.viewModelOf
|
||||
import dev.meloda.fast.domain.ConversationsUseCaseImpl
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.core.module.dsl.viewModelOf
|
||||
import org.koin.dsl.bind
|
||||
import org.koin.dsl.module
|
||||
|
||||
val conversationsModule = module {
|
||||
singleOf(::ConversationsUseCaseImpl) bind dev.meloda.fast.domain.ConversationsUseCase::class
|
||||
singleOf(::ConversationsUseCaseImpl) bind ConversationsUseCase::class
|
||||
viewModelOf(::ConversationsViewModelImpl)
|
||||
}
|
||||
|
||||
+3
@@ -8,6 +8,7 @@ import dev.meloda.fast.conversations.ConversationsViewModelImpl
|
||||
import dev.meloda.fast.conversations.presentation.ConversationsRoute
|
||||
import dev.meloda.fast.model.BaseError
|
||||
import dev.meloda.fast.ui.extensions.sharedViewModel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@@ -17,11 +18,13 @@ fun NavGraphBuilder.conversationsScreen(
|
||||
onError: (BaseError) -> Unit,
|
||||
onConversationItemClicked: (id: Int) -> Unit,
|
||||
onPhotoClicked: (url: String) -> Unit,
|
||||
scrollToTopFlow: Flow<Int>,
|
||||
navController: NavController,
|
||||
) {
|
||||
composable<Conversations> {
|
||||
val viewModel: ConversationsViewModel =
|
||||
it.sharedViewModel<ConversationsViewModelImpl>(navController = navController)
|
||||
viewModel.setScrollToTopFlow(scrollToTopFlow)
|
||||
|
||||
ConversationsRoute(
|
||||
onError = onError,
|
||||
|
||||
+34
-25
@@ -54,7 +54,6 @@ import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -65,8 +64,6 @@ import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.view.HapticFeedbackConstantsCompat
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import coil.imageLoader
|
||||
import coil.request.ImageRequest
|
||||
import dev.chrisbanes.haze.haze
|
||||
import dev.chrisbanes.haze.hazeChild
|
||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||
@@ -96,22 +93,10 @@ fun ConversationsRoute(
|
||||
onConversationPhotoClicked: (url: String) -> Unit,
|
||||
viewModel: ConversationsViewModel
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||
val baseError by viewModel.baseError.collectAsStateWithLifecycle()
|
||||
val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle()
|
||||
|
||||
val imagesToPreload by viewModel.imagesToPreload.collectAsStateWithLifecycle()
|
||||
LaunchedEffect(imagesToPreload) {
|
||||
imagesToPreload.forEach { url ->
|
||||
context.imageLoader.enqueue(
|
||||
ImageRequest.Builder(context)
|
||||
.data(url)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}
|
||||
val isNeedToScrollToTop by viewModel.scrollToTop.collectAsStateWithLifecycle()
|
||||
|
||||
ConversationsScreen(
|
||||
screenState = screenState,
|
||||
@@ -129,7 +114,9 @@ fun ConversationsRoute(
|
||||
onRefresh = viewModel::onRefresh,
|
||||
onConversationPhotoClicked = onConversationPhotoClicked,
|
||||
setScrollIndex = viewModel::setScrollIndex,
|
||||
setScrollOffset = viewModel::setScrollOffset
|
||||
setScrollOffset = viewModel::setScrollOffset,
|
||||
isNeedToScrollToTop = isNeedToScrollToTop,
|
||||
onScrolledToTop = viewModel::onScrolledToTop
|
||||
)
|
||||
|
||||
HandleDialogs(
|
||||
@@ -156,7 +143,9 @@ fun ConversationsScreen(
|
||||
onRefresh: () -> Unit = {},
|
||||
onConversationPhotoClicked: (url: String) -> Unit = {},
|
||||
setScrollIndex: (Int) -> Unit = {},
|
||||
setScrollOffset: (Int) -> Unit = {}
|
||||
setScrollOffset: (Int) -> Unit = {},
|
||||
isNeedToScrollToTop: Boolean = false,
|
||||
onScrolledToTop: () -> Unit = {}
|
||||
) {
|
||||
val view = LocalView.current
|
||||
val currentTheme = LocalThemeConfig.current
|
||||
@@ -170,6 +159,14 @@ fun ConversationsScreen(
|
||||
initialFirstVisibleItemScrollOffset = screenState.scrollOffset
|
||||
)
|
||||
|
||||
LaunchedEffect(isNeedToScrollToTop) {
|
||||
if (isNeedToScrollToTop) {
|
||||
listState.scrollToItem(0)
|
||||
onScrolledToTop()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(listState) {
|
||||
snapshotFlow { listState.firstVisibleItemIndex }
|
||||
.debounce(500L)
|
||||
@@ -310,7 +307,7 @@ fun ConversationsScreen(
|
||||
) {
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
if (AppSettings.Debug.enableHaptic) {
|
||||
if (AppSettings.General.enableHaptic) {
|
||||
view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT)
|
||||
}
|
||||
scope.launch {
|
||||
@@ -342,12 +339,24 @@ fun ConversationsScreen(
|
||||
}
|
||||
) { padding ->
|
||||
when {
|
||||
baseError is BaseError.SessionExpired -> {
|
||||
ErrorView(
|
||||
text = "Session expired",
|
||||
buttonText = "Log out",
|
||||
onButtonClick = onSessionExpiredLogOutButtonClicked
|
||||
)
|
||||
baseError != null -> {
|
||||
when (baseError) {
|
||||
is BaseError.SessionExpired -> {
|
||||
ErrorView(
|
||||
text = "Session expired",
|
||||
buttonText = "Log out",
|
||||
onButtonClick = onSessionExpiredLogOutButtonClicked
|
||||
)
|
||||
}
|
||||
|
||||
is BaseError.SimpleError -> {
|
||||
ErrorView(
|
||||
text = baseError.message,
|
||||
buttonText = "Try again",
|
||||
onButtonClick = onRefresh
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
screenState.isLoading && screenState.conversations.isEmpty() -> FullScreenLoader()
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ import dev.meloda.fast.ui.R as UiR
|
||||
|
||||
fun VkConversation.asPresentation(
|
||||
resources: Resources,
|
||||
useContactName: Boolean = false
|
||||
useContactName: Boolean
|
||||
): UiConversation = UiConversation(
|
||||
id = id,
|
||||
lastMessageId = lastMessageId,
|
||||
|
||||
@@ -8,6 +8,7 @@ import dev.meloda.fast.data.State
|
||||
import dev.meloda.fast.data.processState
|
||||
import dev.meloda.fast.datastore.UserSettings
|
||||
import dev.meloda.fast.domain.FriendsUseCase
|
||||
import dev.meloda.fast.domain.LoadUsersByIdsUseCase
|
||||
import dev.meloda.fast.friends.model.FriendsScreenState
|
||||
import dev.meloda.fast.friends.util.asPresentation
|
||||
import dev.meloda.fast.model.BaseError
|
||||
@@ -42,7 +43,8 @@ interface FriendsViewModel {
|
||||
|
||||
class FriendsViewModelImpl(
|
||||
private val friendsUseCase: FriendsUseCase,
|
||||
private val userSettings: UserSettings
|
||||
private val userSettings: UserSettings,
|
||||
private val loadUsersByIdsUseCase: LoadUsersByIdsUseCase
|
||||
) : ViewModel(), FriendsViewModel {
|
||||
|
||||
override val screenState = MutableStateFlow(FriendsScreenState.EMPTY)
|
||||
@@ -94,6 +96,49 @@ class FriendsViewModelImpl(
|
||||
}
|
||||
|
||||
private fun loadFriends(offset: Int = currentOffset.value) {
|
||||
friendsUseCase.getOnlineFriends(null, null)
|
||||
.listenValue(viewModelScope) { state ->
|
||||
state.processState(
|
||||
error = { error ->
|
||||
if (error is State.Error.ApiError) {
|
||||
when (error.errorCode) {
|
||||
VkErrorCode.USER_AUTHORIZATION_FAILED -> {
|
||||
baseError.setValue { BaseError.SessionExpired }
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
},
|
||||
success = { userIds ->
|
||||
loadUsersByIdsUseCase(userIds = userIds)
|
||||
.listenValue(viewModelScope) { state ->
|
||||
state.processState(
|
||||
error = { error ->
|
||||
if (error is State.Error.ApiError) {
|
||||
when (error.errorCode) {
|
||||
VkErrorCode.USER_AUTHORIZATION_FAILED -> {
|
||||
baseError.setValue { BaseError.SessionExpired }
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
},
|
||||
success = { onlineFriends ->
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
onlineFriends = onlineFriends.map {
|
||||
it.asPresentation(userSettings.useContactNames.value)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
friendsUseCase.getFriends(count = LOAD_COUNT, offset = offset)
|
||||
.listenValue(viewModelScope) { state ->
|
||||
state.processState(
|
||||
@@ -125,10 +170,6 @@ class FriendsViewModelImpl(
|
||||
it.asPresentation(userSettings.useContactNames.value)
|
||||
}
|
||||
|
||||
val loadedOnlineFriends = loadedFriends.filter {
|
||||
it.onlineStatus.isOnline()
|
||||
}
|
||||
|
||||
val newState = screenState.value.copy(
|
||||
isPaginationExhausted = paginationExhausted
|
||||
)
|
||||
@@ -136,18 +177,12 @@ class FriendsViewModelImpl(
|
||||
if (offset == 0) {
|
||||
friends.emit(response)
|
||||
screenState.setValue {
|
||||
newState.copy(
|
||||
friends = loadedFriends,
|
||||
onlineFriends = loadedOnlineFriends
|
||||
)
|
||||
newState.copy(friends = loadedFriends)
|
||||
}
|
||||
} else {
|
||||
friends.emit(friends.value.plus(response))
|
||||
screenState.setValue {
|
||||
newState.copy(
|
||||
friends = newState.friends.plus(loadedFriends),
|
||||
onlineFriends = newState.onlineFriends.plus(loadedOnlineFriends)
|
||||
)
|
||||
newState.copy(friends = newState.friends.plus(loadedFriends))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,12 @@ package dev.meloda.fast.friends.di
|
||||
import dev.meloda.fast.domain.FriendsUseCase
|
||||
import dev.meloda.fast.friends.FriendsViewModelImpl
|
||||
import dev.meloda.fast.domain.FriendsUseCaseImpl
|
||||
import org.koin.androidx.viewmodel.dsl.viewModelOf
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.core.module.dsl.viewModelOf
|
||||
import org.koin.dsl.bind
|
||||
import org.koin.dsl.module
|
||||
|
||||
val friendsModule = module {
|
||||
singleOf(::FriendsUseCaseImpl) bind dev.meloda.fast.domain.FriendsUseCase::class
|
||||
|
||||
singleOf(::FriendsUseCaseImpl) bind FriendsUseCase::class
|
||||
viewModelOf(::FriendsViewModelImpl)
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
package dev.meloda.fast.languagepicker.di
|
||||
|
||||
import dev.meloda.fast.languagepicker.LanguagePickerViewModelImpl
|
||||
import org.koin.androidx.viewmodel.dsl.viewModelOf
|
||||
import org.koin.core.module.dsl.viewModelOf
|
||||
import org.koin.dsl.module
|
||||
|
||||
val languagePickerModule = module {
|
||||
|
||||
+14
-17
@@ -1,15 +1,15 @@
|
||||
package dev.meloda.fast.messageshistory
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.core.content.edit
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.conena.nanokt.collections.indexOfFirstOrNull
|
||||
import com.conena.nanokt.collections.indexOfOrNull
|
||||
import com.conena.nanokt.text.isEmptyOrBlank
|
||||
import com.conena.nanokt.text.isNotEmptyOrBlank
|
||||
import dev.meloda.fast.common.extensions.listenValue
|
||||
import dev.meloda.fast.common.extensions.setValue
|
||||
import dev.meloda.fast.common.provider.ResourceProvider
|
||||
@@ -17,9 +17,9 @@ import dev.meloda.fast.data.UserConfig
|
||||
import dev.meloda.fast.data.VkMemoryCache
|
||||
import dev.meloda.fast.data.processState
|
||||
import dev.meloda.fast.datastore.AppSettings
|
||||
import dev.meloda.fast.datastore.SettingsKeys
|
||||
import dev.meloda.fast.datastore.UserSettings
|
||||
import dev.meloda.fast.domain.ConversationsUseCase
|
||||
import dev.meloda.fast.domain.LoadConversationsByIdUseCase
|
||||
import dev.meloda.fast.domain.LongPollUpdatesParser
|
||||
import dev.meloda.fast.domain.MessagesUseCase
|
||||
import dev.meloda.fast.messageshistory.model.ActionMode
|
||||
@@ -54,19 +54,18 @@ interface MessagesHistoryViewModel {
|
||||
fun onRefresh()
|
||||
fun onAttachmentButtonClicked()
|
||||
fun onMessageInputChanged(newText: TextFieldValue)
|
||||
fun onEmojiButtonClicked()
|
||||
fun onEmojiButtonLongClicked()
|
||||
fun onActionButtonClicked()
|
||||
|
||||
fun onPaginationConditionsMet()
|
||||
fun onToggleAnimationsDropdownItemClicked(enableAnimations: Boolean)
|
||||
}
|
||||
|
||||
class MessagesHistoryViewModelImpl(
|
||||
private val messagesUseCase: MessagesUseCase,
|
||||
private val conversationsUseCase: ConversationsUseCase,
|
||||
private val preferences: SharedPreferences,
|
||||
private val resourceProvider: ResourceProvider,
|
||||
private val userSettings: UserSettings,
|
||||
private val loadConversationsByIdUseCase: LoadConversationsByIdUseCase,
|
||||
updatesParser: LongPollUpdatesParser,
|
||||
savedStateHandle: SavedStateHandle
|
||||
) : MessagesHistoryViewModel, ViewModel() {
|
||||
@@ -123,8 +122,15 @@ class MessagesHistoryViewModelImpl(
|
||||
screenState.setValue { old -> old.copy(message = newText) }
|
||||
}
|
||||
|
||||
override fun onEmojiButtonClicked() {
|
||||
|
||||
override fun onEmojiButtonLongClicked() {
|
||||
AppSettings.Features.fastText.takeIf { it.isNotEmptyOrBlank() }?.let { text ->
|
||||
screenState.setValue { old ->
|
||||
val newText = "${old.message.text}$text"
|
||||
old.copy(
|
||||
message = TextFieldValue(text = newText, selection = TextRange(newText.length))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActionButtonClicked() {
|
||||
@@ -150,15 +156,6 @@ class MessagesHistoryViewModelImpl(
|
||||
loadMessagesHistory()
|
||||
}
|
||||
|
||||
override fun onToggleAnimationsDropdownItemClicked(enableAnimations: Boolean) {
|
||||
preferences.edit {
|
||||
putBoolean(
|
||||
SettingsKeys.KEY_ENABLE_ANIMATIONS_IN_MESSAGES,
|
||||
enableAnimations
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleNewMessage(event: LongPollEvent.VkMessageNewEvent) {
|
||||
val message = event.message
|
||||
|
||||
|
||||
+3
-3
@@ -1,17 +1,17 @@
|
||||
package dev.meloda.fast.messageshistory.di
|
||||
|
||||
import dev.meloda.fast.domain.MessagesUseCase
|
||||
import dev.meloda.fast.domain.MessagesUseCaseImpl
|
||||
import dev.meloda.fast.messageshistory.MessagesHistoryViewModel
|
||||
import dev.meloda.fast.messageshistory.MessagesHistoryViewModelImpl
|
||||
import dev.meloda.fast.domain.MessagesUseCaseImpl
|
||||
import dev.meloda.fast.messageshistory.validation.MessagesHistoryValidator
|
||||
import org.koin.androidx.viewmodel.dsl.viewModelOf
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.core.module.dsl.viewModelOf
|
||||
import org.koin.dsl.bind
|
||||
import org.koin.dsl.module
|
||||
|
||||
val messagesHistoryModule = module {
|
||||
singleOf(::MessagesUseCaseImpl) bind dev.meloda.fast.domain.MessagesUseCase::class
|
||||
singleOf(::MessagesUseCaseImpl) bind MessagesUseCase::class
|
||||
singleOf(::MessagesHistoryValidator)
|
||||
viewModelOf(::MessagesHistoryViewModelImpl) bind MessagesHistoryViewModel::class
|
||||
}
|
||||
|
||||
+2
@@ -18,6 +18,7 @@ data class MessagesHistoryScreenState(
|
||||
val isPaginating: Boolean,
|
||||
val isPaginationExhausted: Boolean,
|
||||
val actionMode: ActionMode,
|
||||
val chatImageUrl: String?
|
||||
) {
|
||||
|
||||
companion object {
|
||||
@@ -33,6 +34,7 @@ data class MessagesHistoryScreenState(
|
||||
isPaginating = false,
|
||||
isPaginationExhausted = false,
|
||||
actionMode = ActionMode.Record,
|
||||
chatImageUrl = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+65
-46
@@ -1,10 +1,12 @@
|
||||
package dev.meloda.fast.messageshistory.presentation
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -23,9 +25,11 @@ import androidx.compose.foundation.layout.imeNestedScroll
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
||||
@@ -37,7 +41,6 @@ import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
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
|
||||
@@ -60,6 +63,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
@@ -72,12 +76,12 @@ 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 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
|
||||
import dev.meloda.fast.messageshistory.MessagesHistoryViewModelImpl
|
||||
@@ -86,8 +90,10 @@ import dev.meloda.fast.messageshistory.model.MessagesHistoryScreenState
|
||||
import dev.meloda.fast.messageshistory.util.firstMessage
|
||||
import dev.meloda.fast.messageshistory.util.indexOfMessageByCmId
|
||||
import dev.meloda.fast.model.BaseError
|
||||
import dev.meloda.fast.ui.components.IconButton
|
||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||
import dev.meloda.fast.ui.util.ImmutableList
|
||||
import dev.meloda.fast.ui.util.getImage
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import org.koin.compose.koinInject
|
||||
@@ -115,11 +121,11 @@ fun MessagesHistoryRoute(
|
||||
onBack = onBack,
|
||||
onChatMaterialsDropdownItemClicked = onChatMaterialsDropdownItemClicked,
|
||||
onRefreshDropdownItemClicked = viewModel::onRefresh,
|
||||
onToggleAnimationsDropdownItemClicked = viewModel::onToggleAnimationsDropdownItemClicked,
|
||||
onPaginationConditionsMet = viewModel::onPaginationConditionsMet,
|
||||
onMessageInputChanged = viewModel::onMessageInputChanged,
|
||||
onAttachmentButtonClicked = viewModel::onAttachmentButtonClicked,
|
||||
onActionButtonClicked = viewModel::onActionButtonClicked
|
||||
onActionButtonClicked = viewModel::onActionButtonClicked,
|
||||
onEmojiButtonLongClicked = viewModel::onEmojiButtonLongClicked
|
||||
)
|
||||
}
|
||||
|
||||
@@ -141,7 +147,8 @@ fun MessagesHistoryScreen(
|
||||
onPaginationConditionsMet: () -> Unit = {},
|
||||
onMessageInputChanged: (TextFieldValue) -> Unit = {},
|
||||
onAttachmentButtonClicked: () -> Unit = {},
|
||||
onActionButtonClicked: () -> Unit = {}
|
||||
onActionButtonClicked: () -> Unit = {},
|
||||
onEmojiButtonLongClicked: () -> Unit = {}
|
||||
) {
|
||||
val view = LocalView.current
|
||||
|
||||
@@ -172,15 +179,6 @@ fun MessagesHistoryScreen(
|
||||
|
||||
val hazeState = remember { HazeState() }
|
||||
|
||||
var animationsEnabled by remember {
|
||||
mutableStateOf(
|
||||
preferences.getBoolean(
|
||||
SettingsKeys.KEY_ENABLE_ANIMATIONS_IN_MESSAGES,
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val toolbarColorAlpha by animateFloatAsState(
|
||||
targetValue = if (!listState.canScrollForward) 1f else 0f,
|
||||
label = "toolbarColorAlpha",
|
||||
@@ -210,14 +208,42 @@ fun MessagesHistoryScreen(
|
||||
)
|
||||
.fillMaxWidth(),
|
||||
title = {
|
||||
Text(
|
||||
text =
|
||||
if (screenState.isLoading) stringResource(id = UiR.string.title_loading)
|
||||
else screenState.title,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.headlineSmall
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val avatar = screenState.avatar.getImage()
|
||||
if (avatar is Painter) {
|
||||
Image(
|
||||
painter = avatar,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.clip(CircleShape)
|
||||
)
|
||||
} else {
|
||||
AsyncImage(
|
||||
model = screenState.avatar.getImage(),
|
||||
contentDescription = "Profile Image",
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.clip(CircleShape),
|
||||
placeholder = painterResource(id = UiR.drawable.ic_account_circle_cut),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
Text(
|
||||
text =
|
||||
if (screenState.isLoading) stringResource(id = UiR.string.title_loading)
|
||||
else screenState.title,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.headlineSmall
|
||||
)
|
||||
}
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
@@ -280,31 +306,19 @@ fun MessagesHistoryScreen(
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
if (preferences.getBoolean(
|
||||
SettingsKeys.KEY_SHOW_DEBUG_CATEGORY,
|
||||
false
|
||||
)
|
||||
) {
|
||||
HorizontalDivider()
|
||||
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(text = if (animationsEnabled) "Disable animations" else "Enable animations")
|
||||
},
|
||||
onClick = {
|
||||
dropDownMenuExpanded = false
|
||||
animationsEnabled = !animationsEnabled
|
||||
onToggleAnimationsDropdownItemClicked(animationsEnabled)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
if (screenState.isLoading && screenState.messages.isNotEmpty()) {
|
||||
|
||||
val showHorizontalProgressBar by remember(screenState) {
|
||||
derivedStateOf { screenState.isLoading && screenState.messages.isNotEmpty() }
|
||||
}
|
||||
if (showHorizontalProgressBar) {
|
||||
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
AnimatedVisibility(!showHorizontalProgressBar) {
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
}
|
||||
) { padding ->
|
||||
@@ -320,7 +334,6 @@ fun MessagesHistoryScreen(
|
||||
listState = listState,
|
||||
immutableMessages = ImmutableList.copyOf(screenState.messages),
|
||||
isPaginating = screenState.isPaginating,
|
||||
enableAnimations = animationsEnabled,
|
||||
messageBarHeight = messageBarHeight,
|
||||
onRequestScrollToCmId = { cmId ->
|
||||
coroutineScope.launch {
|
||||
@@ -371,7 +384,7 @@ fun MessagesHistoryScreen(
|
||||
Column(verticalArrangement = Arrangement.Bottom) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (AppSettings.Debug.enableHaptic) {
|
||||
if (AppSettings.General.enableHaptic) {
|
||||
view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT)
|
||||
}
|
||||
scope.launch {
|
||||
@@ -389,6 +402,12 @@ fun MessagesHistoryScreen(
|
||||
}
|
||||
}
|
||||
},
|
||||
onLongClick = {
|
||||
if (AppSettings.General.enableHaptic) {
|
||||
view.performHapticFeedback(HapticFeedbackConstantsCompat.LONG_PRESS)
|
||||
}
|
||||
onEmojiButtonLongClicked()
|
||||
},
|
||||
modifier = Modifier.rotate(rotation.value)
|
||||
) {
|
||||
Icon(
|
||||
@@ -427,7 +446,7 @@ fun MessagesHistoryScreen(
|
||||
Column(verticalArrangement = Arrangement.Bottom) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (AppSettings.Debug.enableHaptic) {
|
||||
if (AppSettings.General.enableHaptic) {
|
||||
view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT)
|
||||
}
|
||||
scope.launch {
|
||||
@@ -463,7 +482,7 @@ fun MessagesHistoryScreen(
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (screenState.actionMode == ActionMode.Record) {
|
||||
if (AppSettings.Debug.enableHaptic) {
|
||||
if (AppSettings.General.enableHaptic) {
|
||||
view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT)
|
||||
}
|
||||
scope.launch {
|
||||
|
||||
+8
-2
@@ -13,12 +13,14 @@ import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.chrisbanes.haze.HazeState
|
||||
import dev.chrisbanes.haze.haze
|
||||
import dev.meloda.fast.datastore.AppSettings
|
||||
import dev.meloda.fast.messageshistory.model.UiItem
|
||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||
import dev.meloda.fast.ui.util.ImmutableList
|
||||
@@ -30,11 +32,15 @@ fun MessagesList(
|
||||
listState: LazyListState,
|
||||
immutableMessages: ImmutableList<UiItem>,
|
||||
isPaginating: Boolean,
|
||||
enableAnimations: Boolean,
|
||||
messageBarHeight: Dp,
|
||||
onRequestScrollToCmId: (cmId: Int) -> Unit = {}
|
||||
) {
|
||||
val messages = immutableMessages.toList()
|
||||
val enableAnimations = remember {
|
||||
AppSettings.Experimental.moreAnimations
|
||||
}
|
||||
val messages = remember(immutableMessages) {
|
||||
immutableMessages.toList()
|
||||
}
|
||||
val currentTheme = LocalThemeConfig.current
|
||||
|
||||
LazyColumn(
|
||||
|
||||
@@ -2,7 +2,7 @@ package dev.meloda.fast.photoviewer.di
|
||||
|
||||
import dev.meloda.fast.photoviewer.PhotoViewViewModel
|
||||
import dev.meloda.fast.photoviewer.PhotoViewViewModelImpl
|
||||
import org.koin.androidx.viewmodel.dsl.viewModelOf
|
||||
import org.koin.core.module.dsl.viewModelOf
|
||||
import org.koin.dsl.bind
|
||||
import org.koin.dsl.module
|
||||
|
||||
|
||||
+1
-1
@@ -123,7 +123,7 @@ fun TopBar(
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
// IconButton(
|
||||
// IconButton.kt(
|
||||
// onClick = { dropdownMenuShown = true }
|
||||
// ) {
|
||||
// Icon(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package dev.meloda.fast.profile.di
|
||||
|
||||
import dev.meloda.fast.profile.ProfileViewModelImpl
|
||||
import org.koin.androidx.viewmodel.dsl.viewModelOf
|
||||
import org.koin.core.module.dsl.viewModelOf
|
||||
import org.koin.dsl.module
|
||||
|
||||
val profileModule = module {
|
||||
|
||||
@@ -154,13 +154,6 @@ class SettingsViewModelImpl(
|
||||
userSettings.onUseContactNamesChanged(isUsing)
|
||||
}
|
||||
|
||||
SettingsKeys.KEY_ENABLE_PULL_TO_REFRESH -> {
|
||||
val enable =
|
||||
newValue as? Boolean ?: SettingsKeys.DEFAULT_VALUE_ENABLE_PULL_TO_REFRESH
|
||||
userSettings.onEnablePullToRefreshChanged(enable)
|
||||
}
|
||||
|
||||
|
||||
SettingsKeys.KEY_APPEARANCE_MULTILINE -> {
|
||||
val isUsing = newValue as? Boolean ?: SettingsKeys.DEFAULT_VALUE_MULTILINE
|
||||
userSettings.onEnableMultilineChanged(isUsing)
|
||||
@@ -206,9 +199,9 @@ class SettingsViewModelImpl(
|
||||
userSettings.onShowAlertAfterCrashChanged(show)
|
||||
}
|
||||
|
||||
SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND -> {
|
||||
SettingsKeys.KEY_LONG_POLL_IN_BACKGROUND -> {
|
||||
val inBackground = newValue as? Boolean
|
||||
?: SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND
|
||||
?: SettingsKeys.DEFAULT_LONG_POLL_IN_BACKGROUND
|
||||
userSettings.onLongPollInBackgroundChanged(inBackground)
|
||||
|
||||
longPollController.setStateToApply(
|
||||
@@ -221,9 +214,9 @@ class SettingsViewModelImpl(
|
||||
)
|
||||
}
|
||||
|
||||
SettingsKeys.KEY_APPEARANCE_USE_BLUR -> {
|
||||
SettingsKeys.KEY_USE_BLUR -> {
|
||||
val isUsing =
|
||||
newValue as? Boolean ?: SettingsKeys.DEFAULT_VALUE_KEY_APPEARANCE_USE_BLUR
|
||||
newValue as? Boolean ?: SettingsKeys.DEFAULT_USE_BLUR
|
||||
userSettings.onUseBlurChanged(isUsing)
|
||||
}
|
||||
|
||||
@@ -232,14 +225,14 @@ class SettingsViewModelImpl(
|
||||
userSettings.onShowEmojiButtonChanged(show)
|
||||
}
|
||||
|
||||
SettingsKeys.KEY_APPEARANCE_SHOW_TIME_IN_ACTION_MESSAGES -> {
|
||||
SettingsKeys.KEY_SHOW_TIME_IN_ACTION_MESSAGES -> {
|
||||
val show = newValue as? Boolean
|
||||
?: SettingsKeys.DEFAULT_VALUE_APPEARANCE_SHOW_TIME_IN_ACTION_MESSAGES
|
||||
?: SettingsKeys.DEFAULT_SHOW_TIME_IN_ACTION_MESSAGES
|
||||
userSettings.onShowTimeInActionMessagesChanged(show)
|
||||
}
|
||||
|
||||
SettingsKeys.KEY_DEBUG_USE_SYSTEM_FONT -> {
|
||||
val use = newValue as? Boolean ?: SettingsKeys.DEFAULT_DEBUG_USE_SYSTEM_FONT
|
||||
SettingsKeys.KEY_USE_SYSTEM_FONT -> {
|
||||
val use = newValue as? Boolean ?: SettingsKeys.DEFAULT_USE_SYSTEM_FONT
|
||||
userSettings.onUseSystemFontChanged(use)
|
||||
}
|
||||
|
||||
@@ -283,10 +276,16 @@ class SettingsViewModelImpl(
|
||||
text = UiText.Resource(UiR.string.settings_general_contact_names_summary),
|
||||
defaultValue = SettingsKeys.DEFAULT_VALUE_USE_CONTACT_NAMES
|
||||
)
|
||||
val generalEnablePullToRefresh = SettingsItem.Switch(
|
||||
key = SettingsKeys.KEY_ENABLE_PULL_TO_REFRESH,
|
||||
defaultValue = SettingsKeys.DEFAULT_VALUE_ENABLE_PULL_TO_REFRESH,
|
||||
title = UiText.Resource(UiR.string.settings_general_enable_pull_to_refresh_title)
|
||||
val generalShowEmojiButton = SettingsItem.Switch(
|
||||
key = SettingsKeys.KEY_SHOW_EMOJI_BUTTON,
|
||||
title = UiText.Resource(UiR.string.settings_general_show_emoji_button_title),
|
||||
text = UiText.Resource(UiR.string.settings_general_show_emoji_button_summary),
|
||||
defaultValue = SettingsKeys.DEFAULT_VALUE_KEY_SHOW_EMOJI_BUTTON
|
||||
)
|
||||
val generalEnableHaptic = SettingsItem.Switch(
|
||||
key = SettingsKeys.KEY_ENABLE_HAPTIC,
|
||||
defaultValue = SettingsKeys.DEFAULT_ENABLE_HAPTIC,
|
||||
title = UiText.Resource(UiR.string.settings_general_enable_haptic_title)
|
||||
)
|
||||
|
||||
val appearanceTitle = SettingsItem.Title(
|
||||
@@ -340,7 +339,11 @@ class SettingsViewModelImpl(
|
||||
text = UiText.Resource(UiR.string.settings_dynamic_colors_description),
|
||||
defaultValue = SettingsKeys.DEFAULT_VALUE_USE_DYNAMIC_COLORS
|
||||
)
|
||||
|
||||
val appearanceUseSystemFont = SettingsItem.Switch(
|
||||
key = SettingsKeys.KEY_USE_SYSTEM_FONT,
|
||||
defaultValue = SettingsKeys.DEFAULT_USE_SYSTEM_FONT,
|
||||
title = UiText.Resource(UiR.string.settings_appearance_use_system_font_title)
|
||||
)
|
||||
val appearanceLanguage = SettingsItem.TitleText(
|
||||
key = SettingsKeys.KEY_APPEARANCE_LANGUAGE,
|
||||
title = UiText.Resource(UiR.string.settings_application_language),
|
||||
@@ -374,6 +377,35 @@ class SettingsViewModelImpl(
|
||||
text = UiText.Resource(UiR.string.settings_activity_send_online_summary)
|
||||
)
|
||||
|
||||
val experimentalTitle = SettingsItem.Title(
|
||||
key = "experimental",
|
||||
title = UiText.Resource(UiR.string.settings_experimental_title)
|
||||
)
|
||||
val experimentalLongPollBackground = SettingsItem.Switch(
|
||||
key = SettingsKeys.KEY_LONG_POLL_IN_BACKGROUND,
|
||||
defaultValue = SettingsKeys.DEFAULT_LONG_POLL_IN_BACKGROUND,
|
||||
title = UiText.Resource(UiR.string.settings_features_long_poll_in_background_title),
|
||||
text = UiText.Resource(UiR.string.settings_features_long_poll_in_background_summary)
|
||||
)
|
||||
val experimentalShowTimeInActionMessages = SettingsItem.Switch(
|
||||
key = SettingsKeys.KEY_SHOW_TIME_IN_ACTION_MESSAGES,
|
||||
defaultValue = SettingsKeys.DEFAULT_SHOW_TIME_IN_ACTION_MESSAGES,
|
||||
title = UiText.Resource(UiR.string.settings_features_show_time_in_action_messages_title)
|
||||
)
|
||||
val experimentalUseBlur = SettingsItem.Switch(
|
||||
key = SettingsKeys.KEY_USE_BLUR,
|
||||
defaultValue = SettingsKeys.DEFAULT_USE_BLUR,
|
||||
title = UiText.Resource(UiR.string.settings_experimental_use_blur_title),
|
||||
text = UiText.Resource(UiR.string.settings_experimental_use_blur_summary),
|
||||
isVisible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||
)
|
||||
val enableAnimations = SettingsItem.Switch(
|
||||
key = SettingsKeys.KEY_MORE_ANIMATIONS,
|
||||
defaultValue = SettingsKeys.DEFAULT_MORE_ANIMATIONS,
|
||||
title = UiText.Resource(UiR.string.settings_experimental_more_animations_title),
|
||||
text = UiText.Resource(UiR.string.settings_experimental_more_animations_summary)
|
||||
)
|
||||
|
||||
val debugTitle = SettingsItem.Title(
|
||||
key = "debug",
|
||||
title = UiText.Resource(UiR.string.settings_debug_title)
|
||||
@@ -389,34 +421,6 @@ class SettingsViewModelImpl(
|
||||
title = UiText.Simple("Show alert after crash"),
|
||||
text = UiText.Simple("Shows alert dialog with stacktrace after app crashed\n(it will be not shown if you perform crash manually)")
|
||||
)
|
||||
val debugLongPollBackground = SettingsItem.Switch(
|
||||
key = SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND,
|
||||
defaultValue = SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND,
|
||||
title = UiText.Resource(UiR.string.settings_features_long_poll_in_background_title),
|
||||
text = UiText.Resource(UiR.string.settings_features_long_poll_in_background_summary)
|
||||
)
|
||||
val debugUseBlur = SettingsItem.Switch(
|
||||
key = SettingsKeys.KEY_APPEARANCE_USE_BLUR,
|
||||
defaultValue = SettingsKeys.DEFAULT_VALUE_KEY_APPEARANCE_USE_BLUR,
|
||||
title = UiText.Simple("[WIP] Use blur"),
|
||||
text = UiText.Simple("Adds blur wherever possible\nWorks on android 12 and newer"),
|
||||
)
|
||||
val debugShowEmojiButton = SettingsItem.Switch(
|
||||
key = SettingsKeys.KEY_SHOW_EMOJI_BUTTON,
|
||||
title = UiText.Simple("Show emoji button"),
|
||||
text = UiText.Simple("Show emoji button in chat panel"),
|
||||
defaultValue = SettingsKeys.DEFAULT_VALUE_KEY_SHOW_EMOJI_BUTTON
|
||||
)
|
||||
val debugShowTimeInActionMessages = SettingsItem.Switch(
|
||||
key = SettingsKeys.KEY_APPEARANCE_SHOW_TIME_IN_ACTION_MESSAGES,
|
||||
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 = SettingsKeys.DEFAULT_DEBUG_ENABLE_HAPTIC,
|
||||
title = UiText.Simple("Enable haptic")
|
||||
)
|
||||
|
||||
val logLevelValues = listOf(
|
||||
LogLevel.NONE to UiText.Simple("None"),
|
||||
@@ -440,12 +444,6 @@ class SettingsViewModelImpl(
|
||||
}
|
||||
}
|
||||
|
||||
val debugUseSystemFont = SettingsItem.Switch(
|
||||
key = SettingsKeys.KEY_DEBUG_USE_SYSTEM_FONT,
|
||||
defaultValue = SettingsKeys.DEFAULT_DEBUG_USE_SYSTEM_FONT,
|
||||
title = UiText.Simple("Use system font")
|
||||
)
|
||||
|
||||
val debugHideDebugList = SettingsItem.TitleText(
|
||||
key = SettingsKeys.KEY_DEBUG_HIDE_DEBUG_LIST,
|
||||
title = UiText.Simple("Hide debug list")
|
||||
@@ -458,7 +456,8 @@ class SettingsViewModelImpl(
|
||||
val generalList = listOf(
|
||||
generalTitle,
|
||||
generalUseContactNames,
|
||||
generalEnablePullToRefresh
|
||||
generalShowEmojiButton,
|
||||
generalEnableHaptic
|
||||
)
|
||||
val appearanceList = listOf(
|
||||
appearanceTitle,
|
||||
@@ -466,6 +465,7 @@ class SettingsViewModelImpl(
|
||||
appearanceDarkTheme,
|
||||
appearanceUseAmoledDarkTheme,
|
||||
appearanceUseDynamicColors,
|
||||
appearanceUseSystemFont,
|
||||
appearanceLanguage
|
||||
)
|
||||
val featuresList = listOf(
|
||||
@@ -476,18 +476,19 @@ class SettingsViewModelImpl(
|
||||
activityTitle,
|
||||
visibilitySendOnlineStatus,
|
||||
)
|
||||
val experimentalList = listOf(
|
||||
experimentalTitle,
|
||||
experimentalLongPollBackground,
|
||||
experimentalShowTimeInActionMessages,
|
||||
experimentalUseBlur,
|
||||
enableAnimations
|
||||
)
|
||||
val debugList = mutableListOf<SettingsItem<*>>()
|
||||
listOf(
|
||||
debugTitle,
|
||||
debugPerformCrash,
|
||||
debugShowCrashAlert,
|
||||
debugLongPollBackground,
|
||||
debugUseBlur,
|
||||
debugShowEmojiButton,
|
||||
debugShowTimeInActionMessages,
|
||||
debugEnableHaptic,
|
||||
debugNetworkLogLevel,
|
||||
debugUseSystemFont
|
||||
).forEach(debugList::add)
|
||||
|
||||
debugList += debugHideDebugList
|
||||
@@ -499,6 +500,7 @@ class SettingsViewModelImpl(
|
||||
appearanceList,
|
||||
featuresList,
|
||||
visibilityList,
|
||||
experimentalList,
|
||||
debugList,
|
||||
).forEach(settingsList::addAll)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ package dev.meloda.fast.settings.di
|
||||
|
||||
import dev.meloda.fast.settings.SettingsViewModel
|
||||
import dev.meloda.fast.settings.SettingsViewModelImpl
|
||||
import org.koin.androidx.viewmodel.dsl.viewModelOf
|
||||
import org.koin.core.module.dsl.viewModelOf
|
||||
import org.koin.dsl.bind
|
||||
import org.koin.dsl.module
|
||||
|
||||
|
||||
+1
-1
@@ -113,7 +113,7 @@ fun SettingsScreen(
|
||||
|
||||
LaunchedEffect(hapticType) {
|
||||
if (hapticType != null) {
|
||||
if (AppSettings.Debug.enableHaptic) {
|
||||
if (AppSettings.General.enableHaptic) {
|
||||
view.performHapticFeedback(hapticType.getHaptic())
|
||||
}
|
||||
onHapticPerformed()
|
||||
|
||||
+14
-14
@@ -2,25 +2,25 @@
|
||||
minSdk = "23"
|
||||
targetSdk = "35"
|
||||
compileSdk = "35"
|
||||
versionCode = "7"
|
||||
versionName = "0.1.4"
|
||||
versionCode = "9"
|
||||
versionName = "0.1.6"
|
||||
|
||||
agp = "8.7.3"
|
||||
agp = "8.9.0"
|
||||
converterMoshi = "2.11.0"
|
||||
eithernet = "2.0.0"
|
||||
haze = "1.1.1"
|
||||
kotlin = "2.1.0"
|
||||
ksp = "2.1.0-1.0.29"
|
||||
haze = "1.5.1"
|
||||
kotlin = "2.1.10"
|
||||
ksp = "2.1.10-1.0.31"
|
||||
|
||||
compose-bom = "2024.12.01"
|
||||
koin = "4.0.0"
|
||||
compose-bom = "2025.03.00"
|
||||
koin = "4.0.2"
|
||||
|
||||
accompanist = "0.37.0"
|
||||
accompanist = "0.37.2"
|
||||
coil = "2.7.0"
|
||||
coroutines = "1.9.0"
|
||||
coroutines = "1.10.1"
|
||||
junit = "4.13.2"
|
||||
chucker = "4.1.0"
|
||||
guava = "33.3.1-jre"
|
||||
guava = "33.4.5-jre"
|
||||
lifecycle = "2.8.7"
|
||||
core-ktx = "1.15.0"
|
||||
material = "1.12.0"
|
||||
@@ -33,10 +33,10 @@ nanokt = "1.2.0"
|
||||
junitVersion = "1.2.1"
|
||||
espressoCore = "3.6.1"
|
||||
appcompat = "1.7.0"
|
||||
androidx-navigation = "2.8.5"
|
||||
serialization = "1.7.3"
|
||||
androidx-navigation = "2.8.9"
|
||||
serialization = "1.8.0"
|
||||
rebugger = "1.0.0-rc03"
|
||||
moduleGraph = "2.7.1"
|
||||
moduleGraph = "2.8.0"
|
||||
|
||||
[libraries]
|
||||
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" }
|
||||
|
||||
Vendored
BIN
Binary file not shown.
+3
-2
@@ -1,6 +1,7 @@
|
||||
#Mon Oct 28 18:41:43 MSK 2024
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
@@ -55,7 +57,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
@@ -80,13 +82,12 @@ do
|
||||
esac
|
||||
done
|
||||
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
APP_NAME="Gradle"
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
||||
' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
@@ -133,22 +134,29 @@ location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
@@ -193,11 +201,15 @@ if "$cygwin" || "$msys" ; then
|
||||
done
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
@@ -205,6 +217,12 @@ set -- \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
|
||||
Vendored
+21
-16
@@ -13,8 +13,10 @@
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@@ -25,7 +27,8 @@
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@@ -40,13 +43,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
@@ -56,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
@@ -75,13 +78,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
Reference in New Issue
Block a user