forked from melod1n/fast-messenger
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0682d6c42c | |||
| 6a69f28256 | |||
| 85cda2065e |
@@ -7,10 +7,10 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "dev.meloda.fastvk"
|
namespace = "dev.meloda.fast"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "dev.meloda.fastvk"
|
applicationId = "dev.meloda.fast"
|
||||||
|
|
||||||
versionCode = libs.versions.versionCode.get().toInt()
|
versionCode = libs.versions.versionCode.get().toInt()
|
||||||
versionName = libs.versions.versionName.get()
|
versionName = libs.versions.versionName.get()
|
||||||
@@ -77,7 +77,6 @@ dependencies {
|
|||||||
implementation(projects.feature.friends)
|
implementation(projects.feature.friends)
|
||||||
implementation(projects.feature.profile)
|
implementation(projects.feature.profile)
|
||||||
implementation(projects.feature.photoviewer)
|
implementation(projects.feature.photoviewer)
|
||||||
implementation(projects.feature.createchat)
|
|
||||||
|
|
||||||
implementation(projects.core.common)
|
implementation(projects.core.common)
|
||||||
implementation(projects.core.ui)
|
implementation(projects.core.ui)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
tools:targetApi="tiramisu">
|
tools:targetApi="tiramisu">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="dev.meloda.fast.presentation.MainActivity"
|
android:name=".presentation.MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
|
||||||
@@ -38,13 +38,13 @@
|
|||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name="dev.meloda.fast.service.longpolling.LongPollingService"
|
android:name=".service.longpolling.LongPollingService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:foregroundServiceType="dataSync" />
|
android:foregroundServiceType="dataSync" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name="dev.meloda.fast.service.OnlineService"
|
android:name=".service.OnlineService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:foregroundServiceType="dataSync" />
|
android:foregroundServiceType="dataSync" />
|
||||||
|
|||||||
@@ -86,8 +86,6 @@ class MainViewModelImpl(
|
|||||||
BaseError.SessionExpired -> {
|
BaseError.SessionExpired -> {
|
||||||
isNeedToReplaceWithAuth.update { true }
|
isNeedToReplaceWithAuth.update { true }
|
||||||
}
|
}
|
||||||
|
|
||||||
is BaseError.SimpleError -> Unit // TODO: 21-Mar-25, Danil Nikolaev: show error in ui
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import dev.meloda.fast.common.provider.Provider
|
|||||||
import dev.meloda.fast.common.provider.ResourceProvider
|
import dev.meloda.fast.common.provider.ResourceProvider
|
||||||
import dev.meloda.fast.common.provider.ResourceProviderImpl
|
import dev.meloda.fast.common.provider.ResourceProviderImpl
|
||||||
import dev.meloda.fast.conversations.di.conversationsModule
|
import dev.meloda.fast.conversations.di.conversationsModule
|
||||||
import dev.meloda.fast.conversations.di.createChatModule
|
|
||||||
import dev.meloda.fast.domain.di.domainModule
|
import dev.meloda.fast.domain.di.domainModule
|
||||||
import dev.meloda.fast.friends.di.friendsModule
|
import dev.meloda.fast.friends.di.friendsModule
|
||||||
import dev.meloda.fast.languagepicker.di.languagePickerModule
|
import dev.meloda.fast.languagepicker.di.languagePickerModule
|
||||||
@@ -27,8 +26,8 @@ import dev.meloda.fast.provider.ApiLanguageProvider
|
|||||||
import dev.meloda.fast.service.longpolling.di.longPollModule
|
import dev.meloda.fast.service.longpolling.di.longPollModule
|
||||||
import dev.meloda.fast.settings.di.settingsModule
|
import dev.meloda.fast.settings.di.settingsModule
|
||||||
import org.koin.android.ext.koin.androidContext
|
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.singleOf
|
||||||
import org.koin.core.module.dsl.viewModelOf
|
|
||||||
import org.koin.core.qualifier.qualifier
|
import org.koin.core.qualifier.qualifier
|
||||||
import org.koin.dsl.bind
|
import org.koin.dsl.bind
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
@@ -47,8 +46,7 @@ val applicationModule = module {
|
|||||||
longPollModule,
|
longPollModule,
|
||||||
friendsModule,
|
friendsModule,
|
||||||
profileModule,
|
profileModule,
|
||||||
chatMaterialsModule,
|
chatMaterialsModule
|
||||||
createChatModule
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: 14/05/2024, Danil Nikolaev: research on memory leaks and potentials errors
|
// TODO: 14/05/2024, Danil Nikolaev: research on memory leaks and potentials errors
|
||||||
@@ -63,7 +61,7 @@ val applicationModule = module {
|
|||||||
qualifier = qualifier("main")
|
qualifier = qualifier("main")
|
||||||
}
|
}
|
||||||
|
|
||||||
single<ImageLoader> {
|
single {
|
||||||
ImageLoader.Builder(get())
|
ImageLoader.Builder(get())
|
||||||
.crossfade(true)
|
.crossfade(true)
|
||||||
.build()
|
.build()
|
||||||
|
|||||||
@@ -24,8 +24,6 @@ fun NavGraphBuilder.mainScreen(
|
|||||||
onSettingsButtonClicked: () -> Unit,
|
onSettingsButtonClicked: () -> Unit,
|
||||||
onConversationClicked: (conversationId: Int) -> Unit,
|
onConversationClicked: (conversationId: Int) -> Unit,
|
||||||
onPhotoClicked: (url: String) -> Unit,
|
onPhotoClicked: (url: String) -> Unit,
|
||||||
onMessageClicked: (userId: Int) -> Unit,
|
|
||||||
onCreateChatClicked: () -> Unit,
|
|
||||||
viewModel: MainViewModel
|
viewModel: MainViewModel
|
||||||
) {
|
) {
|
||||||
val navigationItems = ImmutableList.of(
|
val navigationItems = ImmutableList.of(
|
||||||
@@ -56,8 +54,6 @@ fun NavGraphBuilder.mainScreen(
|
|||||||
onSettingsButtonClicked = onSettingsButtonClicked,
|
onSettingsButtonClicked = onSettingsButtonClicked,
|
||||||
onConversationItemClicked = onConversationClicked,
|
onConversationItemClicked = onConversationClicked,
|
||||||
onPhotoClicked = onPhotoClicked,
|
onPhotoClicked = onPhotoClicked,
|
||||||
onMessageClicked = onMessageClicked,
|
|
||||||
onCreateChatClicked = onCreateChatClicked,
|
|
||||||
viewModel = viewModel
|
viewModel = viewModel
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import androidx.navigation.compose.navigation
|
|||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import coil.compose.SubcomposeAsyncImage
|
import coil.compose.SubcomposeAsyncImage
|
||||||
import dev.chrisbanes.haze.HazeState
|
import dev.chrisbanes.haze.HazeState
|
||||||
import dev.chrisbanes.haze.hazeEffect
|
import dev.chrisbanes.haze.hazeChild
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
import dev.chrisbanes.haze.materials.HazeMaterials
|
import dev.chrisbanes.haze.materials.HazeMaterials
|
||||||
import dev.meloda.fast.MainViewModel
|
import dev.meloda.fast.MainViewModel
|
||||||
@@ -47,6 +47,8 @@ import dev.meloda.fast.ui.theme.LocalBottomPadding
|
|||||||
import dev.meloda.fast.ui.theme.LocalHazeState
|
import dev.meloda.fast.ui.theme.LocalHazeState
|
||||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||||
import dev.meloda.fast.ui.util.ImmutableList
|
import dev.meloda.fast.ui.util.ImmutableList
|
||||||
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
|
||||||
@OptIn(ExperimentalHazeMaterialsApi::class)
|
@OptIn(ExperimentalHazeMaterialsApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -56,8 +58,6 @@ fun MainScreen(
|
|||||||
onSettingsButtonClicked: () -> Unit = {},
|
onSettingsButtonClicked: () -> Unit = {},
|
||||||
onConversationItemClicked: (conversationId: Int) -> Unit = {},
|
onConversationItemClicked: (conversationId: Int) -> Unit = {},
|
||||||
onPhotoClicked: (url: String) -> Unit = {},
|
onPhotoClicked: (url: String) -> Unit = {},
|
||||||
onMessageClicked: (userId: Int) -> Unit = {},
|
|
||||||
onCreateChatClicked: () -> Unit = {},
|
|
||||||
viewModel: MainViewModel
|
viewModel: MainViewModel
|
||||||
) {
|
) {
|
||||||
val currentTheme = LocalThemeConfig.current
|
val currentTheme = LocalThemeConfig.current
|
||||||
@@ -70,13 +70,21 @@ fun MainScreen(
|
|||||||
mutableIntStateOf(1)
|
mutableIntStateOf(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val sharedFlow = remember {
|
||||||
|
MutableSharedFlow<Int>(
|
||||||
|
replay = 0,
|
||||||
|
extraBufferCapacity = 1,
|
||||||
|
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
NavigationBar(
|
NavigationBar(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.then(
|
.then(
|
||||||
if (currentTheme.enableBlur) {
|
if (currentTheme.enableBlur) {
|
||||||
Modifier.hazeEffect(
|
Modifier.hazeChild(
|
||||||
state = hazeState,
|
state = hazeState,
|
||||||
style = HazeMaterials.thick()
|
style = HazeMaterials.thick()
|
||||||
)
|
)
|
||||||
@@ -100,6 +108,8 @@ fun MainScreen(
|
|||||||
inclusive = true
|
inclusive = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
sharedFlow.tryEmit(index)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
icon = {
|
icon = {
|
||||||
@@ -166,14 +176,13 @@ fun MainScreen(
|
|||||||
friendsScreen(
|
friendsScreen(
|
||||||
onError = onError,
|
onError = onError,
|
||||||
navController = navController,
|
navController = navController,
|
||||||
onPhotoClicked = onPhotoClicked,
|
onPhotoClicked = onPhotoClicked
|
||||||
onMessageClicked = onMessageClicked
|
|
||||||
)
|
)
|
||||||
conversationsScreen(
|
conversationsScreen(
|
||||||
onError = onError,
|
onError = onError,
|
||||||
onConversationItemClicked = onConversationItemClicked,
|
onConversationItemClicked = onConversationItemClicked,
|
||||||
onPhotoClicked = onPhotoClicked,
|
onPhotoClicked = onPhotoClicked,
|
||||||
onCreateChatClicked = onCreateChatClicked,
|
scrollToTopFlow = sharedFlow,
|
||||||
navController = navController,
|
navController = navController,
|
||||||
)
|
)
|
||||||
profileScreen(
|
profileScreen(
|
||||||
|
|||||||
@@ -25,8 +25,6 @@ import dev.meloda.fast.auth.authNavGraph
|
|||||||
import dev.meloda.fast.auth.navigateToAuth
|
import dev.meloda.fast.auth.navigateToAuth
|
||||||
import dev.meloda.fast.chatmaterials.navigation.chatMaterialsScreen
|
import dev.meloda.fast.chatmaterials.navigation.chatMaterialsScreen
|
||||||
import dev.meloda.fast.chatmaterials.navigation.navigateToChatMaterials
|
import dev.meloda.fast.chatmaterials.navigation.navigateToChatMaterials
|
||||||
import dev.meloda.fast.conversations.navigation.createChatScreen
|
|
||||||
import dev.meloda.fast.conversations.navigation.navigateToCreateChat
|
|
||||||
import dev.meloda.fast.languagepicker.navigation.languagePickerScreen
|
import dev.meloda.fast.languagepicker.navigation.languagePickerScreen
|
||||||
import dev.meloda.fast.languagepicker.navigation.navigateToLanguagePicker
|
import dev.meloda.fast.languagepicker.navigation.navigateToLanguagePicker
|
||||||
import dev.meloda.fast.messageshistory.navigation.messagesHistoryScreen
|
import dev.meloda.fast.messageshistory.navigation.messagesHistoryScreen
|
||||||
@@ -126,8 +124,6 @@ fun RootScreen(
|
|||||||
onSettingsButtonClicked = navController::navigateToSettings,
|
onSettingsButtonClicked = navController::navigateToSettings,
|
||||||
onConversationClicked = navController::navigateToMessagesHistory,
|
onConversationClicked = navController::navigateToMessagesHistory,
|
||||||
onPhotoClicked = { url -> navController.navigateToPhotoView(listOf(url)) },
|
onPhotoClicked = { url -> navController.navigateToPhotoView(listOf(url)) },
|
||||||
onMessageClicked = navController::navigateToMessagesHistory,
|
|
||||||
onCreateChatClicked = navController::navigateToCreateChat,
|
|
||||||
viewModel = viewModel
|
viewModel = viewModel
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -140,13 +136,6 @@ fun RootScreen(
|
|||||||
onBack = navController::navigateUp,
|
onBack = navController::navigateUp,
|
||||||
onPhotoClicked = { url -> navController.navigateToPhotoView(listOf(url)) }
|
onPhotoClicked = { url -> navController.navigateToPhotoView(listOf(url)) }
|
||||||
)
|
)
|
||||||
createChatScreen(
|
|
||||||
onChatCreated = { conversationId ->
|
|
||||||
navController.popBackStack()
|
|
||||||
navController.navigateToMessagesHistory(conversationId)
|
|
||||||
},
|
|
||||||
navController = navController
|
|
||||||
)
|
|
||||||
|
|
||||||
settingsScreen(
|
settingsScreen(
|
||||||
onBack = navController::navigateUp,
|
onBack = navController::navigateUp,
|
||||||
|
|||||||
@@ -95,6 +95,11 @@ class OnlineService : Service() {
|
|||||||
}.also { coroutine -> coroutine.invokeOnCompletion { onlineJob = null } }
|
}.also { coroutine -> coroutine.invokeOnCompletion { onlineJob = null } }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onLowMemory() {
|
||||||
|
Log.d(STATE_TAG, "onLowMemory")
|
||||||
|
super.onLowMemory()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
Log.d(STATE_TAG, "onDestroy")
|
Log.d(STATE_TAG, "onDestroy")
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package dev.meloda.fast.common.extensions
|
package dev.meloda.fast.common.extensions
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ object AndroidUtils {
|
|||||||
action = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
action = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||||
Settings.ACTION_SECURITY_SETTINGS
|
Settings.ACTION_SECURITY_SETTINGS
|
||||||
} else {
|
} else {
|
||||||
data = Uri.parse("package:dev.meloda.fastvk")
|
data = Uri.parse("package:dev.meloda.fast")
|
||||||
Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES
|
Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -20,20 +20,18 @@ sealed class State<out T> {
|
|||||||
|
|
||||||
data object ConnectionError : Error()
|
data object ConnectionError : Error()
|
||||||
|
|
||||||
data object UnknownError : Error()
|
data object Unknown : Error()
|
||||||
|
|
||||||
data object InternalError : Error()
|
data object InternalError : Error()
|
||||||
|
|
||||||
data class OAuthError(val error: OAuthErrorDomain) : Error()
|
data class OAuthError(val error: OAuthErrorDomain) : Error()
|
||||||
|
|
||||||
data class TestError(val message: String) : Error()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isLoading(): Boolean = this is Loading
|
fun isLoading(): Boolean = this is Loading
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
val UNKNOWN_ERROR = Error.UnknownError
|
val UNKNOWN_ERROR = Error.Unknown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,12 +73,11 @@ fun <T : Any> ApiResult<T, RestApiErrorDomain>.mapToState() = when (this) {
|
|||||||
is ApiResult.Failure.ApiFailure -> this.error.toStateApiError()
|
is ApiResult.Failure.ApiFailure -> this.error.toStateApiError()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T : Any, N> ApiResult<T, RestApiErrorDomain>.mapToState(successMapper: (T) -> N) =
|
fun <T : Any, N> ApiResult<T, RestApiErrorDomain>.mapToState(successMapper: (T) -> N) = when (this) {
|
||||||
when (this) {
|
|
||||||
is ApiResult.Success -> State.Success(successMapper(this.value))
|
is ApiResult.Success -> State.Success(successMapper(this.value))
|
||||||
|
|
||||||
is ApiResult.Failure.NetworkFailure -> State.Error.ConnectionError
|
is ApiResult.Failure.NetworkFailure -> State.Error.ConnectionError
|
||||||
is ApiResult.Failure.UnknownFailure -> State.UNKNOWN_ERROR
|
is ApiResult.Failure.UnknownFailure -> State.UNKNOWN_ERROR
|
||||||
is ApiResult.Failure.HttpFailure -> this.error.toStateApiError()
|
is ApiResult.Failure.HttpFailure -> this.error.toStateApiError()
|
||||||
is ApiResult.Failure.ApiFailure -> this.error.toStateApiError()
|
is ApiResult.Failure.ApiFailure -> this.error.toStateApiError()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
package dev.meloda.fast.data.api.messages
|
package dev.meloda.fast.data.api.messages
|
||||||
|
|
||||||
import com.slack.eithernet.ApiResult
|
|
||||||
import dev.meloda.fast.model.api.domain.VkAttachment
|
import dev.meloda.fast.model.api.domain.VkAttachment
|
||||||
import dev.meloda.fast.model.api.domain.VkAttachmentHistoryMessage
|
import dev.meloda.fast.model.api.domain.VkAttachmentHistoryMessage
|
||||||
import dev.meloda.fast.model.api.domain.VkMessage
|
import dev.meloda.fast.model.api.domain.VkMessage
|
||||||
import dev.meloda.fast.network.RestApiErrorDomain
|
import dev.meloda.fast.network.RestApiErrorDomain
|
||||||
|
import com.slack.eithernet.ApiResult
|
||||||
|
|
||||||
interface MessagesRepository {
|
interface MessagesRepository {
|
||||||
|
|
||||||
@@ -41,11 +41,6 @@ interface MessagesRepository {
|
|||||||
conversationMessageId: Int
|
conversationMessageId: Int
|
||||||
): ApiResult<List<VkAttachmentHistoryMessage>, RestApiErrorDomain>
|
): ApiResult<List<VkAttachmentHistoryMessage>, RestApiErrorDomain>
|
||||||
|
|
||||||
suspend fun createChat(
|
|
||||||
userIds: List<Int>?,
|
|
||||||
title: String?
|
|
||||||
): ApiResult<Int, RestApiErrorDomain>
|
|
||||||
|
|
||||||
suspend fun storeMessages(messages: List<VkMessage>)
|
suspend fun storeMessages(messages: List<VkMessage>)
|
||||||
|
|
||||||
// suspend fun markAsImportant(
|
// suspend fun markAsImportant(
|
||||||
|
|||||||
+1
-19
@@ -1,6 +1,5 @@
|
|||||||
package dev.meloda.fast.data.api.messages
|
package dev.meloda.fast.data.api.messages
|
||||||
|
|
||||||
import com.slack.eithernet.ApiResult
|
|
||||||
import dev.meloda.fast.common.VkConstants
|
import dev.meloda.fast.common.VkConstants
|
||||||
import dev.meloda.fast.data.VkGroupsMap
|
import dev.meloda.fast.data.VkGroupsMap
|
||||||
import dev.meloda.fast.data.VkMemoryCache
|
import dev.meloda.fast.data.VkMemoryCache
|
||||||
@@ -15,7 +14,6 @@ import dev.meloda.fast.model.api.domain.VkAttachment
|
|||||||
import dev.meloda.fast.model.api.domain.VkAttachmentHistoryMessage
|
import dev.meloda.fast.model.api.domain.VkAttachmentHistoryMessage
|
||||||
import dev.meloda.fast.model.api.domain.VkMessage
|
import dev.meloda.fast.model.api.domain.VkMessage
|
||||||
import dev.meloda.fast.model.api.domain.asEntity
|
import dev.meloda.fast.model.api.domain.asEntity
|
||||||
import dev.meloda.fast.model.api.requests.MessagesCreateChatRequest
|
|
||||||
import dev.meloda.fast.model.api.requests.MessagesGetByIdRequest
|
import dev.meloda.fast.model.api.requests.MessagesGetByIdRequest
|
||||||
import dev.meloda.fast.model.api.requests.MessagesGetHistoryAttachmentsRequest
|
import dev.meloda.fast.model.api.requests.MessagesGetHistoryAttachmentsRequest
|
||||||
import dev.meloda.fast.model.api.requests.MessagesGetHistoryRequest
|
import dev.meloda.fast.model.api.requests.MessagesGetHistoryRequest
|
||||||
@@ -25,6 +23,7 @@ import dev.meloda.fast.network.RestApiErrorDomain
|
|||||||
import dev.meloda.fast.network.mapApiDefault
|
import dev.meloda.fast.network.mapApiDefault
|
||||||
import dev.meloda.fast.network.mapApiResult
|
import dev.meloda.fast.network.mapApiResult
|
||||||
import dev.meloda.fast.network.service.messages.MessagesService
|
import dev.meloda.fast.network.service.messages.MessagesService
|
||||||
|
import com.slack.eithernet.ApiResult
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@@ -199,23 +198,6 @@ class MessagesRepositoryImpl(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun createChat(
|
|
||||||
userIds: List<Int>?,
|
|
||||||
title: String?
|
|
||||||
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
|
||||||
val requestModel = MessagesCreateChatRequest(
|
|
||||||
userIds = userIds,
|
|
||||||
title = title
|
|
||||||
)
|
|
||||||
|
|
||||||
messagesService.createChat(requestModel.map).mapApiResult(
|
|
||||||
successMapper = { apiResponse ->
|
|
||||||
apiResponse.requireResponse().chatId
|
|
||||||
},
|
|
||||||
errorMapper = { error -> error?.toDomain() }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun storeMessages(messages: List<VkMessage>) {
|
override suspend fun storeMessages(messages: List<VkMessage>) {
|
||||||
messageDao.insertAll(messages.map(VkMessage::asEntity))
|
messageDao.insertAll(messages.map(VkMessage::asEntity))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,8 +20,4 @@ class GetLocalUserByIdUseCase(private val repository: UsersRepository) {
|
|||||||
|
|
||||||
emit(newState)
|
emit(newState)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun proceed(userId: Int): VkUser? {
|
|
||||||
return repository.getLocalUsers(userIds = listOf(userId)).singleOrNull()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,9 +41,15 @@ class LongPollUpdatesParser(
|
|||||||
fun parseNextUpdate(event: List<Any>) {
|
fun parseNextUpdate(event: List<Any>) {
|
||||||
val eventId = event.first().asInt()
|
val eventId = event.first().asInt()
|
||||||
|
|
||||||
when (val eventType = ApiEvent.parseOrNull(eventId)) {
|
val eventType: ApiEvent = try {
|
||||||
null -> Log.d("LongPollUpdatesParser", "parseNextUpdate: unknownEvent: $event")
|
ApiEvent.parse(eventId)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
Log.d("LongPollUpdatesParser", "parseNextUpdate: unknownEvent: $event")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
when (eventType) {
|
||||||
ApiEvent.MESSAGE_SET_FLAGS -> parseMessageSetFlags(eventType, event)
|
ApiEvent.MESSAGE_SET_FLAGS -> parseMessageSetFlags(eventType, event)
|
||||||
ApiEvent.MESSAGE_CLEAR_FLAGS -> parseMessageClearFlags(eventType, event)
|
ApiEvent.MESSAGE_CLEAR_FLAGS -> parseMessageClearFlags(eventType, event)
|
||||||
ApiEvent.MESSAGE_NEW -> parseMessageNew(eventType, event)
|
ApiEvent.MESSAGE_NEW -> parseMessageNew(eventType, event)
|
||||||
|
|||||||
@@ -42,11 +42,6 @@ interface MessagesUseCase {
|
|||||||
conversationMessageId: Int
|
conversationMessageId: Int
|
||||||
): Flow<State<List<VkAttachmentHistoryMessage>>>
|
): Flow<State<List<VkAttachmentHistoryMessage>>>
|
||||||
|
|
||||||
fun createChat(
|
|
||||||
userIds: List<Int>?,
|
|
||||||
title: String?
|
|
||||||
): Flow<State<Int>>
|
|
||||||
|
|
||||||
suspend fun storeMessage(message: VkMessage)
|
suspend fun storeMessage(message: VkMessage)
|
||||||
suspend fun storeMessages(messages: List<VkMessage>)
|
suspend fun storeMessages(messages: List<VkMessage>)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,14 +100,6 @@ class MessagesUseCaseImpl(
|
|||||||
emit(newState)
|
emit(newState)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createChat(userIds: List<Int>?, title: String?): Flow<State<Int>> = flow {
|
|
||||||
emit(State.Loading)
|
|
||||||
|
|
||||||
val newState = repository.createChat(userIds, title).mapToState()
|
|
||||||
|
|
||||||
emit(newState)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun storeMessage(message: VkMessage) {
|
override suspend fun storeMessage(message: VkMessage) {
|
||||||
repository.storeMessages(listOf(message))
|
repository.storeMessages(listOf(message))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ class OAuthUseCaseImpl(
|
|||||||
forceSms = forceSms
|
forceSms = forceSms
|
||||||
)
|
)
|
||||||
|
|
||||||
kotlin.runCatching {
|
|
||||||
val error = response.error?.let(VkOAuthError::parse)
|
val error = response.error?.let(VkOAuthError::parse)
|
||||||
val errorType = response.errorType?.let(VkOAuthErrorType::parse)
|
val errorType = response.errorType?.let(VkOAuthErrorType::parse)
|
||||||
|
|
||||||
@@ -121,12 +120,5 @@ class OAuthUseCaseImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
emit(newState)
|
emit(newState)
|
||||||
}.fold(
|
|
||||||
onSuccess = {
|
|
||||||
},
|
|
||||||
onFailure = {
|
|
||||||
emit(State.Error.TestError(it.stackTraceToString()))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,5 @@ enum class ApiEvent(val value: Int) {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun parse(value: Int) = entries.first { it.value == value }
|
fun parse(value: Int) = entries.first { it.value == value }
|
||||||
fun parseOrNull(value: Int) = entries.firstOrNull { it.value == value }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,4 @@ import androidx.compose.runtime.Immutable
|
|||||||
sealed class BaseError {
|
sealed class BaseError {
|
||||||
|
|
||||||
data object SessionExpired : BaseError()
|
data object SessionExpired : BaseError()
|
||||||
|
|
||||||
data class SimpleError(val message: String) : BaseError()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package dev.meloda.fast.model.api.data
|
package dev.meloda.fast.model.api.data
|
||||||
|
|
||||||
import com.squareup.moshi.JsonClass
|
|
||||||
import dev.meloda.fast.model.api.domain.VkVideoDomain
|
import dev.meloda.fast.model.api.domain.VkVideoDomain
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class VkVideoData(
|
data class VkVideoData(
|
||||||
@@ -12,7 +12,7 @@ data class VkVideoData(
|
|||||||
val duration: Int,
|
val duration: Int,
|
||||||
val date: Int,
|
val date: Int,
|
||||||
val comments: Int?,
|
val comments: Int?,
|
||||||
val description: String?,
|
val description: String,
|
||||||
val player: String?,
|
val player: String?,
|
||||||
val added: Int?,
|
val added: Int?,
|
||||||
val type: String,
|
val type: String,
|
||||||
@@ -20,9 +20,9 @@ data class VkVideoData(
|
|||||||
val access_key: String?,
|
val access_key: String?,
|
||||||
val owner_id: Int,
|
val owner_id: Int,
|
||||||
val is_favorite: Boolean?,
|
val is_favorite: Boolean?,
|
||||||
val image: List<Image>?,
|
val image: List<Image>,
|
||||||
val first_frame: List<FirstFrame>?,
|
val first_frame: List<FirstFrame>?,
|
||||||
val files: File?
|
val files: File?,
|
||||||
) : VkAttachmentData {
|
) : VkAttachmentData {
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
@@ -67,7 +67,7 @@ data class VkVideoData(
|
|||||||
fun toDomain() = VkVideoDomain(
|
fun toDomain() = VkVideoDomain(
|
||||||
id = id,
|
id = id,
|
||||||
ownerId = owner_id,
|
ownerId = owner_id,
|
||||||
images = image.orEmpty().map { it.asVideoImage() },
|
images = image.map { it.asVideoImage() },
|
||||||
firstFrames = first_frame,
|
firstFrames = first_frame,
|
||||||
accessKey = access_key,
|
accessKey = access_key,
|
||||||
title = title
|
title = title
|
||||||
|
|||||||
@@ -38,41 +38,6 @@ data class VkConversation(
|
|||||||
fun isPinned(): Boolean = majorId > 0
|
fun isPinned(): Boolean = majorId > 0
|
||||||
fun isInUnread() = inRead - (lastMessageId ?: 0) < 0
|
fun isInUnread() = inRead - (lastMessageId ?: 0) < 0
|
||||||
fun isOutUnread() = outRead - (lastMessageId ?: 0) < 0
|
fun isOutUnread() = outRead - (lastMessageId ?: 0) < 0
|
||||||
|
|
||||||
companion object {
|
|
||||||
val EMPTY: VkConversation = VkConversation(
|
|
||||||
id = -1,
|
|
||||||
localId = -1,
|
|
||||||
ownerId = null,
|
|
||||||
title = "...",
|
|
||||||
photo50 = null,
|
|
||||||
photo100 = null,
|
|
||||||
photo200 = null,
|
|
||||||
isCallInProgress = false,
|
|
||||||
isPhantom = false,
|
|
||||||
lastConversationMessageId = -1,
|
|
||||||
inReadCmId = -1,
|
|
||||||
outReadCmId = -1,
|
|
||||||
inRead = -1,
|
|
||||||
outRead = -1,
|
|
||||||
lastMessageId = null,
|
|
||||||
unreadCount = -1,
|
|
||||||
membersCount = null,
|
|
||||||
canChangePin = false,
|
|
||||||
canChangeInfo = false,
|
|
||||||
majorId = -1,
|
|
||||||
minorId = -1,
|
|
||||||
pinnedMessageId = null,
|
|
||||||
interactionType = -1,
|
|
||||||
interactionIds = emptyList(),
|
|
||||||
peerType = PeerType.USER,
|
|
||||||
lastMessage = null,
|
|
||||||
pinnedMessage = null,
|
|
||||||
user = null,
|
|
||||||
group = null
|
|
||||||
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun VkConversation.asEntity(): VkConversationEntity = VkConversationEntity(
|
fun VkConversation.asEntity(): VkConversationEntity = VkConversationEntity(
|
||||||
|
|||||||
@@ -38,10 +38,11 @@ data class VkMessage(
|
|||||||
|
|
||||||
fun isGroup() = fromId < 0
|
fun isGroup() = fromId < 0
|
||||||
|
|
||||||
fun isRead(conversation: VkConversation): Boolean = when {
|
fun isRead(conversation: VkConversation) =
|
||||||
id <= 0 -> false
|
if (isOut) {
|
||||||
isOut -> conversation.outRead - id >= 0
|
conversation.outRead - id >= 0
|
||||||
else -> conversation.inRead - id >= 0
|
} else {
|
||||||
|
conversation.inRead - id >= 0
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hasAttachments(): Boolean = attachments.orEmpty().isNotEmpty()
|
fun hasAttachments(): Boolean = attachments.orEmpty().isNotEmpty()
|
||||||
|
|||||||
@@ -267,14 +267,3 @@ data class MessagesGetHistoryAttachmentsRequest(
|
|||||||
fields?.let { this["fields"] = it }
|
fields?.let { this["fields"] = it }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class MessagesCreateChatRequest(
|
|
||||||
val userIds: List<Int>?,
|
|
||||||
val title: String?
|
|
||||||
) {
|
|
||||||
|
|
||||||
val map = mutableMapOf<String, String>().apply {
|
|
||||||
userIds?.let { this["user_ids"] = it.joinToString(",") }
|
|
||||||
title?.let { this["title"] = it }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
package dev.meloda.fast.model.api.responses
|
package dev.meloda.fast.model.api.responses
|
||||||
|
|
||||||
import com.squareup.moshi.Json
|
|
||||||
import com.squareup.moshi.JsonClass
|
|
||||||
import dev.meloda.fast.model.api.data.VkAttachmentHistoryMessageData
|
import dev.meloda.fast.model.api.data.VkAttachmentHistoryMessageData
|
||||||
import dev.meloda.fast.model.api.data.VkChatMemberData
|
import dev.meloda.fast.model.api.data.VkChatMemberData
|
||||||
import dev.meloda.fast.model.api.data.VkContactData
|
import dev.meloda.fast.model.api.data.VkContactData
|
||||||
@@ -9,6 +7,8 @@ import dev.meloda.fast.model.api.data.VkConversationData
|
|||||||
import dev.meloda.fast.model.api.data.VkGroupData
|
import dev.meloda.fast.model.api.data.VkGroupData
|
||||||
import dev.meloda.fast.model.api.data.VkMessageData
|
import dev.meloda.fast.model.api.data.VkMessageData
|
||||||
import dev.meloda.fast.model.api.data.VkUserData
|
import dev.meloda.fast.model.api.data.VkUserData
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class MessagesGetHistoryResponse(
|
data class MessagesGetHistoryResponse(
|
||||||
@@ -44,9 +44,3 @@ data class MessagesGetHistoryAttachmentsResponse(
|
|||||||
@Json(name = "groups") val groups: List<VkGroupData>?,
|
@Json(name = "groups") val groups: List<VkGroupData>?,
|
||||||
@Json(name = "contacts") val contacts: List<VkContactData>?
|
@Json(name = "contacts") val contacts: List<VkContactData>?
|
||||||
)
|
)
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
data class MessagesCreateChatResponse(
|
|
||||||
@Json(name = "chat_id") val chatId: Int,
|
|
||||||
@Json(name = "peer_ids") val peerIds: List<Int>
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -43,22 +43,11 @@ class ResponseConverterFactory(private val converter: JsonConverter) : Converter
|
|||||||
converter.fromJson(successType, string)
|
converter.fromJson(successType, string)
|
||||||
}.fold(
|
}.fold(
|
||||||
onSuccess = { successModel ->
|
onSuccess = { successModel ->
|
||||||
if (successModel is ApiResponse<*>) {
|
|
||||||
if (successModel.error != null) {
|
|
||||||
throw ApiException(successModel.error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return successModel
|
return successModel
|
||||||
},
|
},
|
||||||
onFailure = { failure ->
|
onFailure = { failure ->
|
||||||
if (failure is JsonDataException) {
|
if(failure is JsonDataException) {
|
||||||
throw ApiException(
|
throw failure
|
||||||
RestApiError(
|
|
||||||
errorCode = -1,
|
|
||||||
errorMsg = failure.message.orEmpty()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val isUnit = successType == Unit::class.java
|
val isUnit = successType == Unit::class.java
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ enum class ValidationType(val value: String) {
|
|||||||
SMS("2fa_sms");
|
SMS("2fa_sms");
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun parse(value: String): ValidationType =
|
fun parse(value: String): ValidationType = entries.first { it.value == value }
|
||||||
entries.firstOrNull { it.value == value }
|
|
||||||
?: throw IllegalArgumentException("Unknown validation type $value")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package dev.meloda.fast.network
|
package dev.meloda.fast.network
|
||||||
|
|
||||||
enum class VkErrorCode(val code: Int) {
|
enum class VkErrorCode(val code: Int) {
|
||||||
WTF(-1),
|
|
||||||
UNKNOWN_ERROR(1),
|
UNKNOWN_ERROR(1),
|
||||||
APP_DISABLED(2),
|
APP_DISABLED(2),
|
||||||
UNKNOWN_METHOD(3),
|
UNKNOWN_METHOD(3),
|
||||||
@@ -42,8 +41,6 @@ enum class VkErrorCode(val code: Int) {
|
|||||||
ACCESS_TO_DOC_DENIED(1153),
|
ACCESS_TO_DOC_DENIED(1153),
|
||||||
|
|
||||||
SOME_AUTH_ERROR(104),
|
SOME_AUTH_ERROR(104),
|
||||||
|
|
||||||
CANNOT_SEND_MESSAGE_DUE_TO_PRIVACY_SETTINGS(902),
|
|
||||||
ACCESS_TOKEN_EXPIRED(1117);
|
ACCESS_TOKEN_EXPIRED(1117);
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.slack.eithernet.integration.retrofit.ApiResultCallAdapterFactory
|
|||||||
import com.slack.eithernet.integration.retrofit.ApiResultConverterFactory
|
import com.slack.eithernet.integration.retrofit.ApiResultConverterFactory
|
||||||
import com.squareup.moshi.Moshi
|
import com.squareup.moshi.Moshi
|
||||||
import dev.meloda.fast.common.AppConstants
|
import dev.meloda.fast.common.AppConstants
|
||||||
|
import dev.meloda.fast.common.model.LogLevel
|
||||||
import dev.meloda.fast.datastore.AppSettings
|
import dev.meloda.fast.datastore.AppSettings
|
||||||
import dev.meloda.fast.network.JsonConverter
|
import dev.meloda.fast.network.JsonConverter
|
||||||
import dev.meloda.fast.network.MoshiConverter
|
import dev.meloda.fast.network.MoshiConverter
|
||||||
@@ -56,8 +57,12 @@ val networkModule = module {
|
|||||||
.followSslRedirects(true)
|
.followSslRedirects(true)
|
||||||
.addInterceptor(
|
.addInterceptor(
|
||||||
HttpLoggingInterceptor().apply {
|
HttpLoggingInterceptor().apply {
|
||||||
level =
|
level = when (AppSettings.Debug.networkLogLevel) {
|
||||||
HttpLoggingInterceptor.Level.entries[AppSettings.Debug.networkLogLevel.ordinal]
|
LogLevel.NONE -> HttpLoggingInterceptor.Level.NONE
|
||||||
|
LogLevel.BASIC -> HttpLoggingInterceptor.Level.BASIC
|
||||||
|
LogLevel.HEADERS -> HttpLoggingInterceptor.Level.HEADERS
|
||||||
|
LogLevel.BODY -> HttpLoggingInterceptor.Level.BODY
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
|
|||||||
+1
-8
@@ -1,13 +1,12 @@
|
|||||||
package dev.meloda.fast.network.service.messages
|
package dev.meloda.fast.network.service.messages
|
||||||
|
|
||||||
import com.slack.eithernet.ApiResult
|
|
||||||
import dev.meloda.fast.model.api.data.VkLongPollData
|
import dev.meloda.fast.model.api.data.VkLongPollData
|
||||||
import dev.meloda.fast.model.api.responses.MessagesCreateChatResponse
|
|
||||||
import dev.meloda.fast.model.api.responses.MessagesGetByIdResponse
|
import dev.meloda.fast.model.api.responses.MessagesGetByIdResponse
|
||||||
import dev.meloda.fast.model.api.responses.MessagesGetHistoryAttachmentsResponse
|
import dev.meloda.fast.model.api.responses.MessagesGetHistoryAttachmentsResponse
|
||||||
import dev.meloda.fast.model.api.responses.MessagesGetHistoryResponse
|
import dev.meloda.fast.model.api.responses.MessagesGetHistoryResponse
|
||||||
import dev.meloda.fast.network.ApiResponse
|
import dev.meloda.fast.network.ApiResponse
|
||||||
import dev.meloda.fast.network.RestApiError
|
import dev.meloda.fast.network.RestApiError
|
||||||
|
import com.slack.eithernet.ApiResult
|
||||||
import retrofit2.http.FieldMap
|
import retrofit2.http.FieldMap
|
||||||
import retrofit2.http.FormUrlEncoded
|
import retrofit2.http.FormUrlEncoded
|
||||||
import retrofit2.http.POST
|
import retrofit2.http.POST
|
||||||
@@ -50,12 +49,6 @@ interface MessagesService {
|
|||||||
@FieldMap params: Map<String, String>
|
@FieldMap params: Map<String, String>
|
||||||
): ApiResult<ApiResponse<MessagesGetHistoryAttachmentsResponse>, RestApiError>
|
): ApiResult<ApiResponse<MessagesGetHistoryAttachmentsResponse>, RestApiError>
|
||||||
|
|
||||||
@FormUrlEncoded
|
|
||||||
@POST(MessagesUrls.CREATE_CHAT)
|
|
||||||
suspend fun createChat(
|
|
||||||
@FieldMap params: Map<String, String>
|
|
||||||
): ApiResult<ApiResponse<MessagesCreateChatResponse>, RestApiError>
|
|
||||||
|
|
||||||
// @FormUrlEncoded
|
// @FormUrlEncoded
|
||||||
// @POST(MessagesUrls.MarkAsImportant)
|
// @POST(MessagesUrls.MarkAsImportant)
|
||||||
// suspend fun markAsImportant(
|
// suspend fun markAsImportant(
|
||||||
|
|||||||
@@ -19,5 +19,4 @@ object MessagesUrls {
|
|||||||
const val GET_CONVERSATIONS_MEMBERS = "${AppConstants.URL_API}/messages.getConversationMembers"
|
const val GET_CONVERSATIONS_MEMBERS = "${AppConstants.URL_API}/messages.getConversationMembers"
|
||||||
const val REMOVE_CHAT_USER = "${AppConstants.URL_API}/messages.removeChatUser"
|
const val REMOVE_CHAT_USER = "${AppConstants.URL_API}/messages.removeChatUser"
|
||||||
const val GET_HISTORY_ATTACHMENTS = "${AppConstants.URL_API}/messages.getHistoryAttachments"
|
const val GET_HISTORY_ATTACHMENTS = "${AppConstants.URL_API}/messages.getHistoryAttachments"
|
||||||
const val CREATE_CHAT = "${AppConstants.URL_API}/messages.createChat"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
/build
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
plugins {
|
|
||||||
alias(libs.plugins.fast.android.library)
|
|
||||||
alias(libs.plugins.fast.android.library.compose)
|
|
||||||
alias(libs.plugins.kotlin.serialization)
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace = "dev.meloda.fast.presentation"
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
|
|
||||||
</manifest>
|
|
||||||
@@ -11,7 +11,6 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
api(projects.core.common)
|
api(projects.core.common)
|
||||||
api(projects.core.model)
|
api(projects.core.model)
|
||||||
implementation(projects.core.presentation)
|
|
||||||
|
|
||||||
implementation(libs.haze)
|
implementation(libs.haze)
|
||||||
implementation(libs.haze.materials)
|
implementation(libs.haze.materials)
|
||||||
|
|||||||
@@ -5,14 +5,12 @@ import androidx.compose.foundation.layout.Column
|
|||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@@ -24,16 +22,13 @@ fun ErrorView(
|
|||||||
onButtonClick: (() -> Unit)? = null,
|
onButtonClick: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier.fillMaxSize(),
|
||||||
.fillMaxSize()
|
|
||||||
.padding(horizontal = 16.dp),
|
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = text,
|
text = text,
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge
|
||||||
textAlign = TextAlign.Center
|
|
||||||
)
|
)
|
||||||
|
|
||||||
buttonText?.let {
|
buttonText?.let {
|
||||||
|
|||||||
@@ -29,9 +29,9 @@ import androidx.compose.ui.unit.dp
|
|||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun IconButton(
|
fun IconButton(
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
onClick: () -> Unit = {},
|
onClick: () -> Unit = {},
|
||||||
onLongClick: (() -> Unit)? = null,
|
onLongClick: (() -> Unit)? = null,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
colors: IconButtonColors = IconButtonDefaults.iconButtonColors(),
|
colors: IconButtonColors = IconButtonDefaults.iconButtonColors(),
|
||||||
interactionSource: MutableInteractionSource? = null,
|
interactionSource: MutableInteractionSource? = null,
|
||||||
|
|||||||
@@ -1,51 +1,29 @@
|
|||||||
package dev.meloda.fast.ui.components
|
package dev.meloda.fast.ui.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
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.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import dev.meloda.fast.ui.R
|
import dev.meloda.fast.ui.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NoItemsView(
|
fun NoItemsView(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
customText: String? = null,
|
customText: String? = null
|
||||||
buttonText: String? = null,
|
|
||||||
onButtonClick: (() -> Unit)? = null,
|
|
||||||
) {
|
) {
|
||||||
Column(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier.fillMaxSize(),
|
||||||
.fillMaxSize()
|
contentAlignment = Alignment.Center
|
||||||
.padding(horizontal = 16.dp),
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = customText ?: stringResource(R.string.no_items),
|
text = customText ?: stringResource(id = R.string.no_items),
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge
|
||||||
textAlign = TextAlign.Center
|
|
||||||
)
|
)
|
||||||
|
|
||||||
buttonText?.let {
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
Button(
|
|
||||||
onClick = { onButtonClick?.invoke() }
|
|
||||||
) {
|
|
||||||
Text(text = buttonText)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +31,6 @@ fun NoItemsView(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun NoItemsViewPreview() {
|
private fun NoItemsViewPreview() {
|
||||||
NoItemsView(
|
NoItemsView(
|
||||||
customText = "Nothing here...",
|
customText = "Nothing here..."
|
||||||
buttonText = "Refresh"
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
package dev.meloda.fast.ui.model.api
|
|
||||||
|
|
||||||
import dev.meloda.fast.common.model.UiImage
|
|
||||||
import dev.meloda.fast.common.model.UiText
|
|
||||||
import dev.meloda.fast.ui.R
|
|
||||||
|
|
||||||
sealed class ConversationOption(
|
|
||||||
val title: UiText,
|
|
||||||
val icon: UiImage
|
|
||||||
) {
|
|
||||||
|
|
||||||
data object MarkAsRead : ConversationOption(
|
|
||||||
title = UiText.Resource(R.string.action_mark_as_read),
|
|
||||||
icon = UiImage.Resource(R.drawable.round_done_all_24)
|
|
||||||
)
|
|
||||||
|
|
||||||
data object Pin : ConversationOption(
|
|
||||||
title = UiText.Resource(R.string.action_pin),
|
|
||||||
icon = UiImage.Resource(R.drawable.pin_outline_24)
|
|
||||||
)
|
|
||||||
|
|
||||||
data object Unpin : ConversationOption(
|
|
||||||
title = UiText.Resource(R.string.action_unpin),
|
|
||||||
icon = UiImage.Resource(R.drawable.pin_off_outline_24)
|
|
||||||
)
|
|
||||||
|
|
||||||
data object Delete : ConversationOption(
|
|
||||||
title = UiText.Resource(R.string.action_delete),
|
|
||||||
icon = UiImage.Resource(R.drawable.round_delete_outline_24)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package dev.meloda.fast.ui.util
|
package dev.meloda.fast.ui.util
|
||||||
|
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
import android.graphics.drawable.ColorDrawable
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
@@ -20,7 +21,6 @@ import androidx.compose.ui.res.painterResource
|
|||||||
import androidx.compose.ui.res.pluralStringResource
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import androidx.core.graphics.drawable.toDrawable
|
|
||||||
import dev.meloda.fast.common.model.DarkMode
|
import dev.meloda.fast.common.model.DarkMode
|
||||||
import dev.meloda.fast.common.model.UiImage
|
import dev.meloda.fast.common.model.UiImage
|
||||||
import dev.meloda.fast.common.model.UiText
|
import dev.meloda.fast.common.model.UiText
|
||||||
@@ -64,8 +64,8 @@ fun UiImage.getResourcePainter(): Painter? {
|
|||||||
@Composable
|
@Composable
|
||||||
fun UiImage.getImage(): Any {
|
fun UiImage.getImage(): Any {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
is UiImage.Color -> color.toDrawable()
|
is UiImage.Color -> ColorDrawable(color)
|
||||||
is UiImage.ColorResource -> colorResource(id = resId).toArgb().toDrawable()
|
is UiImage.ColorResource -> ColorDrawable(colorResource(id = resId).toArgb())
|
||||||
is UiImage.Resource -> painterResource(id = resId)
|
is UiImage.Resource -> painterResource(id = resId)
|
||||||
is UiImage.Simple -> drawable
|
is UiImage.Simple -> drawable
|
||||||
is UiImage.Url -> url
|
is UiImage.Url -> url
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
|
|
||||||
<path
|
<path android:fillColor="@android:color/white" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10s10,-4.48 10,-10S17.52,2 12,2zM12,6c1.93,0 3.5,1.57 3.5,3.5S13.93,13 12,13s-3.5,-1.57 -3.5,-3.5S10.07,6 12,6zM12,20c-2.03,0 -4.43,-0.82 -6.14,-2.88C7.55,15.8 9.68,15 12,15s4.45,0.8 6.14,2.12C16.43,19.18 14.03,20 12,20z"/>
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10s10,-4.48 10,-10S17.52,2 12,2zM12,6c1.93,0 3.5,1.57 3.5,3.5S13.93,13 12,13s-3.5,-1.57 -3.5,-3.5S10.07,6 12,6zM12,20c-2.03,0 -4.43,-0.82 -6.14,-2.88C7.55,15.8 9.68,15 12,15s4.45,0.8 6.14,2.12C16.43,19.18 14.03,20 12,20z" />
|
|
||||||
|
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:autoMirrored="true"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
|
|
||||||
<path
|
<path android:fillColor="@android:color/white" android:pathData="M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM6,9h12v2L6,11L6,9zM14,14L6,14v-2h8v2zM18,8L6,8L6,6h12v2z"/>
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM6,9h12v2L6,11L6,9zM14,14L6,14v-2h8v2zM18,8L6,8L6,6h12v2z" />
|
|
||||||
|
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -1,27 +1,11 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
|
|
||||||
<path
|
<path android:fillColor="@android:color/white" android:fillType="evenOdd" android:pathData="M16.67,13.13C18.04,14.06 19,15.32 19,17v3h4v-3C23,14.82 19.43,13.53 16.67,13.13z"/>
|
||||||
android:fillColor="#ffffff"
|
|
||||||
android:fillType="evenOdd"
|
|
||||||
android:pathData="M16.67,13.13C18.04,14.06 19,15.32 19,17v3h4v-3C23,14.82 19.43,13.53 16.67,13.13z" />
|
|
||||||
|
|
||||||
<path
|
<path android:fillColor="@android:color/white" android:fillType="evenOdd" android:pathData="M9,8m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0"/>
|
||||||
android:fillColor="#ffffff"
|
|
||||||
android:fillType="evenOdd"
|
|
||||||
android:pathData="M9,8m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0" />
|
|
||||||
|
|
||||||
<path
|
<path android:fillColor="@android:color/white" android:fillType="evenOdd" android:pathData="M15,12c2.21,0 4,-1.79 4,-4c0,-2.21 -1.79,-4 -4,-4c-0.47,0 -0.91,0.1 -1.33,0.24C14.5,5.27 15,6.58 15,8s-0.5,2.73 -1.33,3.76C14.09,11.9 14.53,12 15,12z"/>
|
||||||
android:fillColor="#ffffff"
|
|
||||||
android:fillType="evenOdd"
|
|
||||||
android:pathData="M15,12c2.21,0 4,-1.79 4,-4c0,-2.21 -1.79,-4 -4,-4c-0.47,0 -0.91,0.1 -1.33,0.24C14.5,5.27 15,6.58 15,8s-0.5,2.73 -1.33,3.76C14.09,11.9 14.53,12 15,12z" />
|
|
||||||
|
|
||||||
<path
|
<path android:fillColor="@android:color/white" android:fillType="evenOdd" android:pathData="M9,13c-2.67,0 -8,1.34 -8,4v3h16v-3C17,14.34 11.67,13 9,13z"/>
|
||||||
android:fillColor="#ffffff"
|
|
||||||
android:fillType="evenOdd"
|
|
||||||
android:pathData="M9,13c-2.67,0 -8,1.34 -8,4v3h16v-3C17,14.34 11.67,13 9,13z" />
|
|
||||||
|
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="28dp"
|
||||||
|
android:height="28dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M12,7c-0.55,0 -1,0.45 -1,1v3L8,11c-0.55,0 -1,0.45 -1,1s0.45,1 1,1h3v3c0,0.55 0.45,1 1,1s1,-0.45 1,-1v-3h3c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1h-3L13,8c0,-0.55 -0.45,-1 -1,-1zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M18.3,5.71c-0.39,-0.39 -1.02,-0.39 -1.41,0L12,10.59 7.11,5.7c-0.39,-0.39 -1.02,-0.39 -1.41,0 -0.39,0.39 -0.39,1.02 0,1.41L10.59,12 5.7,16.89c-0.39,0.39 -0.39,1.02 0,1.41 0.39,0.39 1.02,0.39 1.41,0L12,13.41l4.89,4.89c0.39,0.39 1.02,0.39 1.41,0 0.39,-0.39 0.39,-1.02 0,-1.41L13.41,12l4.89,-4.89c0.38,-0.38 0.38,-1.02 0,-1.4z" />
|
||||||
|
</vector>
|
||||||
@@ -1,15 +1,7 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
|
|
||||||
<path
|
<path android:fillColor="@android:color/white" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10s10,-4.48 10,-10S17.52,2 12,2zM7.35,18.5C8.66,17.56 10.26,17 12,17s3.34,0.56 4.65,1.5C15.34,19.44 13.74,20 12,20S8.66,19.44 7.35,18.5zM18.14,17.12L18.14,17.12C16.45,15.8 14.32,15 12,15s-4.45,0.8 -6.14,2.12l0,0C4.7,15.73 4,13.95 4,12c0,-4.42 3.58,-8 8,-8s8,3.58 8,8C20,13.95 19.3,15.73 18.14,17.12z"/>
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10s10,-4.48 10,-10S17.52,2 12,2zM7.35,18.5C8.66,17.56 10.26,17 12,17s3.34,0.56 4.65,1.5C15.34,19.44 13.74,20 12,20S8.66,19.44 7.35,18.5zM18.14,17.12L18.14,17.12C16.45,15.8 14.32,15 12,15s-4.45,0.8 -6.14,2.12l0,0C4.7,15.73 4,13.95 4,12c0,-4.42 3.58,-8 8,-8s8,3.58 8,8C20,13.95 19.3,15.73 18.14,17.12z" />
|
|
||||||
|
|
||||||
<path
|
<path android:fillColor="@android:color/white" android:pathData="M12,6c-1.93,0 -3.5,1.57 -3.5,3.5S10.07,13 12,13s3.5,-1.57 3.5,-3.5S13.93,6 12,6zM12,11c-0.83,0 -1.5,-0.67 -1.5,-1.5S11.17,8 12,8s1.5,0.67 1.5,1.5S12.83,11 12,11z"/>
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="M12,6c-1.93,0 -3.5,1.57 -3.5,3.5S10.07,13 12,13s3.5,-1.57 3.5,-3.5S13.93,6 12,6zM12,11c-0.83,0 -1.5,-0.67 -1.5,-1.5S11.17,8 12,8s1.5,0.67 1.5,1.5S12.83,11 12,11z" />
|
|
||||||
|
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:autoMirrored="true"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
|
|
||||||
<path
|
<path android:fillColor="@android:color/white" android:pathData="M4,4h16v12L5.17,16L4,17.17L4,4m0,-2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2L4,2zM6,12h8v2L6,14v-2zM6,9h12v2L6,11L6,9zM6,6h12v2L6,8L6,6z"/>
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="M4,4h16v12L5.17,16L4,17.17L4,4m0,-2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2L4,2zM6,12h8v2L6,14v-2zM6,9h12v2L6,11L6,9zM6,6h12v2L6,8L6,6z" />
|
|
||||||
|
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -1,23 +1,11 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
|
|
||||||
<path
|
<path android:fillColor="@android:color/white" android:pathData="M16.67,13.13C18.04,14.06 19,15.32 19,17v3h4v-3C23,14.82 19.43,13.53 16.67,13.13z"/>
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="M16.67,13.13C18.04,14.06 19,15.32 19,17v3h4v-3C23,14.82 19.43,13.53 16.67,13.13z" />
|
|
||||||
|
|
||||||
<path
|
<path android:fillColor="@android:color/white" android:pathData="M15,12c2.21,0 4,-1.79 4,-4c0,-2.21 -1.79,-4 -4,-4c-0.47,0 -0.91,0.1 -1.33,0.24C14.5,5.27 15,6.58 15,8s-0.5,2.73 -1.33,3.76C14.09,11.9 14.53,12 15,12z"/>
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="M15,12c2.21,0 4,-1.79 4,-4c0,-2.21 -1.79,-4 -4,-4c-0.47,0 -0.91,0.1 -1.33,0.24C14.5,5.27 15,6.58 15,8s-0.5,2.73 -1.33,3.76C14.09,11.9 14.53,12 15,12z" />
|
|
||||||
|
|
||||||
<path
|
<path android:fillColor="@android:color/white" android:pathData="M9,12c2.21,0 4,-1.79 4,-4c0,-2.21 -1.79,-4 -4,-4S5,5.79 5,8C5,10.21 6.79,12 9,12zM9,6c1.1,0 2,0.9 2,2c0,1.1 -0.9,2 -2,2S7,9.1 7,8C7,6.9 7.9,6 9,6z"/>
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="M9,12c2.21,0 4,-1.79 4,-4c0,-2.21 -1.79,-4 -4,-4S5,5.79 5,8C5,10.21 6.79,12 9,12zM9,6c1.1,0 2,0.9 2,2c0,1.1 -0.9,2 -2,2S7,9.1 7,8C7,6.9 7.9,6 9,6z" />
|
|
||||||
|
|
||||||
<path
|
<path android:fillColor="@android:color/white" android:pathData="M9,13c-2.67,0 -8,1.34 -8,4v3h16v-3C17,14.34 11.67,13 9,13zM15,18H3l0,-0.99C3.2,16.29 6.3,15 9,15s5.8,1.29 6,2V18z"/>
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="M9,13c-2.67,0 -8,1.34 -8,4v3h16v-3C17,14.34 11.67,13 9,13zM15,18H3l0,-0.99C3.2,16.29 6.3,15 9,15s5.8,1.29 6,2V18z" />
|
|
||||||
|
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
|
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8zM11.78,7h-0.06c-0.4,0 -0.72,0.32 -0.72,0.72v4.72c0,0.35 0.18,0.68 0.49,0.86l4.15,2.49c0.34,0.2 0.78,0.1 0.98,-0.24 0.21,-0.34 0.1,-0.79 -0.25,-0.99l-3.87,-2.3L12.5,7.72c0,-0.4 -0.32,-0.72 -0.72,-0.72z" />
|
|
||||||
|
|
||||||
</vector>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
|
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="M12,7c0.55,0 1,0.45 1,1v4c0,0.55 -0.45,1 -1,1s-1,-0.45 -1,-1L11,8c0,-0.55 0.45,-1 1,-1zM11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8zM13,17h-2v-2h2v2z" />
|
|
||||||
|
|
||||||
</vector>
|
|
||||||
@@ -128,12 +128,11 @@
|
|||||||
<string name="post_type_community">Запись сообщества</string>
|
<string name="post_type_community">Запись сообщества</string>
|
||||||
<string name="post_type_user">Запись пользователя</string>
|
<string name="post_type_user">Запись пользователя</string>
|
||||||
<string name="post_type_unknown">Запись на стене</string>
|
<string name="post_type_unknown">Запись на стене</string>
|
||||||
<string name="action_log_out">Выйти</string>
|
<string name="log_out">Выйти</string>
|
||||||
<string name="confirm">Подтверждение</string>
|
<string name="confirm">Подтверждение</string>
|
||||||
<string name="message_attachment_story_your_story">Ваша история</string>
|
<string name="message_attachment_story_your_story">Ваша история</string>
|
||||||
<string name="settings_dynamic_colors">Динамические цвета</string>
|
<string name="settings_dynamic_colors">Динамические цвета</string>
|
||||||
<string name="settings_dynamic_colors_description">Цвета для приложения будут извлечены из ваших обоев на главном экране</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">Язык приложения</string>
|
||||||
<string name="settings_application_language_value">Текущий: %1$s</string>
|
<string name="settings_application_language_value">Текущий: %1$s</string>
|
||||||
<string name="language_system">Системный</string>
|
<string name="language_system">Системный</string>
|
||||||
@@ -178,7 +177,6 @@
|
|||||||
<string name="settings_general_title">Основное</string>
|
<string name="settings_general_title">Основное</string>
|
||||||
<string name="settings_general_contact_names_title">Использовать имена контактов</string>
|
<string name="settings_general_contact_names_title">Использовать имена контактов</string>
|
||||||
<string name="settings_general_contact_names_summary">Приложение будет использовать доступные имена контактов для пользователей</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_title">Внешний вид</string>
|
||||||
<string name="settings_appearance_multiline_title">Многострочные заголовки и сообщения</string>
|
<string name="settings_appearance_multiline_title">Многострочные заголовки и сообщения</string>
|
||||||
<string name="settings_appearance_multiline_summary">Заголовок чата и текст сообщения смогут занимать несколько строчек</string>
|
<string name="settings_appearance_multiline_summary">Заголовок чата и текст сообщения смогут занимать несколько строчек</string>
|
||||||
@@ -186,11 +184,9 @@
|
|||||||
<string name="settings_features_fast_text_title">Fast текст</string>
|
<string name="settings_features_fast_text_title">Fast текст</string>
|
||||||
<string name="settings_features_long_poll_in_background_title">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_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_title">Активность</string>
|
||||||
<string name="settings_activity_send_online_title">Быть «в сети»</string>
|
<string name="settings_activity_send_online_title">Быть «в сети»</string>
|
||||||
<string name="settings_activity_send_online_summary">Статус «в сети» будет отправляться каждые 5 минут</string>
|
<string name="settings_activity_send_online_summary">Статус «в сети» будет отправляться каждые 5 минут</string>
|
||||||
<string name="settings_experimental_title">Экспериментальные - ОЧЕНЬ нестабильные</string>
|
|
||||||
<string name="settings_debug_title">Отладка</string>
|
<string name="settings_debug_title">Отладка</string>
|
||||||
<string name="action_disable">Отключить</string>
|
<string name="action_disable">Отключить</string>
|
||||||
<string name="background_long_poll_rationale_text">Приложение не сможет обновлять сообщения в фоне без доступа к уведомлениям</string>
|
<string name="background_long_poll_rationale_text">Приложение не сможет обновлять сообщения в фоне без доступа к уведомлениям</string>
|
||||||
@@ -202,20 +198,4 @@
|
|||||||
<string name="notification_channel_no_category_description">Уведомления без категории</string>
|
<string name="notification_channel_no_category_description">Уведомления без категории</string>
|
||||||
<string name="notification_channel_long_polling_service_name">Сервис обновления сообщений</string>
|
<string name="notification_channel_long_polling_service_name">Сервис обновления сообщений</string>
|
||||||
<string name="notification_channel_long_polling_service_description">Уведомления сервиса обновлений сообщений</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>
|
|
||||||
<string name="no_online_friends">Никого в сети</string>
|
|
||||||
<string name="try_again">Попробовать ещё раз</string>
|
|
||||||
<string name="session_expired">Срок действия сессии истёк</string>
|
|
||||||
<string name="title_create_chat">Создать чат</string>
|
|
||||||
<string name="action_create">Создать</string>
|
|
||||||
<string name="create_chat_title">Название</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -119,7 +119,7 @@
|
|||||||
<string name="post_type_community">Community post</string>
|
<string name="post_type_community">Community post</string>
|
||||||
<string name="post_type_user">User post</string>
|
<string name="post_type_user">User post</string>
|
||||||
<string name="post_type_unknown">Post</string>
|
<string name="post_type_unknown">Post</string>
|
||||||
<string name="action_log_out">Log out</string>
|
<string name="log_out">Log out</string>
|
||||||
<string name="confirm">Confirmation</string>
|
<string name="confirm">Confirmation</string>
|
||||||
<string name="sign_out_confirm">Signing out will delete all data related to this account from this device. Continue?</string>
|
<string name="sign_out_confirm">Signing out will delete all data related to this account from this device. Continue?</string>
|
||||||
<string name="yes">Yes</string>
|
<string name="yes">Yes</string>
|
||||||
@@ -204,8 +204,6 @@
|
|||||||
<string name="settings_dynamic_colors">Dynamic colors</string>
|
<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_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">Application Language</string>
|
||||||
<string name="settings_application_language_value">Current: %1$s</string>
|
<string name="settings_application_language_value">Current: %1$s</string>
|
||||||
|
|
||||||
@@ -237,9 +235,6 @@
|
|||||||
<string name="settings_general_title">General</string>
|
<string name="settings_general_title">General</string>
|
||||||
<string name="settings_general_contact_names_title">Use contact names</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_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_title">Appearance</string>
|
||||||
<string name="settings_appearance_multiline_title">Multiline titles and messages</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_appearance_multiline_summary">The title of the conversation and the text of the message can take up multiple lines</string>
|
||||||
@@ -247,18 +242,9 @@
|
|||||||
<string name="settings_features_fast_text_title">Fast text</string>
|
<string name="settings_features_fast_text_title">Fast text</string>
|
||||||
<string name="settings_features_long_poll_in_background_title">LongPoll in background</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_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_title">Activity</string>
|
||||||
<string name="settings_activity_send_online_title">Send online status</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_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="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="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>
|
<string name="action_disable">Disable</string>
|
||||||
@@ -276,11 +262,6 @@
|
|||||||
<string name="warning_confirmation">Confirmation</string>
|
<string name="warning_confirmation">Confirmation</string>
|
||||||
<string name="captcha_exit_warning">Are you sure? Captcha process will be cancelled</string>
|
<string name="captcha_exit_warning">Are you sure? Captcha process will be cancelled</string>
|
||||||
<string name="validation_exit_warning">Are you sure? Validation process will be cancelled</string>
|
<string name="validation_exit_warning">Are you sure? Validation process will be cancelled</string>
|
||||||
|
<string name="settings_general_enable_pull_to_refresh_title">Enable pull to refresh</string>
|
||||||
<string name="action_authorize">Authorize</string>
|
<string name="action_authorize">Authorize</string>
|
||||||
<string name="no_online_friends">No one is online</string>
|
|
||||||
<string name="try_again">Try again</string>
|
|
||||||
<string name="session_expired">Session expired</string>
|
|
||||||
<string name="title_create_chat">Create chat</string>
|
|
||||||
<string name="action_create">Create</string>
|
|
||||||
<string name="create_chat_title">Title</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -345,13 +345,6 @@ class LoginViewModelImpl(
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
is State.Error.TestError -> {
|
|
||||||
val message = stateError.message
|
|
||||||
val error = LoginError.SimpleError(message = message)
|
|
||||||
loginError.update { error }
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,5 +9,4 @@ sealed class LoginError {
|
|||||||
data object TooManyTries : LoginError()
|
data object TooManyTries : LoginError()
|
||||||
data object WrongValidationCode : LoginError()
|
data object WrongValidationCode : LoginError()
|
||||||
data object WrongValidationCodeFormat : LoginError()
|
data object WrongValidationCodeFormat : LoginError()
|
||||||
data class SimpleError(val message: String): LoginError()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ import androidx.compose.ui.text.input.TextFieldValue
|
|||||||
import androidx.compose.ui.text.input.VisualTransformation
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
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.CaptchaArguments
|
||||||
import dev.meloda.fast.auth.login.model.LoginError
|
import dev.meloda.fast.auth.login.model.LoginError
|
||||||
import dev.meloda.fast.auth.login.model.LoginScreenState
|
import dev.meloda.fast.auth.login.model.LoginScreenState
|
||||||
@@ -439,14 +441,5 @@ fun HandleError(
|
|||||||
confirmText = stringResource(id = UiR.string.ok)
|
confirmText = stringResource(id = UiR.string.ok)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is LoginError.SimpleError -> {
|
|
||||||
MaterialDialog(
|
|
||||||
onDismissRequest = onDismiss,
|
|
||||||
title = "Error",
|
|
||||||
text = error.message,
|
|
||||||
confirmText = stringResource(id = UiR.string.ok)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-6
@@ -62,8 +62,8 @@ import androidx.compose.ui.unit.LayoutDirection
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import dev.chrisbanes.haze.HazeState
|
import dev.chrisbanes.haze.HazeState
|
||||||
import dev.chrisbanes.haze.hazeEffect
|
import dev.chrisbanes.haze.haze
|
||||||
import dev.chrisbanes.haze.hazeSource
|
import dev.chrisbanes.haze.hazeChild
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
import dev.chrisbanes.haze.materials.HazeMaterials
|
import dev.chrisbanes.haze.materials.HazeMaterials
|
||||||
import dev.meloda.fast.chatmaterials.ChatMaterialsViewModel
|
import dev.meloda.fast.chatmaterials.ChatMaterialsViewModel
|
||||||
@@ -137,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 listState = rememberLazyListState()
|
||||||
val gridState = rememberLazyGridState()
|
val gridState = rememberLazyGridState()
|
||||||
@@ -179,7 +179,7 @@ fun ChatMaterialsScreen(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.then(
|
.then(
|
||||||
if (currentTheme.enableBlur) {
|
if (currentTheme.enableBlur) {
|
||||||
Modifier.hazeEffect(
|
Modifier.hazeChild(
|
||||||
state = hazeState,
|
state = hazeState,
|
||||||
style = hazeStyle
|
style = hazeStyle
|
||||||
)
|
)
|
||||||
@@ -311,7 +311,7 @@ fun ChatMaterialsScreen(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.then(
|
.then(
|
||||||
if (currentTheme.enableBlur) {
|
if (currentTheme.enableBlur) {
|
||||||
Modifier.hazeSource(state = hazeState)
|
Modifier.haze(state = hazeState)
|
||||||
} else {
|
} else {
|
||||||
Modifier
|
Modifier
|
||||||
}
|
}
|
||||||
@@ -346,7 +346,7 @@ fun ChatMaterialsScreen(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.then(
|
.then(
|
||||||
if (currentTheme.enableBlur) {
|
if (currentTheme.enableBlur) {
|
||||||
Modifier.hazeSource(state = hazeState)
|
Modifier.haze(state = hazeState)
|
||||||
} else {
|
} else {
|
||||||
Modifier
|
Modifier
|
||||||
}
|
}
|
||||||
|
|||||||
+58
-170
@@ -1,24 +1,23 @@
|
|||||||
package dev.meloda.fast.conversations
|
package dev.meloda.fast.conversations
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import coil.ImageLoader
|
|
||||||
import coil.request.ImageRequest
|
|
||||||
import com.conena.nanokt.collections.indexOfFirstOrNull
|
import com.conena.nanokt.collections.indexOfFirstOrNull
|
||||||
import dev.meloda.fast.common.extensions.createTimerFlow
|
import dev.meloda.fast.common.extensions.createTimerFlow
|
||||||
import dev.meloda.fast.common.extensions.findWithIndex
|
import dev.meloda.fast.common.extensions.findWithIndex
|
||||||
import dev.meloda.fast.common.extensions.listenValue
|
import dev.meloda.fast.common.extensions.listenValue
|
||||||
import dev.meloda.fast.common.extensions.setValue
|
import dev.meloda.fast.common.extensions.setValue
|
||||||
|
import dev.meloda.fast.conversations.model.ConversationOption
|
||||||
import dev.meloda.fast.conversations.model.ConversationsScreenState
|
import dev.meloda.fast.conversations.model.ConversationsScreenState
|
||||||
|
import dev.meloda.fast.conversations.model.ConversationsShowOptions
|
||||||
|
import dev.meloda.fast.conversations.model.UiConversation
|
||||||
import dev.meloda.fast.conversations.util.asPresentation
|
import dev.meloda.fast.conversations.util.asPresentation
|
||||||
import dev.meloda.fast.conversations.util.extractAvatar
|
import dev.meloda.fast.conversations.util.extractAvatar
|
||||||
import dev.meloda.fast.data.State
|
import dev.meloda.fast.data.State
|
||||||
import dev.meloda.fast.data.processState
|
import dev.meloda.fast.data.processState
|
||||||
import dev.meloda.fast.datastore.UserSettings
|
import dev.meloda.fast.datastore.UserSettings
|
||||||
import dev.meloda.fast.domain.ConversationsUseCase
|
import dev.meloda.fast.domain.ConversationsUseCase
|
||||||
import dev.meloda.fast.domain.LoadConversationsByIdUseCase
|
|
||||||
import dev.meloda.fast.domain.LongPollUpdatesParser
|
import dev.meloda.fast.domain.LongPollUpdatesParser
|
||||||
import dev.meloda.fast.domain.MessagesUseCase
|
import dev.meloda.fast.domain.MessagesUseCase
|
||||||
import dev.meloda.fast.model.BaseError
|
import dev.meloda.fast.model.BaseError
|
||||||
@@ -26,11 +25,10 @@ import dev.meloda.fast.model.InteractionType
|
|||||||
import dev.meloda.fast.model.LongPollEvent
|
import dev.meloda.fast.model.LongPollEvent
|
||||||
import dev.meloda.fast.model.api.domain.VkConversation
|
import dev.meloda.fast.model.api.domain.VkConversation
|
||||||
import dev.meloda.fast.network.VkErrorCode
|
import dev.meloda.fast.network.VkErrorCode
|
||||||
import dev.meloda.fast.ui.model.api.ConversationOption
|
|
||||||
import dev.meloda.fast.ui.model.api.ConversationsShowOptions
|
|
||||||
import dev.meloda.fast.ui.model.api.UiConversation
|
|
||||||
import dev.meloda.fast.ui.util.ImmutableList
|
import dev.meloda.fast.ui.util.ImmutableList
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -38,14 +36,17 @@ import kotlinx.coroutines.flow.launchIn
|
|||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.coroutines.cancellation.CancellationException
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
||||||
interface ConversationsViewModel {
|
interface ConversationsViewModel {
|
||||||
|
|
||||||
val screenState: StateFlow<ConversationsScreenState>
|
val screenState: StateFlow<ConversationsScreenState>
|
||||||
val baseError: StateFlow<BaseError?>
|
val baseError: StateFlow<BaseError?>
|
||||||
|
val imagesToPreload: StateFlow<List<String>>
|
||||||
val currentOffset: StateFlow<Int>
|
val currentOffset: StateFlow<Int>
|
||||||
val canPaginate: StateFlow<Boolean>
|
val canPaginate: StateFlow<Boolean>
|
||||||
|
val scrollToTop: StateFlow<Boolean>
|
||||||
|
|
||||||
fun onPaginationConditionsMet()
|
fun onPaginationConditionsMet()
|
||||||
|
|
||||||
@@ -66,6 +67,10 @@ interface ConversationsViewModel {
|
|||||||
|
|
||||||
fun setScrollIndex(index: Int)
|
fun setScrollIndex(index: Int)
|
||||||
fun setScrollOffset(offset: Int)
|
fun setScrollOffset(offset: Int)
|
||||||
|
|
||||||
|
|
||||||
|
fun setScrollToTopFlow(scrollToTopFlow: Flow<Int>)
|
||||||
|
fun onScrolledToTop()
|
||||||
}
|
}
|
||||||
|
|
||||||
class ConversationsViewModelImpl(
|
class ConversationsViewModelImpl(
|
||||||
@@ -73,18 +78,15 @@ class ConversationsViewModelImpl(
|
|||||||
private val conversationsUseCase: ConversationsUseCase,
|
private val conversationsUseCase: ConversationsUseCase,
|
||||||
private val messagesUseCase: MessagesUseCase,
|
private val messagesUseCase: MessagesUseCase,
|
||||||
private val resources: Resources,
|
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() {
|
) : ConversationsViewModel, ViewModel() {
|
||||||
|
|
||||||
override val screenState = MutableStateFlow(ConversationsScreenState.EMPTY)
|
override val screenState = MutableStateFlow(ConversationsScreenState.EMPTY)
|
||||||
override val baseError = MutableStateFlow<BaseError?>(null)
|
override val baseError = MutableStateFlow<BaseError?>(null)
|
||||||
|
override val imagesToPreload = MutableStateFlow<List<String>>(emptyList())
|
||||||
override val currentOffset = MutableStateFlow(0)
|
override val currentOffset = MutableStateFlow(0)
|
||||||
override val canPaginate = MutableStateFlow(false)
|
override val canPaginate = MutableStateFlow(false)
|
||||||
|
override val scrollToTop = MutableStateFlow(false)
|
||||||
private val useContactNames: Boolean get() = userSettings.useContactNames.value
|
|
||||||
|
|
||||||
override fun onPaginationConditionsMet() {
|
override fun onPaginationConditionsMet() {
|
||||||
currentOffset.update { screenState.value.conversations.size }
|
currentOffset.update { screenState.value.conversations.size }
|
||||||
@@ -122,7 +124,6 @@ class ConversationsViewModelImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onRefresh() {
|
override fun onRefresh() {
|
||||||
onErrorConsumed()
|
|
||||||
loadConversations(offset = 0)
|
loadConversations(offset = 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,10 +189,7 @@ class ConversationsViewModelImpl(
|
|||||||
onPinDialogDismissed()
|
onPinDialogDismissed()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionClicked(
|
override fun onOptionClicked(conversation: UiConversation, option: ConversationOption) {
|
||||||
conversation: UiConversation,
|
|
||||||
option: ConversationOption
|
|
||||||
) {
|
|
||||||
when (option) {
|
when (option) {
|
||||||
ConversationOption.Delete -> {
|
ConversationOption.Delete -> {
|
||||||
emitShowOptions { old ->
|
emitShowOptions { old ->
|
||||||
@@ -228,6 +226,20 @@ class ConversationsViewModelImpl(
|
|||||||
screenState.setValue { old -> old.copy(scrollOffset = offset) }
|
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) {
|
private fun hideOptions(conversationId: Int) {
|
||||||
screenState.setValue { old ->
|
screenState.setValue { old ->
|
||||||
old.copy(
|
old.copy(
|
||||||
@@ -251,7 +263,17 @@ class ConversationsViewModelImpl(
|
|||||||
conversationsUseCase.getConversations(count = LOAD_COUNT, offset = offset)
|
conversationsUseCase.getConversations(count = LOAD_COUNT, offset = offset)
|
||||||
.listenValue(viewModelScope) { state ->
|
.listenValue(viewModelScope) { state ->
|
||||||
state.processState(
|
state.processState(
|
||||||
error = ::handleError,
|
error = { error ->
|
||||||
|
if (error is State.Error.ApiError) {
|
||||||
|
when (error.errorCode) {
|
||||||
|
VkErrorCode.USER_AUTHORIZATION_FAILED -> {
|
||||||
|
baseError.setValue { BaseError.SessionExpired }
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
success = { response ->
|
success = { response ->
|
||||||
val itemsCountSufficient = response.size == LOAD_COUNT
|
val itemsCountSufficient = response.size == LOAD_COUNT
|
||||||
canPaginate.setValue { itemsCountSufficient }
|
canPaginate.setValue { itemsCountSufficient }
|
||||||
@@ -259,17 +281,9 @@ class ConversationsViewModelImpl(
|
|||||||
val paginationExhausted = !itemsCountSufficient &&
|
val paginationExhausted = !itemsCountSufficient &&
|
||||||
screenState.value.conversations.isNotEmpty()
|
screenState.value.conversations.isNotEmpty()
|
||||||
|
|
||||||
val imagesToPreload =
|
imagesToPreload.setValue {
|
||||||
response.mapNotNull { it.extractAvatar().extractUrl() }
|
response.mapNotNull { it.extractAvatar().extractUrl() }
|
||||||
|
|
||||||
imagesToPreload.forEach { url ->
|
|
||||||
imageLoader.enqueue(
|
|
||||||
ImageRequest.Builder(applicationContext)
|
|
||||||
.data(url)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
conversationsUseCase.storeConversations(response)
|
conversationsUseCase.storeConversations(response)
|
||||||
|
|
||||||
val loadedConversations = response.map {
|
val loadedConversations = response.map {
|
||||||
@@ -307,44 +321,6 @@ 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) {
|
private fun deleteConversation(peerId: Int) {
|
||||||
conversationsUseCase.delete(peerId).listenValue(viewModelScope) { state ->
|
conversationsUseCase.delete(peerId).listenValue(viewModelScope) { state ->
|
||||||
state.processState(
|
state.processState(
|
||||||
@@ -361,12 +337,7 @@ class ConversationsViewModelImpl(
|
|||||||
conversations.update { newConversations }
|
conversations.update { newConversations }
|
||||||
screenState.setValue { old ->
|
screenState.setValue { old ->
|
||||||
old.copy(
|
old.copy(
|
||||||
conversations = newConversations.map {
|
conversations = newConversations.map { it.asPresentation(resources) }
|
||||||
it.asPresentation(
|
|
||||||
resources = resources,
|
|
||||||
useContactName = useContactNames
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -402,40 +373,13 @@ class ConversationsViewModelImpl(
|
|||||||
|
|
||||||
private fun handleNewMessage(event: LongPollEvent.VkMessageNewEvent) {
|
private fun handleNewMessage(event: LongPollEvent.VkMessageNewEvent) {
|
||||||
val message = event.message
|
val message = event.message
|
||||||
|
|
||||||
val newConversations = conversations.value.toMutableList()
|
val newConversations = conversations.value.toMutableList()
|
||||||
val conversationIndex =
|
val conversationIndex =
|
||||||
newConversations.indexOfFirstOrNull { it.id == message.peerId }
|
newConversations.indexOfFirstOrNull { it.id == message.peerId }
|
||||||
|
|
||||||
if (conversationIndex == null) {
|
if (conversationIndex == null) { // диалога нет в списке
|
||||||
loadConversationsByIdUseCase(peerIds = listOf(message.peerId))
|
// pizdets
|
||||||
.listenValue(viewModelScope) { state ->
|
// TODO: 04/07/2024, Danil Nikolaev: load conversation and store info
|
||||||
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 {
|
} else {
|
||||||
val conversation = newConversations[conversationIndex]
|
val conversation = newConversations[conversationIndex]
|
||||||
var newConversation = conversation.copy(
|
var newConversation = conversation.copy(
|
||||||
@@ -476,12 +420,7 @@ class ConversationsViewModelImpl(
|
|||||||
|
|
||||||
screenState.setValue { old ->
|
screenState.setValue { old ->
|
||||||
old.copy(
|
old.copy(
|
||||||
conversations = newConversations.map {
|
conversations = newConversations.map { it.asPresentation(resources) }
|
||||||
it.asPresentation(
|
|
||||||
resources = resources,
|
|
||||||
useContactName = useContactNames
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -505,12 +444,7 @@ class ConversationsViewModelImpl(
|
|||||||
|
|
||||||
screenState.setValue { old ->
|
screenState.setValue { old ->
|
||||||
old.copy(
|
old.copy(
|
||||||
conversations = newConversations.map {
|
conversations = newConversations.map { it.asPresentation(resources) }
|
||||||
it.asPresentation(
|
|
||||||
resources = resources,
|
|
||||||
useContactName = useContactNames
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -520,11 +454,8 @@ class ConversationsViewModelImpl(
|
|||||||
val newConversations = conversations.value.toMutableList()
|
val newConversations = conversations.value.toMutableList()
|
||||||
|
|
||||||
val conversationIndex =
|
val conversationIndex =
|
||||||
newConversations.indexOfFirstOrNull { it.id == event.peerId }
|
newConversations.indexOfFirstOrNull { it.id == event.peerId } ?: return
|
||||||
|
|
||||||
if (conversationIndex == null) { // диалога нет в списке
|
|
||||||
// pizdets
|
|
||||||
} else {
|
|
||||||
newConversations[conversationIndex] =
|
newConversations[conversationIndex] =
|
||||||
newConversations[conversationIndex].copy(
|
newConversations[conversationIndex].copy(
|
||||||
inRead = event.messageId,
|
inRead = event.messageId,
|
||||||
@@ -535,26 +466,17 @@ class ConversationsViewModelImpl(
|
|||||||
|
|
||||||
screenState.setValue { old ->
|
screenState.setValue { old ->
|
||||||
old.copy(
|
old.copy(
|
||||||
conversations = newConversations.map {
|
conversations = newConversations.map { it.asPresentation(resources) }
|
||||||
it.asPresentation(
|
|
||||||
resources = resources,
|
|
||||||
useContactName = useContactNames
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleReadOutgoingMessage(event: LongPollEvent.VkMessageReadOutgoingEvent) {
|
private fun handleReadOutgoingMessage(event: LongPollEvent.VkMessageReadOutgoingEvent) {
|
||||||
val newConversations = conversations.value.toMutableList()
|
val newConversations = conversations.value.toMutableList()
|
||||||
|
|
||||||
val conversationIndex =
|
val conversationIndex =
|
||||||
newConversations.indexOfFirstOrNull { it.id == event.peerId }
|
newConversations.indexOfFirstOrNull { it.id == event.peerId } ?: return
|
||||||
|
|
||||||
if (conversationIndex == null) { // диалога нет в списке
|
|
||||||
// pizdets
|
|
||||||
} else {
|
|
||||||
newConversations[conversationIndex] =
|
newConversations[conversationIndex] =
|
||||||
newConversations[conversationIndex].copy(
|
newConversations[conversationIndex].copy(
|
||||||
outRead = event.messageId,
|
outRead = event.messageId,
|
||||||
@@ -564,15 +486,9 @@ class ConversationsViewModelImpl(
|
|||||||
conversations.update { newConversations }
|
conversations.update { newConversations }
|
||||||
screenState.setValue { old ->
|
screenState.setValue { old ->
|
||||||
old.copy(
|
old.copy(
|
||||||
conversations = newConversations.map {
|
conversations = newConversations.map { it.asPresentation(resources) }
|
||||||
it.asPresentation(
|
|
||||||
resources = resources,
|
|
||||||
useContactName = useContactNames
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handlePinStateChanged(event: LongPollEvent.VkConversationPinStateChangedEvent) {
|
private fun handlePinStateChanged(event: LongPollEvent.VkConversationPinStateChangedEvent) {
|
||||||
@@ -580,11 +496,8 @@ class ConversationsViewModelImpl(
|
|||||||
val newConversations = conversations.value.toMutableList()
|
val newConversations = conversations.value.toMutableList()
|
||||||
|
|
||||||
val conversationIndex =
|
val conversationIndex =
|
||||||
newConversations.indexOfFirstOrNull { it.id == event.peerId }
|
newConversations.indexOfFirstOrNull { it.id == event.peerId } ?: return
|
||||||
|
|
||||||
if (conversationIndex == null) { // диалога нет в списке
|
|
||||||
// pizdets
|
|
||||||
} else {
|
|
||||||
val pin = event.majorId > 0
|
val pin = event.majorId > 0
|
||||||
|
|
||||||
val conversation = newConversations[conversationIndex].copy(majorId = event.majorId)
|
val conversation = newConversations[conversationIndex].copy(majorId = event.majorId)
|
||||||
@@ -610,13 +523,7 @@ class ConversationsViewModelImpl(
|
|||||||
conversations.update { newConversations }
|
conversations.update { newConversations }
|
||||||
|
|
||||||
screenState.setValue { old ->
|
screenState.setValue { old ->
|
||||||
old.copy(conversations = newConversations.map {
|
old.copy(conversations = newConversations.map { it.asPresentation(resources) })
|
||||||
it.asPresentation(
|
|
||||||
resources = resources,
|
|
||||||
useContactName = useContactNames
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -636,11 +543,8 @@ class ConversationsViewModelImpl(
|
|||||||
|
|
||||||
val newConversations = conversations.value.toMutableList()
|
val newConversations = conversations.value.toMutableList()
|
||||||
val conversationAndIndex =
|
val conversationAndIndex =
|
||||||
newConversations.findWithIndex { it.id == peerId }
|
newConversations.findWithIndex { it.id == peerId } ?: return
|
||||||
|
|
||||||
if (conversationAndIndex == null) { // диалога нет в списке
|
|
||||||
// pizdets
|
|
||||||
} else {
|
|
||||||
newConversations[conversationAndIndex.first] =
|
newConversations[conversationAndIndex.first] =
|
||||||
conversationAndIndex.second.copy(
|
conversationAndIndex.second.copy(
|
||||||
interactionType = interactionType.value,
|
interactionType = interactionType.value,
|
||||||
@@ -651,12 +555,7 @@ class ConversationsViewModelImpl(
|
|||||||
|
|
||||||
screenState.setValue { old ->
|
screenState.setValue { old ->
|
||||||
old.copy(
|
old.copy(
|
||||||
conversations = newConversations.map {
|
conversations = newConversations.map { it.asPresentation(resources) }
|
||||||
it.asPresentation(
|
|
||||||
resources = resources,
|
|
||||||
useContactName = useContactNames
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -684,7 +583,6 @@ class ConversationsViewModelImpl(
|
|||||||
stopInteraction(peerId, newInteractionJob)
|
stopInteraction(peerId, newInteractionJob)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun stopInteraction(peerId: Int, interactionJob: InteractionJob) {
|
private fun stopInteraction(peerId: Int, interactionJob: InteractionJob) {
|
||||||
interactionsTimers[peerId] ?: return
|
interactionsTimers[peerId] ?: return
|
||||||
@@ -702,12 +600,7 @@ class ConversationsViewModelImpl(
|
|||||||
conversations.update { newConversations }
|
conversations.update { newConversations }
|
||||||
screenState.setValue { old ->
|
screenState.setValue { old ->
|
||||||
old.copy(
|
old.copy(
|
||||||
conversations = newConversations.map {
|
conversations = newConversations.map { it.asPresentation(resources) }
|
||||||
it.asPresentation(
|
|
||||||
resources = resources,
|
|
||||||
useContactName = useContactNames
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -736,12 +629,7 @@ class ConversationsViewModelImpl(
|
|||||||
conversations.update { newConversations }
|
conversations.update { newConversations }
|
||||||
screenState.setValue { old ->
|
screenState.setValue { old ->
|
||||||
old.copy(
|
old.copy(
|
||||||
conversations = newConversations.map {
|
conversations = newConversations.map { it.asPresentation(resources) }
|
||||||
it.asPresentation(
|
|
||||||
resources = resources,
|
|
||||||
useContactName = useContactNames
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package dev.meloda.fast.ui.model.api
|
package dev.meloda.fast.conversations.model
|
||||||
|
|
||||||
enum class ActionState {
|
enum class ActionState {
|
||||||
PHANTOM, CALL_IN_PROGRESS, NONE;
|
PHANTOM, CALL_IN_PROGRESS, NONE;
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
package dev.meloda.fast.conversations.model
|
||||||
|
|
||||||
|
import dev.meloda.fast.common.model.UiImage
|
||||||
|
import dev.meloda.fast.common.model.UiText
|
||||||
|
import dev.meloda.fast.ui.R as UiR
|
||||||
|
|
||||||
|
sealed class ConversationOption(
|
||||||
|
val title: UiText,
|
||||||
|
val icon: UiImage
|
||||||
|
) {
|
||||||
|
|
||||||
|
data object MarkAsRead : ConversationOption(
|
||||||
|
title = UiText.Resource(UiR.string.action_mark_as_read),
|
||||||
|
icon = UiImage.Resource(UiR.drawable.round_done_all_24)
|
||||||
|
)
|
||||||
|
|
||||||
|
data object Pin : ConversationOption(
|
||||||
|
title = UiText.Resource(UiR.string.action_pin),
|
||||||
|
icon = UiImage.Resource(UiR.drawable.pin_outline_24)
|
||||||
|
)
|
||||||
|
|
||||||
|
data object Unpin : ConversationOption(
|
||||||
|
title = UiText.Resource(UiR.string.action_unpin),
|
||||||
|
icon = UiImage.Resource(UiR.drawable.pin_off_outline_24)
|
||||||
|
)
|
||||||
|
|
||||||
|
data object Delete : ConversationOption(
|
||||||
|
title = UiText.Resource(UiR.string.action_delete),
|
||||||
|
icon = UiImage.Resource(UiR.drawable.round_delete_outline_24)
|
||||||
|
)
|
||||||
|
}
|
||||||
-2
@@ -1,8 +1,6 @@
|
|||||||
package dev.meloda.fast.conversations.model
|
package dev.meloda.fast.conversations.model
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import dev.meloda.fast.ui.model.api.ConversationsShowOptions
|
|
||||||
import dev.meloda.fast.ui.model.api.UiConversation
|
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class ConversationsScreenState(
|
data class ConversationsScreenState(
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package dev.meloda.fast.ui.model.api
|
package dev.meloda.fast.conversations.model
|
||||||
|
|
||||||
data class ConversationsShowOptions(
|
data class ConversationsShowOptions(
|
||||||
val showDeleteDialog: Int?,
|
val showDeleteDialog: Int?,
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package dev.meloda.fast.ui.model.api
|
package dev.meloda.fast.conversations.model
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
+3
-2
@@ -8,6 +8,7 @@ import dev.meloda.fast.conversations.ConversationsViewModelImpl
|
|||||||
import dev.meloda.fast.conversations.presentation.ConversationsRoute
|
import dev.meloda.fast.conversations.presentation.ConversationsRoute
|
||||||
import dev.meloda.fast.model.BaseError
|
import dev.meloda.fast.model.BaseError
|
||||||
import dev.meloda.fast.ui.extensions.sharedViewModel
|
import dev.meloda.fast.ui.extensions.sharedViewModel
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -17,18 +18,18 @@ fun NavGraphBuilder.conversationsScreen(
|
|||||||
onError: (BaseError) -> Unit,
|
onError: (BaseError) -> Unit,
|
||||||
onConversationItemClicked: (id: Int) -> Unit,
|
onConversationItemClicked: (id: Int) -> Unit,
|
||||||
onPhotoClicked: (url: String) -> Unit,
|
onPhotoClicked: (url: String) -> Unit,
|
||||||
onCreateChatClicked: () -> Unit,
|
scrollToTopFlow: Flow<Int>,
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
) {
|
) {
|
||||||
composable<Conversations> {
|
composable<Conversations> {
|
||||||
val viewModel: ConversationsViewModel =
|
val viewModel: ConversationsViewModel =
|
||||||
it.sharedViewModel<ConversationsViewModelImpl>(navController = navController)
|
it.sharedViewModel<ConversationsViewModelImpl>(navController = navController)
|
||||||
|
viewModel.setScrollToTopFlow(scrollToTopFlow)
|
||||||
|
|
||||||
ConversationsRoute(
|
ConversationsRoute(
|
||||||
onError = onError,
|
onError = onError,
|
||||||
onConversationItemClicked = onConversationItemClicked,
|
onConversationItemClicked = onConversationItemClicked,
|
||||||
onConversationPhotoClicked = onPhotoClicked,
|
onConversationPhotoClicked = onPhotoClicked,
|
||||||
onCreateChatButtonClicked = onCreateChatClicked,
|
|
||||||
viewModel = viewModel
|
viewModel = viewModel
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -48,11 +48,11 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
|
import dev.meloda.fast.conversations.model.ConversationOption
|
||||||
|
import dev.meloda.fast.conversations.model.UiConversation
|
||||||
import dev.meloda.fast.ui.basic.ContentAlpha
|
import dev.meloda.fast.ui.basic.ContentAlpha
|
||||||
import dev.meloda.fast.ui.basic.LocalContentAlpha
|
import dev.meloda.fast.ui.basic.LocalContentAlpha
|
||||||
import dev.meloda.fast.ui.components.DotsFlashing
|
import dev.meloda.fast.ui.components.DotsFlashing
|
||||||
import dev.meloda.fast.ui.model.api.ConversationOption
|
|
||||||
import dev.meloda.fast.ui.model.api.UiConversation
|
|
||||||
import dev.meloda.fast.ui.util.getImage
|
import dev.meloda.fast.ui.util.getImage
|
||||||
import dev.meloda.fast.ui.util.getResourcePainter
|
import dev.meloda.fast.ui.util.getResourcePainter
|
||||||
import dev.meloda.fast.ui.util.getString
|
import dev.meloda.fast.ui.util.getString
|
||||||
@@ -256,7 +256,7 @@ fun ConversationItem(
|
|||||||
Row {
|
Row {
|
||||||
if (conversation.interactionText != null) {
|
if (conversation.interactionText != null) {
|
||||||
Text(
|
Text(
|
||||||
text = conversation.interactionText.orEmpty(),
|
text = conversation.interactionText,
|
||||||
color = MaterialTheme.colorScheme.primary
|
color = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
|||||||
+9
-5
@@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.PaddingValues
|
|||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
@@ -22,10 +23,10 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import dev.meloda.fast.conversations.model.ConversationOption
|
||||||
import dev.meloda.fast.conversations.model.ConversationsScreenState
|
import dev.meloda.fast.conversations.model.ConversationsScreenState
|
||||||
|
import dev.meloda.fast.conversations.model.UiConversation
|
||||||
import dev.meloda.fast.data.UserConfig
|
import dev.meloda.fast.data.UserConfig
|
||||||
import dev.meloda.fast.ui.model.api.ConversationOption
|
|
||||||
import dev.meloda.fast.ui.model.api.UiConversation
|
|
||||||
import dev.meloda.fast.ui.theme.LocalBottomPadding
|
import dev.meloda.fast.ui.theme.LocalBottomPadding
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -82,7 +83,8 @@ fun ConversationsList(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.animateItem(fadeInSpec = null, fadeOutSpec = null),
|
.animateItem(fadeInSpec = null, fadeOutSpec = null)
|
||||||
|
.navigationBarsPadding(),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
if (screenState.isPaginating) {
|
if (screenState.isPaginating) {
|
||||||
@@ -105,9 +107,11 @@ fun ConversationsList(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(bottomPadding))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+75
-37
@@ -2,6 +2,7 @@ package dev.meloda.fast.conversations.presentation
|
|||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
|
import androidx.compose.animation.core.Animatable
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
@@ -47,10 +48,13 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.rotate
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -59,26 +63,30 @@ import androidx.compose.ui.unit.DpOffset
|
|||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.view.HapticFeedbackConstantsCompat
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import dev.chrisbanes.haze.hazeEffect
|
import coil.imageLoader
|
||||||
import dev.chrisbanes.haze.hazeSource
|
import coil.request.ImageRequest
|
||||||
|
import dev.chrisbanes.haze.haze
|
||||||
|
import dev.chrisbanes.haze.hazeChild
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
import dev.chrisbanes.haze.materials.HazeMaterials
|
import dev.chrisbanes.haze.materials.HazeMaterials
|
||||||
import dev.meloda.fast.conversations.ConversationsViewModel
|
import dev.meloda.fast.conversations.ConversationsViewModel
|
||||||
|
import dev.meloda.fast.conversations.model.ConversationOption
|
||||||
import dev.meloda.fast.conversations.model.ConversationsScreenState
|
import dev.meloda.fast.conversations.model.ConversationsScreenState
|
||||||
|
import dev.meloda.fast.conversations.model.UiConversation
|
||||||
|
import dev.meloda.fast.datastore.AppSettings
|
||||||
import dev.meloda.fast.model.BaseError
|
import dev.meloda.fast.model.BaseError
|
||||||
import dev.meloda.fast.ui.components.ErrorView
|
import dev.meloda.fast.ui.components.ErrorView
|
||||||
import dev.meloda.fast.ui.components.FullScreenLoader
|
import dev.meloda.fast.ui.components.FullScreenLoader
|
||||||
import dev.meloda.fast.ui.components.MaterialDialog
|
import dev.meloda.fast.ui.components.MaterialDialog
|
||||||
import dev.meloda.fast.ui.components.NoItemsView
|
|
||||||
import dev.meloda.fast.ui.model.api.ConversationOption
|
|
||||||
import dev.meloda.fast.ui.model.api.UiConversation
|
|
||||||
import dev.meloda.fast.ui.theme.LocalBottomPadding
|
import dev.meloda.fast.ui.theme.LocalBottomPadding
|
||||||
import dev.meloda.fast.ui.theme.LocalHazeState
|
import dev.meloda.fast.ui.theme.LocalHazeState
|
||||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||||
import dev.meloda.fast.ui.util.isScrollingUp
|
import dev.meloda.fast.ui.util.isScrollingUp
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import dev.meloda.fast.ui.R as UiR
|
import dev.meloda.fast.ui.R as UiR
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -86,12 +94,25 @@ fun ConversationsRoute(
|
|||||||
onError: (BaseError) -> Unit,
|
onError: (BaseError) -> Unit,
|
||||||
onConversationItemClicked: (conversationId: Int) -> Unit,
|
onConversationItemClicked: (conversationId: Int) -> Unit,
|
||||||
onConversationPhotoClicked: (url: String) -> Unit,
|
onConversationPhotoClicked: (url: String) -> Unit,
|
||||||
onCreateChatButtonClicked: () -> Unit,
|
|
||||||
viewModel: ConversationsViewModel
|
viewModel: ConversationsViewModel
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||||
val baseError by viewModel.baseError.collectAsStateWithLifecycle()
|
val baseError by viewModel.baseError.collectAsStateWithLifecycle()
|
||||||
val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle()
|
val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle()
|
||||||
|
val isNeedToScrollToTop by viewModel.scrollToTop.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
val imagesToPreload by viewModel.imagesToPreload.collectAsStateWithLifecycle()
|
||||||
|
LaunchedEffect(imagesToPreload) {
|
||||||
|
imagesToPreload.forEach { url ->
|
||||||
|
context.imageLoader.enqueue(
|
||||||
|
ImageRequest.Builder(context)
|
||||||
|
.data(url)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ConversationsScreen(
|
ConversationsScreen(
|
||||||
screenState = screenState,
|
screenState = screenState,
|
||||||
@@ -108,9 +129,10 @@ fun ConversationsRoute(
|
|||||||
onRefreshDropdownItemClicked = viewModel::onRefresh,
|
onRefreshDropdownItemClicked = viewModel::onRefresh,
|
||||||
onRefresh = viewModel::onRefresh,
|
onRefresh = viewModel::onRefresh,
|
||||||
onConversationPhotoClicked = onConversationPhotoClicked,
|
onConversationPhotoClicked = onConversationPhotoClicked,
|
||||||
onCreateChatButtonClicked = onCreateChatButtonClicked,
|
|
||||||
setScrollIndex = viewModel::setScrollIndex,
|
setScrollIndex = viewModel::setScrollIndex,
|
||||||
setScrollOffset = viewModel::setScrollOffset
|
setScrollOffset = viewModel::setScrollOffset,
|
||||||
|
isNeedToScrollToTop = isNeedToScrollToTop,
|
||||||
|
onScrolledToTop = viewModel::onScrolledToTop
|
||||||
)
|
)
|
||||||
|
|
||||||
HandleDialogs(
|
HandleDialogs(
|
||||||
@@ -128,7 +150,7 @@ fun ConversationsScreen(
|
|||||||
screenState: ConversationsScreenState = ConversationsScreenState.EMPTY,
|
screenState: ConversationsScreenState = ConversationsScreenState.EMPTY,
|
||||||
baseError: BaseError? = null,
|
baseError: BaseError? = null,
|
||||||
canPaginate: Boolean = false,
|
canPaginate: Boolean = false,
|
||||||
onSessionExpiredLogOutButtonClicked: () -> Unit = {},
|
onSessionExpiredLogOutButtonClicked: () -> Unit,
|
||||||
onConversationItemClicked: (conversationId: Int) -> Unit = {},
|
onConversationItemClicked: (conversationId: Int) -> Unit = {},
|
||||||
onConversationItemLongClicked: (conversation: UiConversation) -> Unit = {},
|
onConversationItemLongClicked: (conversation: UiConversation) -> Unit = {},
|
||||||
onOptionClicked: (UiConversation, ConversationOption) -> Unit = { _, _ -> },
|
onOptionClicked: (UiConversation, ConversationOption) -> Unit = { _, _ -> },
|
||||||
@@ -136,9 +158,10 @@ fun ConversationsScreen(
|
|||||||
onRefreshDropdownItemClicked: () -> Unit = {},
|
onRefreshDropdownItemClicked: () -> Unit = {},
|
||||||
onRefresh: () -> Unit = {},
|
onRefresh: () -> Unit = {},
|
||||||
onConversationPhotoClicked: (url: String) -> Unit = {},
|
onConversationPhotoClicked: (url: String) -> Unit = {},
|
||||||
onCreateChatButtonClicked: () -> Unit = {},
|
|
||||||
setScrollIndex: (Int) -> Unit = {},
|
setScrollIndex: (Int) -> Unit = {},
|
||||||
setScrollOffset: (Int) -> Unit = {}
|
setScrollOffset: (Int) -> Unit = {},
|
||||||
|
isNeedToScrollToTop: Boolean = false,
|
||||||
|
onScrolledToTop: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
val currentTheme = LocalThemeConfig.current
|
val currentTheme = LocalThemeConfig.current
|
||||||
@@ -152,6 +175,14 @@ fun ConversationsScreen(
|
|||||||
initialFirstVisibleItemScrollOffset = screenState.scrollOffset
|
initialFirstVisibleItemScrollOffset = screenState.scrollOffset
|
||||||
)
|
)
|
||||||
|
|
||||||
|
LaunchedEffect(isNeedToScrollToTop) {
|
||||||
|
if (isNeedToScrollToTop) {
|
||||||
|
listState.scrollToItem(0)
|
||||||
|
onScrolledToTop()
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(listState) {
|
LaunchedEffect(listState) {
|
||||||
snapshotFlow { listState.firstVisibleItemIndex }
|
snapshotFlow { listState.firstVisibleItemIndex }
|
||||||
.debounce(500L)
|
.debounce(500L)
|
||||||
@@ -260,7 +291,7 @@ fun ConversationsScreen(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.then(
|
.then(
|
||||||
if (currentTheme.enableBlur) {
|
if (currentTheme.enableBlur) {
|
||||||
Modifier.hazeEffect(
|
Modifier.hazeChild(
|
||||||
state = hazeState,
|
state = hazeState,
|
||||||
style = HazeMaterials.thick()
|
style = HazeMaterials.thick()
|
||||||
)
|
)
|
||||||
@@ -281,13 +312,37 @@ fun ConversationsScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val rotation = remember { Animatable(0f) }
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = listState.isScrollingUp(),
|
visible = listState.isScrollingUp(),
|
||||||
enter = slideIn { IntOffset(0, 600) } + fadeIn(tween(200)),
|
enter = slideIn { IntOffset(0, 600) } + fadeIn(tween(200)),
|
||||||
exit = slideOut { IntOffset(0, 600) } + fadeOut(tween(200))
|
exit = slideOut { IntOffset(0, 600) } + fadeOut(tween(200))
|
||||||
) {
|
) {
|
||||||
FloatingActionButton(onClick = onCreateChatButtonClicked) {
|
FloatingActionButton(
|
||||||
|
onClick = {
|
||||||
|
if (AppSettings.General.enableHaptic) {
|
||||||
|
view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT)
|
||||||
|
}
|
||||||
|
scope.launch {
|
||||||
|
for (i in 20 downTo 0 step 4) {
|
||||||
|
rotation.animateTo(
|
||||||
|
targetValue = i.toFloat(),
|
||||||
|
animationSpec = tween(50)
|
||||||
|
)
|
||||||
|
if (i > 0) {
|
||||||
|
rotation.animateTo(
|
||||||
|
targetValue = -i.toFloat(),
|
||||||
|
animationSpec = tween(50)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.rotate(rotation.value)
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = UiR.drawable.ic_baseline_create_24),
|
painter = painterResource(id = UiR.drawable.ic_baseline_create_24),
|
||||||
contentDescription = "Add chat button"
|
contentDescription = "Add chat button"
|
||||||
@@ -300,26 +355,14 @@ fun ConversationsScreen(
|
|||||||
}
|
}
|
||||||
) { padding ->
|
) { padding ->
|
||||||
when {
|
when {
|
||||||
baseError != null -> {
|
baseError is BaseError.SessionExpired -> {
|
||||||
when (baseError) {
|
|
||||||
is BaseError.SessionExpired -> {
|
|
||||||
ErrorView(
|
ErrorView(
|
||||||
text = stringResource(UiR.string.session_expired),
|
text = "Session expired",
|
||||||
buttonText = stringResource(UiR.string.action_log_out),
|
buttonText = "Log out",
|
||||||
onButtonClick = onSessionExpiredLogOutButtonClicked
|
onButtonClick = onSessionExpiredLogOutButtonClicked
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is BaseError.SimpleError -> {
|
|
||||||
ErrorView(
|
|
||||||
text = baseError.message,
|
|
||||||
buttonText = stringResource(UiR.string.try_again),
|
|
||||||
onButtonClick = onRefresh
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
screenState.isLoading && screenState.conversations.isEmpty() -> FullScreenLoader()
|
screenState.isLoading && screenState.conversations.isEmpty() -> FullScreenLoader()
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
@@ -351,7 +394,7 @@ fun ConversationsScreen(
|
|||||||
state = listState,
|
state = listState,
|
||||||
maxLines = maxLines,
|
maxLines = maxLines,
|
||||||
modifier = if (currentTheme.enableBlur) {
|
modifier = if (currentTheme.enableBlur) {
|
||||||
Modifier.hazeSource(state = hazeState)
|
Modifier.haze(state = hazeState)
|
||||||
} else {
|
} else {
|
||||||
Modifier
|
Modifier
|
||||||
}.fillMaxSize(),
|
}.fillMaxSize(),
|
||||||
@@ -359,13 +402,6 @@ fun ConversationsScreen(
|
|||||||
padding = padding,
|
padding = padding,
|
||||||
onPhotoClicked = onConversationPhotoClicked
|
onPhotoClicked = onConversationPhotoClicked
|
||||||
)
|
)
|
||||||
|
|
||||||
if (screenState.conversations.isEmpty()) {
|
|
||||||
NoItemsView(
|
|
||||||
buttonText = stringResource(UiR.string.action_refresh),
|
|
||||||
onButtonClick = onRefresh
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -390,7 +426,9 @@ fun HandleDialogs(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
showOptions.showPinDialog?.let { conversation ->
|
if (showOptions.showPinDialog != null) {
|
||||||
|
val conversation = showOptions.showPinDialog
|
||||||
|
|
||||||
MaterialDialog(
|
MaterialDialog(
|
||||||
onDismissRequest = viewModel::onPinDialogDismissed,
|
onDismissRequest = viewModel::onPinDialogDismissed,
|
||||||
title = stringResource(
|
title = stringResource(
|
||||||
|
|||||||
+3
-3
@@ -14,6 +14,8 @@ import dev.meloda.fast.common.model.UiImage
|
|||||||
import dev.meloda.fast.common.model.UiText
|
import dev.meloda.fast.common.model.UiText
|
||||||
import dev.meloda.fast.common.model.parseString
|
import dev.meloda.fast.common.model.parseString
|
||||||
import dev.meloda.fast.common.util.TimeUtils
|
import dev.meloda.fast.common.util.TimeUtils
|
||||||
|
import dev.meloda.fast.conversations.model.ActionState
|
||||||
|
import dev.meloda.fast.conversations.model.UiConversation
|
||||||
import dev.meloda.fast.data.UserConfig
|
import dev.meloda.fast.data.UserConfig
|
||||||
import dev.meloda.fast.data.VkMemoryCache
|
import dev.meloda.fast.data.VkMemoryCache
|
||||||
import dev.meloda.fast.model.InteractionType
|
import dev.meloda.fast.model.InteractionType
|
||||||
@@ -22,8 +24,6 @@ import dev.meloda.fast.model.api.data.AttachmentType
|
|||||||
import dev.meloda.fast.model.api.domain.VkAttachment
|
import dev.meloda.fast.model.api.domain.VkAttachment
|
||||||
import dev.meloda.fast.model.api.domain.VkConversation
|
import dev.meloda.fast.model.api.domain.VkConversation
|
||||||
import dev.meloda.fast.model.api.domain.VkMessage
|
import dev.meloda.fast.model.api.domain.VkMessage
|
||||||
import dev.meloda.fast.ui.model.api.ActionState
|
|
||||||
import dev.meloda.fast.ui.model.api.UiConversation
|
|
||||||
import dev.meloda.fast.ui.util.ImmutableList
|
import dev.meloda.fast.ui.util.ImmutableList
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@@ -33,7 +33,7 @@ import dev.meloda.fast.ui.R as UiR
|
|||||||
|
|
||||||
fun VkConversation.asPresentation(
|
fun VkConversation.asPresentation(
|
||||||
resources: Resources,
|
resources: Resources,
|
||||||
useContactName: Boolean
|
useContactName: Boolean = false
|
||||||
): UiConversation = UiConversation(
|
): UiConversation = UiConversation(
|
||||||
id = id,
|
id = id,
|
||||||
lastMessageId = lastMessageId,
|
lastMessageId = lastMessageId,
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
/build
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
plugins {
|
|
||||||
alias(libs.plugins.fast.android.feature)
|
|
||||||
alias(libs.plugins.fast.android.library.compose)
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace = "dev.meloda.fast.createchat"
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation(projects.core.common)
|
|
||||||
implementation(projects.core.domain)
|
|
||||||
implementation(projects.core.model)
|
|
||||||
implementation(projects.core.ui)
|
|
||||||
|
|
||||||
implementation(libs.bundles.nanokt)
|
|
||||||
|
|
||||||
implementation(libs.koin.android)
|
|
||||||
implementation(libs.koin.androidx.compose)
|
|
||||||
|
|
||||||
implementation(platform(libs.compose.bom))
|
|
||||||
implementation(libs.bundles.compose)
|
|
||||||
|
|
||||||
implementation(libs.coil.compose)
|
|
||||||
|
|
||||||
implementation(libs.haze)
|
|
||||||
implementation(libs.haze.materials)
|
|
||||||
|
|
||||||
implementation(libs.eithernet)
|
|
||||||
|
|
||||||
implementation(libs.androidx.navigation.compose)
|
|
||||||
|
|
||||||
implementation(libs.kotlin.serialization)
|
|
||||||
}
|
|
||||||
-243
@@ -1,243 +0,0 @@
|
|||||||
package dev.meloda.fast.conversations
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import coil.ImageLoader
|
|
||||||
import coil.request.ImageRequest
|
|
||||||
import dev.meloda.fast.common.extensions.listenValue
|
|
||||||
import dev.meloda.fast.common.extensions.setValue
|
|
||||||
import dev.meloda.fast.conversations.model.CreateChatScreenState
|
|
||||||
import dev.meloda.fast.data.State
|
|
||||||
import dev.meloda.fast.data.UserConfig
|
|
||||||
import dev.meloda.fast.data.processState
|
|
||||||
import dev.meloda.fast.datastore.UserSettings
|
|
||||||
import dev.meloda.fast.domain.FriendsUseCase
|
|
||||||
import dev.meloda.fast.domain.GetLocalUserByIdUseCase
|
|
||||||
import dev.meloda.fast.domain.MessagesUseCase
|
|
||||||
import dev.meloda.fast.domain.util.asPresentation
|
|
||||||
import dev.meloda.fast.model.BaseError
|
|
||||||
import dev.meloda.fast.network.VkErrorCode
|
|
||||||
import dev.meloda.fast.ui.model.api.UiFriend
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
interface CreateChatViewModel {
|
|
||||||
|
|
||||||
val screenState: StateFlow<CreateChatScreenState>
|
|
||||||
val baseError: StateFlow<BaseError?>
|
|
||||||
val currentOffset: StateFlow<Int>
|
|
||||||
val canPaginate: StateFlow<Boolean>
|
|
||||||
|
|
||||||
val isChatCreated: StateFlow<Int?>
|
|
||||||
|
|
||||||
fun onPaginationConditionsMet()
|
|
||||||
fun onRefresh()
|
|
||||||
fun onErrorConsumed()
|
|
||||||
|
|
||||||
fun toggleFriendSelection(userId: Int)
|
|
||||||
|
|
||||||
fun onTitleTextInputChanged(newTitle: String)
|
|
||||||
|
|
||||||
fun onCreateChatButtonClicked()
|
|
||||||
|
|
||||||
fun onNavigatedBack()
|
|
||||||
}
|
|
||||||
|
|
||||||
class CreateChatViewModelImpl(
|
|
||||||
private val friendsUseCase: FriendsUseCase,
|
|
||||||
private val messagesUseCase: MessagesUseCase,
|
|
||||||
private val imageLoader: ImageLoader,
|
|
||||||
private val applicationContext: Context,
|
|
||||||
private val getLocalUserByIdUseCase: GetLocalUserByIdUseCase,
|
|
||||||
private val userSettings: UserSettings
|
|
||||||
) : CreateChatViewModel, ViewModel() {
|
|
||||||
|
|
||||||
override val screenState = MutableStateFlow(CreateChatScreenState.EMPTY)
|
|
||||||
override val baseError = MutableStateFlow<BaseError?>(null)
|
|
||||||
override val currentOffset = MutableStateFlow(0)
|
|
||||||
override val canPaginate = MutableStateFlow(false)
|
|
||||||
|
|
||||||
override val isChatCreated = MutableStateFlow<Int?>(null)
|
|
||||||
|
|
||||||
private val useContactNames: Boolean = userSettings.useContactNames.value
|
|
||||||
|
|
||||||
init {
|
|
||||||
loadFriends()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPaginationConditionsMet() {
|
|
||||||
currentOffset.update { screenState.value.friends.size }
|
|
||||||
loadFriends()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRefresh() {
|
|
||||||
onErrorConsumed()
|
|
||||||
loadFriends(offset = 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onErrorConsumed() {
|
|
||||||
baseError.setValue { null }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toggleFriendSelection(userId: Int) {
|
|
||||||
val newSelectionList = screenState.value.selectedFriendsIds.toMutableList()
|
|
||||||
|
|
||||||
if (newSelectionList.contains(userId)) {
|
|
||||||
newSelectionList.remove(userId)
|
|
||||||
} else {
|
|
||||||
newSelectionList.add(userId)
|
|
||||||
}
|
|
||||||
|
|
||||||
screenState.setValue { old ->
|
|
||||||
old.copy(selectedFriendsIds = newSelectionList)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTitleTextInputChanged(newTitle: String) {
|
|
||||||
screenState.setValue { old -> old.copy(chatTitle = newTitle) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateChatButtonClicked() {
|
|
||||||
createChat()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNavigatedBack() {
|
|
||||||
viewModelScope.launch(Dispatchers.Main) {
|
|
||||||
isChatCreated.emit(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadFriends(
|
|
||||||
offset: Int = currentOffset.value
|
|
||||||
) {
|
|
||||||
friendsUseCase.getFriends(count = LOAD_COUNT, offset = offset)
|
|
||||||
.listenValue(viewModelScope) { state ->
|
|
||||||
state.processState(
|
|
||||||
error = ::handleError,
|
|
||||||
success = { response ->
|
|
||||||
val itemsCountSufficient = response.size == LOAD_COUNT
|
|
||||||
canPaginate.setValue { itemsCountSufficient }
|
|
||||||
|
|
||||||
val paginationExhausted = !itemsCountSufficient &&
|
|
||||||
screenState.value.friends.isNotEmpty()
|
|
||||||
|
|
||||||
val imagesToPreload =
|
|
||||||
response.mapNotNull { it.photo100.takeIf { !it.isNullOrEmpty() } }
|
|
||||||
|
|
||||||
imagesToPreload.forEach { url ->
|
|
||||||
imageLoader.enqueue(
|
|
||||||
ImageRequest.Builder(applicationContext)
|
|
||||||
.data(url)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
friendsUseCase.storeUsers(response)
|
|
||||||
|
|
||||||
val loadedFriends = response.map {
|
|
||||||
it.asPresentation(useContactNames)
|
|
||||||
}
|
|
||||||
|
|
||||||
val newState = screenState.value.copy(
|
|
||||||
isPaginationExhausted = paginationExhausted
|
|
||||||
)
|
|
||||||
if (offset == 0) {
|
|
||||||
screenState.setValue {
|
|
||||||
newState.copy(friends = loadedFriends)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
screenState.setValue {
|
|
||||||
newState.copy(
|
|
||||||
friends = newState.friends.plus(loadedFriends)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
screenState.setValue { old ->
|
|
||||||
old.copy(
|
|
||||||
isLoading = offset == 0 && state.isLoading(),
|
|
||||||
isPaginating = offset > 0 && state.isLoading()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createChat() {
|
|
||||||
viewModelScope.launch {
|
|
||||||
val title = screenState.value.chatTitle.takeUnless(String::isBlank)
|
|
||||||
|
|
||||||
val accountAsFriend =
|
|
||||||
getLocalUserByIdUseCase.proceed(UserConfig.userId)?.asPresentation(useContactNames)
|
|
||||||
|
|
||||||
val accountList = accountAsFriend?.let(::listOf) ?: emptyList()
|
|
||||||
|
|
||||||
val selectedFriends = screenState.value.selectedFriendsIds
|
|
||||||
.takeIf { it.isNotEmpty() }
|
|
||||||
?.mapNotNull { userId -> screenState.value.friends.find { it.userId == userId } }
|
|
||||||
|
|
||||||
messagesUseCase.createChat(
|
|
||||||
userIds = selectedFriends?.map { it.userId },
|
|
||||||
title = title
|
|
||||||
?: (accountList + selectedFriends.orEmpty()).joinToString(transform = UiFriend::firstName)
|
|
||||||
).listenValue(viewModelScope) { state ->
|
|
||||||
state.processState(
|
|
||||||
error = ::handleError,
|
|
||||||
success = { response ->
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
isChatCreated.emit(2_000_000_000 + response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val LOAD_COUNT = 30
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-9
@@ -1,9 +0,0 @@
|
|||||||
package dev.meloda.fast.conversations.di
|
|
||||||
|
|
||||||
import dev.meloda.fast.conversations.CreateChatViewModelImpl
|
|
||||||
import org.koin.core.module.dsl.viewModelOf
|
|
||||||
import org.koin.dsl.module
|
|
||||||
|
|
||||||
val createChatModule = module {
|
|
||||||
viewModelOf(::CreateChatViewModelImpl)
|
|
||||||
}
|
|
||||||
-25
@@ -1,25 +0,0 @@
|
|||||||
package dev.meloda.fast.conversations.model
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
|
||||||
import dev.meloda.fast.ui.model.api.UiFriend
|
|
||||||
|
|
||||||
@Immutable
|
|
||||||
data class CreateChatScreenState(
|
|
||||||
val isLoading: Boolean,
|
|
||||||
val isPaginating: Boolean,
|
|
||||||
val isPaginationExhausted: Boolean,
|
|
||||||
val friends: List<UiFriend>,
|
|
||||||
val selectedFriendsIds: List<Int>,
|
|
||||||
val chatTitle: String
|
|
||||||
) {
|
|
||||||
companion object {
|
|
||||||
val EMPTY: CreateChatScreenState = CreateChatScreenState(
|
|
||||||
isLoading = true,
|
|
||||||
isPaginating = false,
|
|
||||||
isPaginationExhausted = false,
|
|
||||||
friends = emptyList(),
|
|
||||||
selectedFriendsIds = emptyList(),
|
|
||||||
chatTitle = ""
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-36
@@ -1,36 +0,0 @@
|
|||||||
package dev.meloda.fast.conversations.navigation
|
|
||||||
|
|
||||||
import androidx.navigation.NavController
|
|
||||||
import androidx.navigation.NavGraphBuilder
|
|
||||||
import androidx.navigation.compose.composable
|
|
||||||
import dev.meloda.fast.conversations.CreateChatViewModel
|
|
||||||
import dev.meloda.fast.conversations.CreateChatViewModelImpl
|
|
||||||
import dev.meloda.fast.conversations.presentation.CreateChatRoute
|
|
||||||
import dev.meloda.fast.ui.extensions.sharedViewModel
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
object CreateChat
|
|
||||||
|
|
||||||
fun NavGraphBuilder.createChatScreen(
|
|
||||||
onChatCreated: (Int) -> Unit,
|
|
||||||
navController: NavController,
|
|
||||||
) {
|
|
||||||
composable<CreateChat> {
|
|
||||||
val viewModel: CreateChatViewModel =
|
|
||||||
it.sharedViewModel<CreateChatViewModelImpl>(navController = navController)
|
|
||||||
|
|
||||||
CreateChatRoute(
|
|
||||||
onError = {
|
|
||||||
|
|
||||||
},
|
|
||||||
onBack = navController::popBackStack,
|
|
||||||
onChatCreated = onChatCreated,
|
|
||||||
viewModel = viewModel
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun NavController.navigateToCreateChat() {
|
|
||||||
this.navigate(CreateChat)
|
|
||||||
}
|
|
||||||
-110
@@ -1,110 +0,0 @@
|
|||||||
package dev.meloda.fast.conversations.presentation
|
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.material3.Checkbox
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import coil.compose.AsyncImage
|
|
||||||
import dev.meloda.fast.ui.R
|
|
||||||
import dev.meloda.fast.ui.model.api.UiFriend
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun CreateChatItem(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
friend: UiFriend,
|
|
||||||
maxLines: Int,
|
|
||||||
isSelected: Boolean,
|
|
||||||
onItemClicked: (Int) -> Unit
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable { onItemClicked(friend.userId) }
|
|
||||||
.padding(vertical = 8.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
|
||||||
|
|
||||||
val friendAvatar = friend.avatar?.extractUrl()
|
|
||||||
|
|
||||||
Box(modifier = Modifier.size(56.dp)) {
|
|
||||||
if (friendAvatar == null) {
|
|
||||||
Image(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.clip(CircleShape),
|
|
||||||
painter = painterResource(id = R.drawable.ic_account_circle_cut),
|
|
||||||
contentDescription = "Avatar",
|
|
||||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
AsyncImage(
|
|
||||||
model = friendAvatar,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.clip(CircleShape),
|
|
||||||
placeholder = painterResource(id = R.drawable.ic_account_circle_cut)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (friend.onlineStatus.isOnline()) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.clip(CircleShape)
|
|
||||||
.size(18.dp)
|
|
||||||
.background(MaterialTheme.colorScheme.background)
|
|
||||||
.padding(2.dp)
|
|
||||||
.align(Alignment.BottomEnd)
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.clip(CircleShape)
|
|
||||||
.matchParentSize()
|
|
||||||
.background(MaterialTheme.colorScheme.primary)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = friend.title,
|
|
||||||
minLines = 1,
|
|
||||||
maxLines = maxLines,
|
|
||||||
style = MaterialTheme.typography.headlineSmall.copy(fontSize = 20.sp),
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
|
||||||
|
|
||||||
Checkbox(
|
|
||||||
checked = isSelected,
|
|
||||||
onCheckedChange = { onItemClicked(friend.userId) },
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
-101
@@ -1,101 +0,0 @@
|
|||||||
package dev.meloda.fast.conversations.presentation
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.rounded.KeyboardArrowUp
|
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.IconButtonDefaults
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextField
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import dev.meloda.fast.conversations.model.CreateChatScreenState
|
|
||||||
import dev.meloda.fast.ui.R
|
|
||||||
import dev.meloda.fast.ui.model.api.UiFriend
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun CreateChatList(
|
|
||||||
screenState: CreateChatScreenState,
|
|
||||||
state: LazyListState,
|
|
||||||
maxLines: Int,
|
|
||||||
modifier: Modifier,
|
|
||||||
padding: PaddingValues,
|
|
||||||
onItemClicked: (Int) -> Unit,
|
|
||||||
onTitleTextInputChanged: (String) -> Unit
|
|
||||||
) {
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
LazyColumn(
|
|
||||||
modifier = modifier,
|
|
||||||
state = state
|
|
||||||
) {
|
|
||||||
item {
|
|
||||||
Spacer(modifier = Modifier.height(padding.calculateTopPadding()))
|
|
||||||
|
|
||||||
}
|
|
||||||
items(
|
|
||||||
items = screenState.friends,
|
|
||||||
key = UiFriend::userId,
|
|
||||||
) { friend ->
|
|
||||||
CreateChatItem(
|
|
||||||
maxLines = maxLines,
|
|
||||||
modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null),
|
|
||||||
friend = friend,
|
|
||||||
isSelected = screenState.selectedFriendsIds.contains(friend.userId),
|
|
||||||
onItemClicked = onItemClicked
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.animateItem(fadeInSpec = null, fadeOutSpec = null),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
if (screenState.isPaginating) {
|
|
||||||
CircularProgressIndicator()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (screenState.isPaginationExhausted) {
|
|
||||||
IconButton(
|
|
||||||
onClick = {
|
|
||||||
coroutineScope.launch(Dispatchers.Main) {
|
|
||||||
state.scrollToItem(14)
|
|
||||||
state.animateScrollToItem(0)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
colors = IconButtonDefaults.filledIconButtonColors()
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Rounded.KeyboardArrowUp,
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-342
@@ -1,342 +0,0 @@
|
|||||||
package dev.meloda.fast.conversations.presentation
|
|
||||||
|
|
||||||
import androidx.compose.animation.animateColorAsState
|
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
|
||||||
import androidx.compose.animation.core.tween
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.border
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
|
||||||
import androidx.compose.foundation.layout.calculateEndPadding
|
|
||||||
import androidx.compose.foundation.layout.calculateStartPadding
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.imePadding
|
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.statusBars
|
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
|
||||||
import androidx.compose.material.icons.rounded.Done
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextField
|
|
||||||
import androidx.compose.material3.TextFieldDefaults
|
|
||||||
import androidx.compose.material3.TopAppBar
|
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
|
||||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
|
||||||
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
|
|
||||||
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
|
||||||
import androidx.compose.material3.surfaceColorAtElevation
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
||||||
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.conversations.CreateChatViewModel
|
|
||||||
import dev.meloda.fast.conversations.model.CreateChatScreenState
|
|
||||||
import dev.meloda.fast.model.BaseError
|
|
||||||
import dev.meloda.fast.ui.components.ErrorView
|
|
||||||
import dev.meloda.fast.ui.components.FullScreenLoader
|
|
||||||
import dev.meloda.fast.ui.components.IconButton
|
|
||||||
import dev.meloda.fast.ui.components.NoItemsView
|
|
||||||
import dev.meloda.fast.ui.theme.LocalHazeState
|
|
||||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
|
||||||
import dev.meloda.fast.ui.util.isScrollingUp
|
|
||||||
import dev.meloda.fast.ui.R as UiR
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun CreateChatRoute(
|
|
||||||
onError: (BaseError) -> Unit,
|
|
||||||
onBack: () -> Unit,
|
|
||||||
onChatCreated: (Int) -> Unit,
|
|
||||||
viewModel: CreateChatViewModel
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
|
||||||
val baseError by viewModel.baseError.collectAsStateWithLifecycle()
|
|
||||||
val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle()
|
|
||||||
val isChatCreated by viewModel.isChatCreated.collectAsStateWithLifecycle()
|
|
||||||
|
|
||||||
LaunchedEffect(isChatCreated) {
|
|
||||||
if (isChatCreated != null) {
|
|
||||||
onChatCreated(isChatCreated ?: -1)
|
|
||||||
viewModel.onNavigatedBack()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CreateChatScreen(
|
|
||||||
screenState = screenState,
|
|
||||||
baseError = baseError,
|
|
||||||
canPaginate = canPaginate,
|
|
||||||
onSessionExpiredLogOutButtonClicked = { onError(BaseError.SessionExpired) },
|
|
||||||
onPaginationConditionsMet = viewModel::onPaginationConditionsMet,
|
|
||||||
onBack = onBack,
|
|
||||||
onRefresh = viewModel::onRefresh,
|
|
||||||
onCreateChatButtonClicked = viewModel::onCreateChatButtonClicked,
|
|
||||||
onItemClicked = viewModel::toggleFriendSelection,
|
|
||||||
onTitleTextInputChanged = viewModel::onTitleTextInputChanged
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(
|
|
||||||
ExperimentalMaterial3Api::class,
|
|
||||||
ExperimentalHazeMaterialsApi::class,
|
|
||||||
)
|
|
||||||
@Composable
|
|
||||||
fun CreateChatScreen(
|
|
||||||
screenState: CreateChatScreenState = CreateChatScreenState.EMPTY,
|
|
||||||
baseError: BaseError? = null,
|
|
||||||
canPaginate: Boolean = false,
|
|
||||||
onSessionExpiredLogOutButtonClicked: () -> Unit = {},
|
|
||||||
onPaginationConditionsMet: () -> Unit = {},
|
|
||||||
onBack: () -> Unit = {},
|
|
||||||
onRefresh: () -> Unit = {},
|
|
||||||
onCreateChatButtonClicked: () -> Unit = {},
|
|
||||||
onItemClicked: (Int) -> Unit = {},
|
|
||||||
onTitleTextInputChanged: (String) -> Unit = {}
|
|
||||||
) {
|
|
||||||
val currentTheme = LocalThemeConfig.current
|
|
||||||
|
|
||||||
val maxLines by remember(currentTheme) {
|
|
||||||
mutableIntStateOf(if (currentTheme.enableMultiline) 2 else 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
val listState = rememberLazyListState()
|
|
||||||
|
|
||||||
val paginationConditionMet by remember(canPaginate, listState) {
|
|
||||||
derivedStateOf {
|
|
||||||
canPaginate &&
|
|
||||||
(listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index
|
|
||||||
?: -9) >= (listState.layoutInfo.totalItemsCount - 6)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(paginationConditionMet) {
|
|
||||||
if (paginationConditionMet && !screenState.isPaginating) {
|
|
||||||
onPaginationConditionsMet()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val hazeState = LocalHazeState.current
|
|
||||||
|
|
||||||
val toolbarColorAlpha by animateFloatAsState(
|
|
||||||
targetValue = if (!listState.canScrollBackward) 1f else 0f,
|
|
||||||
label = "toolbarColorAlpha",
|
|
||||||
animationSpec = tween(durationMillis = 50)
|
|
||||||
)
|
|
||||||
|
|
||||||
val toolbarContainerColor by animateColorAsState(
|
|
||||||
targetValue =
|
|
||||||
if (currentTheme.enableBlur || !listState.canScrollBackward)
|
|
||||||
MaterialTheme.colorScheme.surface
|
|
||||||
else
|
|
||||||
MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp),
|
|
||||||
label = "toolbarColorAlpha",
|
|
||||||
animationSpec = tween(durationMillis = 50)
|
|
||||||
)
|
|
||||||
|
|
||||||
Scaffold(
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
contentWindowInsets = WindowInsets.statusBars,
|
|
||||||
topBar = {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(
|
|
||||||
toolbarContainerColor.copy(
|
|
||||||
alpha = if (currentTheme.enableBlur) toolbarColorAlpha else 1f
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.then(
|
|
||||||
if (currentTheme.enableBlur) {
|
|
||||||
Modifier.hazeEffect(
|
|
||||||
state = hazeState,
|
|
||||||
style = HazeMaterials.thick()
|
|
||||||
)
|
|
||||||
} else Modifier
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
TopAppBar(
|
|
||||||
navigationIcon = {
|
|
||||||
IconButton(onClick = onBack) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
text = stringResource(
|
|
||||||
id = if (screenState.isLoading) UiR.string.title_loading
|
|
||||||
else UiR.string.title_create_chat
|
|
||||||
),
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
style = MaterialTheme.typography.headlineSmall
|
|
||||||
)
|
|
||||||
},
|
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
|
||||||
containerColor = toolbarContainerColor.copy(
|
|
||||||
alpha = 0f
|
|
||||||
)
|
|
||||||
),
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
)
|
|
||||||
|
|
||||||
var isTextFieldFocused by remember {
|
|
||||||
mutableStateOf(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
val borderWidth by animateDpAsState(if (isTextFieldFocused) 1.5.dp else 0.dp)
|
|
||||||
val borderColor by animateColorAsState(
|
|
||||||
if (isTextFieldFocused) MaterialTheme.colorScheme.primary
|
|
||||||
else Color.Transparent
|
|
||||||
)
|
|
||||||
|
|
||||||
TextField(
|
|
||||||
modifier = Modifier
|
|
||||||
.height(58.dp)
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
.border(
|
|
||||||
borderWidth,
|
|
||||||
borderColor,
|
|
||||||
RoundedCornerShape(16.dp)
|
|
||||||
)
|
|
||||||
.clip(RoundedCornerShape(16.dp))
|
|
||||||
.onFocusChanged { isTextFieldFocused = it.hasFocus },
|
|
||||||
value = screenState.chatTitle,
|
|
||||||
onValueChange = onTitleTextInputChanged,
|
|
||||||
label = { Text(text = stringResource(UiR.string.create_chat_title)) },
|
|
||||||
placeholder = { Text(text = stringResource(UiR.string.create_chat_title)) },
|
|
||||||
singleLine = true,
|
|
||||||
colors = TextFieldDefaults.colors(
|
|
||||||
unfocusedIndicatorColor = Color.Transparent,
|
|
||||||
focusedIndicatorColor = Color.Transparent,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
Spacer(Modifier.height(16.dp))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
floatingActionButton = {
|
|
||||||
if (baseError == null) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.imePadding()
|
|
||||||
.navigationBarsPadding()
|
|
||||||
) {
|
|
||||||
ExtendedFloatingActionButton(
|
|
||||||
onClick = onCreateChatButtonClicked,
|
|
||||||
expanded = listState.isScrollingUp(),
|
|
||||||
text = { Text(text = stringResource(UiR.string.action_create)) },
|
|
||||||
icon = {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Rounded.Done,
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) { padding ->
|
|
||||||
when {
|
|
||||||
baseError != null -> {
|
|
||||||
when (baseError) {
|
|
||||||
is BaseError.SessionExpired -> {
|
|
||||||
ErrorView(
|
|
||||||
text = stringResource(UiR.string.session_expired),
|
|
||||||
buttonText = stringResource(UiR.string.action_log_out),
|
|
||||||
onButtonClick = onSessionExpiredLogOutButtonClicked
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
is BaseError.SimpleError -> {
|
|
||||||
ErrorView(
|
|
||||||
text = baseError.message,
|
|
||||||
buttonText = stringResource(UiR.string.try_again),
|
|
||||||
onButtonClick = onRefresh
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
screenState.isLoading && screenState.friends.isEmpty() -> FullScreenLoader()
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
val pullToRefreshState = rememberPullToRefreshState()
|
|
||||||
|
|
||||||
PullToRefreshBox(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(start = padding.calculateStartPadding(LayoutDirection.Ltr))
|
|
||||||
.padding(end = padding.calculateEndPadding(LayoutDirection.Ltr))
|
|
||||||
.padding(bottom = padding.calculateBottomPadding()),
|
|
||||||
state = pullToRefreshState,
|
|
||||||
isRefreshing = screenState.isLoading,
|
|
||||||
onRefresh = onRefresh,
|
|
||||||
indicator = {
|
|
||||||
PullToRefreshDefaults.Indicator(
|
|
||||||
state = pullToRefreshState,
|
|
||||||
isRefreshing = screenState.isLoading,
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.TopCenter)
|
|
||||||
.padding(top = padding.calculateTopPadding()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
CreateChatList(
|
|
||||||
screenState = screenState,
|
|
||||||
state = listState,
|
|
||||||
maxLines = maxLines,
|
|
||||||
modifier = if (currentTheme.enableBlur) {
|
|
||||||
Modifier.hazeSource(state = hazeState)
|
|
||||||
} else {
|
|
||||||
Modifier
|
|
||||||
}.fillMaxSize(),
|
|
||||||
padding = padding,
|
|
||||||
onItemClicked = onItemClicked,
|
|
||||||
onTitleTextInputChanged = onTitleTextInputChanged
|
|
||||||
)
|
|
||||||
|
|
||||||
if (screenState.friends.isEmpty()) {
|
|
||||||
NoItemsView(
|
|
||||||
buttonText = stringResource(UiR.string.action_refresh),
|
|
||||||
onButtonClick = onRefresh
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,8 +9,8 @@ import dev.meloda.fast.data.processState
|
|||||||
import dev.meloda.fast.datastore.UserSettings
|
import dev.meloda.fast.datastore.UserSettings
|
||||||
import dev.meloda.fast.domain.FriendsUseCase
|
import dev.meloda.fast.domain.FriendsUseCase
|
||||||
import dev.meloda.fast.domain.LoadUsersByIdsUseCase
|
import dev.meloda.fast.domain.LoadUsersByIdsUseCase
|
||||||
import dev.meloda.fast.domain.util.asPresentation
|
|
||||||
import dev.meloda.fast.friends.model.FriendsScreenState
|
import dev.meloda.fast.friends.model.FriendsScreenState
|
||||||
|
import dev.meloda.fast.friends.util.asPresentation
|
||||||
import dev.meloda.fast.model.BaseError
|
import dev.meloda.fast.model.BaseError
|
||||||
import dev.meloda.fast.model.api.domain.VkUser
|
import dev.meloda.fast.model.api.domain.VkUser
|
||||||
import dev.meloda.fast.network.VkErrorCode
|
import dev.meloda.fast.network.VkErrorCode
|
||||||
@@ -68,7 +68,6 @@ class FriendsViewModelImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onRefresh() {
|
override fun onRefresh() {
|
||||||
onErrorConsumed()
|
|
||||||
loadFriends(offset = 0)
|
loadFriends(offset = 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,12 +99,32 @@ class FriendsViewModelImpl(
|
|||||||
friendsUseCase.getOnlineFriends(null, null)
|
friendsUseCase.getOnlineFriends(null, null)
|
||||||
.listenValue(viewModelScope) { state ->
|
.listenValue(viewModelScope) { state ->
|
||||||
state.processState(
|
state.processState(
|
||||||
error = ::handleError,
|
error = { error ->
|
||||||
|
if (error is State.Error.ApiError) {
|
||||||
|
when (error.errorCode) {
|
||||||
|
VkErrorCode.USER_AUTHORIZATION_FAILED -> {
|
||||||
|
baseError.setValue { BaseError.SessionExpired }
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
success = { userIds ->
|
success = { userIds ->
|
||||||
loadUsersByIdsUseCase(userIds = userIds)
|
loadUsersByIdsUseCase(userIds = userIds)
|
||||||
.listenValue(viewModelScope) { state ->
|
.listenValue(viewModelScope) { state ->
|
||||||
state.processState(
|
state.processState(
|
||||||
error = ::handleError,
|
error = { error ->
|
||||||
|
if (error is State.Error.ApiError) {
|
||||||
|
when (error.errorCode) {
|
||||||
|
VkErrorCode.USER_AUTHORIZATION_FAILED -> {
|
||||||
|
baseError.setValue { BaseError.SessionExpired }
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
success = { onlineFriends ->
|
success = { onlineFriends ->
|
||||||
screenState.setValue { old ->
|
screenState.setValue { old ->
|
||||||
old.copy(
|
old.copy(
|
||||||
@@ -123,7 +142,17 @@ class FriendsViewModelImpl(
|
|||||||
friendsUseCase.getFriends(count = LOAD_COUNT, offset = offset)
|
friendsUseCase.getFriends(count = LOAD_COUNT, offset = offset)
|
||||||
.listenValue(viewModelScope) { state ->
|
.listenValue(viewModelScope) { state ->
|
||||||
state.processState(
|
state.processState(
|
||||||
error = ::handleError,
|
error = { error ->
|
||||||
|
if (error is State.Error.ApiError) {
|
||||||
|
when (error.errorCode) {
|
||||||
|
VkErrorCode.USER_AUTHORIZATION_FAILED -> {
|
||||||
|
baseError.setValue { BaseError.SessionExpired }
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
success = { response ->
|
success = { response ->
|
||||||
val itemsCountSufficient = response.size == LOAD_COUNT
|
val itemsCountSufficient = response.size == LOAD_COUNT
|
||||||
canPaginate.setValue { itemsCountSufficient }
|
canPaginate.setValue { itemsCountSufficient }
|
||||||
@@ -168,40 +197,6 @@ class FriendsViewModelImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 updateFriendsNames(useContactNames: Boolean) {
|
private fun updateFriendsNames(useContactNames: Boolean) {
|
||||||
val friends = friends.value
|
val friends = friends.value
|
||||||
if (friends.isEmpty()) return
|
if (friends.isEmpty()) return
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package dev.meloda.fast.friends.model
|
package dev.meloda.fast.friends.model
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import dev.meloda.fast.ui.model.api.UiFriend
|
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class FriendsScreenState(
|
data class FriendsScreenState(
|
||||||
|
|||||||
+1
-5
@@ -1,15 +1,11 @@
|
|||||||
package dev.meloda.fast.ui.model.api
|
package dev.meloda.fast.friends.model
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
|
||||||
import dev.meloda.fast.common.model.UiImage
|
import dev.meloda.fast.common.model.UiImage
|
||||||
import dev.meloda.fast.model.api.domain.OnlineStatus
|
import dev.meloda.fast.model.api.domain.OnlineStatus
|
||||||
|
|
||||||
@Immutable
|
|
||||||
data class UiFriend(
|
data class UiFriend(
|
||||||
val userId: Int,
|
val userId: Int,
|
||||||
val avatar: UiImage?,
|
val avatar: UiImage?,
|
||||||
val firstName: String,
|
|
||||||
val lastName: String,
|
|
||||||
val title: String,
|
val title: String,
|
||||||
val onlineStatus: OnlineStatus,
|
val onlineStatus: OnlineStatus,
|
||||||
val photo400Orig: UiImage?
|
val photo400Orig: UiImage?
|
||||||
+2
-4
@@ -16,8 +16,7 @@ object Friends
|
|||||||
fun NavGraphBuilder.friendsScreen(
|
fun NavGraphBuilder.friendsScreen(
|
||||||
onError: (BaseError) -> Unit,
|
onError: (BaseError) -> Unit,
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
onPhotoClicked: (url: String) -> Unit,
|
onPhotoClicked: (url: String) -> Unit
|
||||||
onMessageClicked: (userId: Int) -> Unit
|
|
||||||
) {
|
) {
|
||||||
composable<Friends> {
|
composable<Friends> {
|
||||||
val viewModel: FriendsViewModel =
|
val viewModel: FriendsViewModel =
|
||||||
@@ -26,8 +25,7 @@ fun NavGraphBuilder.friendsScreen(
|
|||||||
FriendsRoute(
|
FriendsRoute(
|
||||||
onError = onError,
|
onError = onError,
|
||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
onPhotoClicked = onPhotoClicked,
|
onPhotoClicked = onPhotoClicked
|
||||||
onMessageClicked = onMessageClicked
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,10 +12,6 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.rounded.MailOutline
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -27,16 +23,15 @@ import androidx.compose.ui.res.painterResource
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
|
import dev.meloda.fast.friends.model.UiFriend
|
||||||
import dev.meloda.fast.ui.R
|
import dev.meloda.fast.ui.R
|
||||||
import dev.meloda.fast.ui.model.api.UiFriend
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun FriendItem(
|
fun FriendItem(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
friend: UiFriend,
|
friend: UiFriend,
|
||||||
maxLines: Int,
|
maxLines: Int,
|
||||||
onPhotoClicked: (url: String) -> Unit,
|
onPhotoClicked: (url: String) -> Unit
|
||||||
onMessageClicked: (userId: Int) -> Unit
|
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
@@ -97,24 +92,9 @@ fun FriendItem(
|
|||||||
text = friend.title,
|
text = friend.title,
|
||||||
minLines = 1,
|
minLines = 1,
|
||||||
maxLines = maxLines,
|
maxLines = maxLines,
|
||||||
style = MaterialTheme.typography.headlineSmall.copy(fontSize = 20.sp),
|
style = MaterialTheme.typography.headlineSmall.copy(fontSize = 20.sp)
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
|
||||||
IconButton(
|
|
||||||
onClick = {
|
|
||||||
onMessageClicked(friend.userId)
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Rounded.MailOutline,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-7
@@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.PaddingValues
|
|||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
@@ -22,7 +23,8 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import dev.meloda.fast.friends.model.FriendsScreenState
|
import dev.meloda.fast.friends.model.FriendsScreenState
|
||||||
import dev.meloda.fast.ui.model.api.UiFriend
|
import dev.meloda.fast.friends.model.UiFriend
|
||||||
|
import dev.meloda.fast.ui.theme.LocalBottomPadding
|
||||||
import dev.meloda.fast.ui.util.ImmutableList
|
import dev.meloda.fast.ui.util.ImmutableList
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -36,7 +38,6 @@ fun FriendsList(
|
|||||||
maxLines: Int,
|
maxLines: Int,
|
||||||
padding: PaddingValues,
|
padding: PaddingValues,
|
||||||
onPhotoClicked: (url: String) -> Unit,
|
onPhotoClicked: (url: String) -> Unit,
|
||||||
onMessageClicked: (userId: Int) -> Unit,
|
|
||||||
setCanScrollBackward: (Boolean) -> Unit
|
setCanScrollBackward: (Boolean) -> Unit
|
||||||
) {
|
) {
|
||||||
LaunchedEffect(listState) {
|
LaunchedEffect(listState) {
|
||||||
@@ -48,6 +49,8 @@ fun FriendsList(
|
|||||||
|
|
||||||
val friends = uiFriends.toList()
|
val friends = uiFriends.toList()
|
||||||
|
|
||||||
|
val bottomPadding = LocalBottomPadding.current
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
state = listState
|
state = listState
|
||||||
@@ -64,8 +67,7 @@ fun FriendsList(
|
|||||||
FriendItem(
|
FriendItem(
|
||||||
friend = friend,
|
friend = friend,
|
||||||
maxLines = maxLines,
|
maxLines = maxLines,
|
||||||
onPhotoClicked = onPhotoClicked,
|
onPhotoClicked = onPhotoClicked
|
||||||
onMessageClicked = onMessageClicked
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
@@ -75,7 +77,8 @@ fun FriendsList(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.animateItem(fadeInSpec = null, fadeOutSpec = null),
|
.animateItem(fadeInSpec = null, fadeOutSpec = null)
|
||||||
|
.navigationBarsPadding(),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
if (screenState.isPaginating) {
|
if (screenState.isPaginating) {
|
||||||
@@ -98,9 +101,11 @@ fun FriendsList(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(bottomPadding))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-31
@@ -48,8 +48,8 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import coil.imageLoader
|
import coil.imageLoader
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import dev.chrisbanes.haze.hazeEffect
|
import dev.chrisbanes.haze.haze
|
||||||
import dev.chrisbanes.haze.hazeSource
|
import dev.chrisbanes.haze.hazeChild
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
import dev.chrisbanes.haze.materials.HazeMaterials
|
import dev.chrisbanes.haze.materials.HazeMaterials
|
||||||
import dev.meloda.fast.friends.FriendsViewModel
|
import dev.meloda.fast.friends.FriendsViewModel
|
||||||
@@ -72,7 +72,6 @@ import dev.meloda.fast.ui.R as UiR
|
|||||||
fun FriendsRoute(
|
fun FriendsRoute(
|
||||||
onError: (BaseError) -> Unit,
|
onError: (BaseError) -> Unit,
|
||||||
onPhotoClicked: (url: String) -> Unit,
|
onPhotoClicked: (url: String) -> Unit,
|
||||||
onMessageClicked: (userId: Int) -> Unit,
|
|
||||||
viewModel: FriendsViewModel = koinViewModel<FriendsViewModelImpl>()
|
viewModel: FriendsViewModel = koinViewModel<FriendsViewModelImpl>()
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -100,12 +99,11 @@ fun FriendsRoute(
|
|||||||
onPaginationConditionsMet = viewModel::onPaginationConditionsMet,
|
onPaginationConditionsMet = viewModel::onPaginationConditionsMet,
|
||||||
onRefresh = viewModel::onRefresh,
|
onRefresh = viewModel::onRefresh,
|
||||||
onPhotoClicked = onPhotoClicked,
|
onPhotoClicked = onPhotoClicked,
|
||||||
onMessageClicked = onMessageClicked,
|
|
||||||
setSelectedTabIndex = viewModel::onTabSelected,
|
setSelectedTabIndex = viewModel::onTabSelected,
|
||||||
setScrollIndex = viewModel::setScrollIndex,
|
setScrollIndex = viewModel::setScrollIndex,
|
||||||
setScrollOffset = viewModel::setScrollOffset,
|
setScrollOffset = viewModel::setScrollOffset,
|
||||||
setScrollIndexOnline = viewModel::setScrollIndexOnline,
|
setScrollIndexOnline = viewModel::setScrollIndexOnline,
|
||||||
setScrollOffsetOnline = viewModel::setScrollOffsetOnline
|
setScrollOffsetOnline = viewModel::setScrollOffsetOnline,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,12 +120,11 @@ fun FriendsScreen(
|
|||||||
onPaginationConditionsMet: () -> Unit = {},
|
onPaginationConditionsMet: () -> Unit = {},
|
||||||
onRefresh: () -> Unit = {},
|
onRefresh: () -> Unit = {},
|
||||||
onPhotoClicked: (url: String) -> Unit = {},
|
onPhotoClicked: (url: String) -> Unit = {},
|
||||||
onMessageClicked: (userId: Int) -> Unit = {},
|
|
||||||
setSelectedTabIndex: (Int) -> Unit = {},
|
setSelectedTabIndex: (Int) -> Unit = {},
|
||||||
setScrollIndex: (Int) -> Unit = {},
|
setScrollIndex: (Int) -> Unit = {},
|
||||||
setScrollOffset: (Int) -> Unit = {},
|
setScrollOffset: (Int) -> Unit = {},
|
||||||
setScrollIndexOnline: (Int) -> Unit = {},
|
setScrollIndexOnline: (Int) -> Unit = {},
|
||||||
setScrollOffsetOnline: (Int) -> Unit = {}
|
setScrollOffsetOnline: (Int) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val currentTheme = LocalThemeConfig.current
|
val currentTheme = LocalThemeConfig.current
|
||||||
|
|
||||||
@@ -234,7 +231,7 @@ fun FriendsScreen(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.then(
|
.then(
|
||||||
if (currentTheme.enableBlur) {
|
if (currentTheme.enableBlur) {
|
||||||
Modifier.hazeEffect(
|
Modifier.hazeChild(
|
||||||
state = hazeState,
|
state = hazeState,
|
||||||
style = HazeMaterials.thick()
|
style = HazeMaterials.thick()
|
||||||
)
|
)
|
||||||
@@ -284,26 +281,14 @@ fun FriendsScreen(
|
|||||||
}
|
}
|
||||||
) { padding ->
|
) { padding ->
|
||||||
when {
|
when {
|
||||||
baseError != null -> {
|
baseError is BaseError.SessionExpired -> {
|
||||||
when (baseError) {
|
|
||||||
is BaseError.SessionExpired -> {
|
|
||||||
ErrorView(
|
ErrorView(
|
||||||
text = stringResource(UiR.string.session_expired),
|
text = "Session expired",
|
||||||
buttonText = stringResource(UiR.string.action_log_out),
|
buttonText = "Log out",
|
||||||
onButtonClick = onSessionExpiredLogOutButtonClicked
|
onButtonClick = onSessionExpiredLogOutButtonClicked
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is BaseError.SimpleError -> {
|
|
||||||
ErrorView(
|
|
||||||
text = baseError.message,
|
|
||||||
buttonText = stringResource(UiR.string.try_again),
|
|
||||||
onButtonClick = onRefresh
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
screenState.isLoading && screenState.friends.isEmpty() -> FullScreenLoader()
|
screenState.isLoading && screenState.friends.isEmpty() -> FullScreenLoader()
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
@@ -348,17 +333,15 @@ fun FriendsScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
val friendsToDisplay = remember(index) {
|
val friendsToDisplay = if (index == 0) {
|
||||||
if (index == 0) {
|
|
||||||
screenState.friends
|
screenState.friends
|
||||||
} else {
|
} else {
|
||||||
screenState.onlineFriends
|
screenState.onlineFriends
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
FriendsList(
|
FriendsList(
|
||||||
modifier = if (currentTheme.enableBlur) {
|
modifier = if (currentTheme.enableBlur) {
|
||||||
Modifier.hazeSource(state = hazeState)
|
Modifier.haze(state = hazeState)
|
||||||
} else {
|
} else {
|
||||||
Modifier
|
Modifier
|
||||||
}.fillMaxSize(),
|
}.fillMaxSize(),
|
||||||
@@ -368,7 +351,6 @@ fun FriendsScreen(
|
|||||||
maxLines = maxLines,
|
maxLines = maxLines,
|
||||||
padding = padding,
|
padding = padding,
|
||||||
onPhotoClicked = onPhotoClicked,
|
onPhotoClicked = onPhotoClicked,
|
||||||
onMessageClicked = onMessageClicked,
|
|
||||||
setCanScrollBackward = { can ->
|
setCanScrollBackward = { can ->
|
||||||
canScrollBackward = can
|
canScrollBackward = can
|
||||||
}
|
}
|
||||||
@@ -376,9 +358,10 @@ fun FriendsScreen(
|
|||||||
|
|
||||||
if (friendsToDisplay.isEmpty()) {
|
if (friendsToDisplay.isEmpty()) {
|
||||||
NoItemsView(
|
NoItemsView(
|
||||||
customText = if (index == 1) stringResource(UiR.string.no_online_friends) else null,
|
modifier = Modifier
|
||||||
buttonText = stringResource(UiR.string.action_refresh),
|
.padding(padding.calculateTopPadding())
|
||||||
onButtonClick = onRefresh
|
.padding(top = 16.dp),
|
||||||
|
customText = "No${if (index == 1) " online" else ""} friends :("
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-5
@@ -1,9 +1,9 @@
|
|||||||
package dev.meloda.fast.domain.util
|
package dev.meloda.fast.friends.util
|
||||||
|
|
||||||
import dev.meloda.fast.common.model.UiImage
|
import dev.meloda.fast.common.model.UiImage
|
||||||
import dev.meloda.fast.data.VkMemoryCache
|
import dev.meloda.fast.data.VkMemoryCache
|
||||||
|
import dev.meloda.fast.friends.model.UiFriend
|
||||||
import dev.meloda.fast.model.api.domain.VkUser
|
import dev.meloda.fast.model.api.domain.VkUser
|
||||||
import dev.meloda.fast.ui.model.api.UiFriend
|
|
||||||
|
|
||||||
fun VkUser.asPresentation(
|
fun VkUser.asPresentation(
|
||||||
useContactNames: Boolean = false
|
useContactNames: Boolean = false
|
||||||
@@ -16,7 +16,5 @@ fun VkUser.asPresentation(
|
|||||||
fullName
|
fullName
|
||||||
},
|
},
|
||||||
onlineStatus = onlineStatus,
|
onlineStatus = onlineStatus,
|
||||||
photo400Orig = photo400Orig?.let(UiImage::Url),
|
photo400Orig = photo400Orig?.let(UiImage::Url)
|
||||||
firstName = firstName,
|
|
||||||
lastName = lastName
|
|
||||||
)
|
)
|
||||||
+34
-83
@@ -24,13 +24,11 @@ import dev.meloda.fast.domain.LongPollUpdatesParser
|
|||||||
import dev.meloda.fast.domain.MessagesUseCase
|
import dev.meloda.fast.domain.MessagesUseCase
|
||||||
import dev.meloda.fast.messageshistory.model.ActionMode
|
import dev.meloda.fast.messageshistory.model.ActionMode
|
||||||
import dev.meloda.fast.messageshistory.model.MessagesHistoryScreenState
|
import dev.meloda.fast.messageshistory.model.MessagesHistoryScreenState
|
||||||
import dev.meloda.fast.messageshistory.model.SendingStatus
|
|
||||||
import dev.meloda.fast.messageshistory.model.UiItem
|
import dev.meloda.fast.messageshistory.model.UiItem
|
||||||
import dev.meloda.fast.messageshistory.navigation.MessagesHistory
|
import dev.meloda.fast.messageshistory.navigation.MessagesHistory
|
||||||
import dev.meloda.fast.messageshistory.util.asPresentation
|
import dev.meloda.fast.messageshistory.util.asPresentation
|
||||||
import dev.meloda.fast.messageshistory.util.extractAvatar
|
import dev.meloda.fast.messageshistory.util.extractAvatar
|
||||||
import dev.meloda.fast.messageshistory.util.extractTitle
|
import dev.meloda.fast.messageshistory.util.extractTitle
|
||||||
import dev.meloda.fast.messageshistory.util.findMessageById
|
|
||||||
import dev.meloda.fast.model.BaseError
|
import dev.meloda.fast.model.BaseError
|
||||||
import dev.meloda.fast.model.LongPollEvent
|
import dev.meloda.fast.model.LongPollEvent
|
||||||
import dev.meloda.fast.model.api.domain.VkAttachment
|
import dev.meloda.fast.model.api.domain.VkAttachment
|
||||||
@@ -162,9 +160,7 @@ class MessagesHistoryViewModelImpl(
|
|||||||
val message = event.message
|
val message = event.message
|
||||||
|
|
||||||
Log.d("MessagesHistoryViewModel", "handleNewMessage: $message")
|
Log.d("MessagesHistoryViewModel", "handleNewMessage: $message")
|
||||||
|
|
||||||
if (message.peerId != screenState.value.conversationId) return
|
if (message.peerId != screenState.value.conversationId) return
|
||||||
if (screenState.value.messages.findMessageById(message.id) != null) return
|
|
||||||
|
|
||||||
val randomIds = messages.value.map(VkMessage::randomId)
|
val randomIds = messages.value.map(VkMessage::randomId)
|
||||||
if (message.randomId != 0 && message.randomId in randomIds) return
|
if (message.randomId != 0 && message.randomId in randomIds) return
|
||||||
@@ -178,22 +174,22 @@ class MessagesHistoryViewModelImpl(
|
|||||||
|
|
||||||
val newMessage = message.asPresentation(
|
val newMessage = message.asPresentation(
|
||||||
resourceProvider = resourceProvider,
|
resourceProvider = resourceProvider,
|
||||||
|
showDate = false,
|
||||||
showName = false,
|
showName = false,
|
||||||
prevMessage = prevMessage,
|
prevMessage = prevMessage,
|
||||||
nextMessage = null,
|
nextMessage = null,
|
||||||
showTimeInActionMessages = userSettings.showTimeInActionMessages.value,
|
showTimeInActionMessages = userSettings.showTimeInActionMessages.value
|
||||||
conversation = screenState.value.conversation,
|
|
||||||
)
|
)
|
||||||
newMessages.add(0, newMessage)
|
newMessages.add(0, newMessage)
|
||||||
|
|
||||||
prevMessage?.let { prev ->
|
prevMessage?.let { prev ->
|
||||||
newMessages[1] = prev.asPresentation(
|
newMessages[1] = prev.asPresentation(
|
||||||
resourceProvider = resourceProvider,
|
resourceProvider = resourceProvider,
|
||||||
|
showDate = false,
|
||||||
showName = false,
|
showName = false,
|
||||||
prevMessage = prevMessage,
|
prevMessage = prevMessage,
|
||||||
nextMessage = messages.value.first(),
|
nextMessage = messages.value.first(),
|
||||||
showTimeInActionMessages = userSettings.showTimeInActionMessages.value,
|
showTimeInActionMessages = userSettings.showTimeInActionMessages.value
|
||||||
conversation = screenState.value.conversation
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,11 +205,11 @@ class MessagesHistoryViewModelImpl(
|
|||||||
?.let { index ->
|
?.let { index ->
|
||||||
val newMessage = message.asPresentation(
|
val newMessage = message.asPresentation(
|
||||||
resourceProvider = resourceProvider,
|
resourceProvider = resourceProvider,
|
||||||
|
showDate = false,
|
||||||
showName = false,
|
showName = false,
|
||||||
prevMessage = messages.value.getOrNull(index + 1),
|
prevMessage = messages.value.getOrNull(index + 1),
|
||||||
nextMessage = messages.value.getOrNull(index - 1),
|
nextMessage = messages.value.getOrNull(index - 1),
|
||||||
showTimeInActionMessages = userSettings.showTimeInActionMessages.value,
|
showTimeInActionMessages = userSettings.showTimeInActionMessages.value
|
||||||
conversation = screenState.value.conversation
|
|
||||||
)
|
)
|
||||||
|
|
||||||
val newMessages = screenState.value.messages.toMutableList()
|
val newMessages = screenState.value.messages.toMutableList()
|
||||||
@@ -228,37 +224,7 @@ class MessagesHistoryViewModelImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleReadOutgoingEvent(event: LongPollEvent.VkMessageReadOutgoingEvent) {
|
private fun handleReadOutgoingEvent(event: LongPollEvent.VkMessageReadOutgoingEvent) {
|
||||||
if (event.peerId != screenState.value.conversationId) return
|
|
||||||
|
|
||||||
val messages = messages.value
|
|
||||||
val messageIndex =
|
|
||||||
messages.indexOfFirstOrNull { it.id == event.messageId }
|
|
||||||
|
|
||||||
if (messageIndex == null) { // диалога нет в списке
|
|
||||||
// pizdets
|
|
||||||
} else {
|
|
||||||
val newConversation = screenState.value.conversation.copy(
|
|
||||||
outRead = event.messageId
|
|
||||||
)
|
|
||||||
|
|
||||||
val uiMessages = messages.mapIndexed { index, item ->
|
|
||||||
item.asPresentation(
|
|
||||||
resourceProvider = resourceProvider,
|
|
||||||
showName = false,
|
|
||||||
prevMessage = messages.getOrNull(index + 1),
|
|
||||||
nextMessage = messages.getOrNull(index - 1),
|
|
||||||
showTimeInActionMessages = userSettings.showTimeInActionMessages.value,
|
|
||||||
conversation = newConversation
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
screenState.setValue { old ->
|
|
||||||
old.copy(
|
|
||||||
conversation = newConversation,
|
|
||||||
messages = uiMessages,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadMessagesHistory(offset: Int = currentOffset.value) {
|
private fun loadMessagesHistory(offset: Int = currentOffset.value) {
|
||||||
@@ -270,7 +236,9 @@ class MessagesHistoryViewModelImpl(
|
|||||||
offset = offset,
|
offset = offset,
|
||||||
).listenValue(viewModelScope) { state ->
|
).listenValue(viewModelScope) { state ->
|
||||||
state.processState(
|
state.processState(
|
||||||
error = { error -> },
|
error = { error ->
|
||||||
|
|
||||||
|
},
|
||||||
success = { response ->
|
success = { response ->
|
||||||
val messages = response.messages
|
val messages = response.messages
|
||||||
val fullMessages = if (offset == 0) {
|
val fullMessages = if (offset == 0) {
|
||||||
@@ -288,6 +256,16 @@ class MessagesHistoryViewModelImpl(
|
|||||||
messagesUseCase.storeMessages(messages)
|
messagesUseCase.storeMessages(messages)
|
||||||
conversationsUseCase.storeConversations(conversations)
|
conversationsUseCase.storeConversations(conversations)
|
||||||
|
|
||||||
|
val loadedMessages = fullMessages.mapIndexed { index, message ->
|
||||||
|
message.asPresentation(
|
||||||
|
resourceProvider = resourceProvider,
|
||||||
|
showDate = false,
|
||||||
|
showName = false,
|
||||||
|
prevMessage = messages.getOrNull(index + 1),
|
||||||
|
nextMessage = messages.getOrNull(index - 1),
|
||||||
|
showTimeInActionMessages = userSettings.showTimeInActionMessages.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val itemsCountSufficient = messages.size == MESSAGES_LOAD_COUNT
|
val itemsCountSufficient = messages.size == MESSAGES_LOAD_COUNT
|
||||||
|
|
||||||
@@ -300,25 +278,12 @@ class MessagesHistoryViewModelImpl(
|
|||||||
conversations
|
conversations
|
||||||
.firstOrNull { it.id == screenState.value.conversationId }
|
.firstOrNull { it.id == screenState.value.conversationId }
|
||||||
?.let { conversation ->
|
?.let { conversation ->
|
||||||
screenState.setValue { old -> old.copy(conversation = conversation) }
|
|
||||||
newState = newState.copy(
|
newState = newState.copy(
|
||||||
title = conversation.extractTitle(
|
title = conversation.extractTitle(
|
||||||
useContactName = AppSettings.General.useContactNames,
|
useContactName = AppSettings.General.useContactNames,
|
||||||
resources = resourceProvider.resources
|
resources = resourceProvider.resources
|
||||||
),
|
),
|
||||||
avatar = conversation.extractAvatar(),
|
avatar = conversation.extractAvatar()
|
||||||
conversation = conversation
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val loadedMessages = fullMessages.mapIndexed { index, message ->
|
|
||||||
message.asPresentation(
|
|
||||||
resourceProvider = resourceProvider,
|
|
||||||
showName = false,
|
|
||||||
prevMessage = messages.getOrNull(index + 1),
|
|
||||||
nextMessage = messages.getOrNull(index - 1),
|
|
||||||
showTimeInActionMessages = userSettings.showTimeInActionMessages.value,
|
|
||||||
conversation = screenState.value.conversation
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -382,14 +347,18 @@ class MessagesHistoryViewModelImpl(
|
|||||||
val newMessages = screenState.value.messages.toMutableList()
|
val newMessages = screenState.value.messages.toMutableList()
|
||||||
val newUiMessage = newMessage.asPresentation(
|
val newUiMessage = newMessage.asPresentation(
|
||||||
resourceProvider = resourceProvider,
|
resourceProvider = resourceProvider,
|
||||||
|
showDate = false,
|
||||||
showName = false,
|
showName = false,
|
||||||
prevMessage = messages.value.firstOrNull(),
|
prevMessage = messages.value.firstOrNull(),
|
||||||
nextMessage = null,
|
nextMessage = null,
|
||||||
showTimeInActionMessages = userSettings.showTimeInActionMessages.value,
|
showTimeInActionMessages = userSettings.showTimeInActionMessages.value
|
||||||
conversation = screenState.value.conversation
|
|
||||||
)
|
)
|
||||||
newMessages.add(0, newUiMessage)
|
newMessages.add(0, newUiMessage)
|
||||||
|
|
||||||
|
messages.setValue { old ->
|
||||||
|
listOf(newMessage).plus(old)
|
||||||
|
}
|
||||||
|
|
||||||
screenState.setValue { old ->
|
screenState.setValue { old ->
|
||||||
old.copy(
|
old.copy(
|
||||||
message = TextFieldValue(),
|
message = TextFieldValue(),
|
||||||
@@ -408,37 +377,19 @@ class MessagesHistoryViewModelImpl(
|
|||||||
state.processState(
|
state.processState(
|
||||||
error = { error ->
|
error = { error ->
|
||||||
sendingMessages -= newMessage
|
sendingMessages -= newMessage
|
||||||
|
|
||||||
val uiMessages = screenState.value.messages.toMutableList()
|
|
||||||
|
|
||||||
uiMessages.indexOfOrNull(newUiMessage)?.let { index ->
|
|
||||||
(uiMessages[index] as? UiItem.Message)?.let { message ->
|
|
||||||
uiMessages[index] = message.copy(sendingStatus = SendingStatus.FAILED)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
screenState.setValue { old -> old.copy(messages = uiMessages) }
|
|
||||||
},
|
},
|
||||||
success = { messageId ->
|
success = { messageId ->
|
||||||
sendingMessages -= newMessage
|
sendingMessages += newMessage
|
||||||
|
|
||||||
val uiMessages = screenState.value.messages.toMutableList()
|
val messages = screenState.value.messages.toMutableList()
|
||||||
messages.setValue { old ->
|
|
||||||
listOf(newMessage.copy(id = messageId)).plus(old)
|
|
||||||
}
|
|
||||||
|
|
||||||
uiMessages.indexOfOrNull(newUiMessage)?.let { index ->
|
messages.indexOfOrNull(newUiMessage)?.let { index ->
|
||||||
(uiMessages[index] as? UiItem.Message)?.let { message ->
|
(messages[index] as? UiItem.Message)?.let { message ->
|
||||||
uiMessages[index] = message
|
messages[index] = message.copy(id = messageId)
|
||||||
.copy(
|
|
||||||
id = messageId,
|
|
||||||
sendingStatus = SendingStatus.SENT
|
|
||||||
)
|
|
||||||
.copy(isRead = newMessage.isRead(screenState.value.conversation))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
screenState.setValue { old -> old.copy(messages = uiMessages) }
|
screenState.setValue { old -> old.copy(messages = messages) }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -557,11 +508,11 @@ class MessagesHistoryViewModelImpl(
|
|||||||
val uiMessages = messages.mapIndexed { index, item ->
|
val uiMessages = messages.mapIndexed { index, item ->
|
||||||
item.asPresentation(
|
item.asPresentation(
|
||||||
resourceProvider = resourceProvider,
|
resourceProvider = resourceProvider,
|
||||||
|
showDate = false,
|
||||||
showName = false,
|
showName = false,
|
||||||
prevMessage = messages.getOrNull(index + 1),
|
prevMessage = messages.getOrNull(index + 1),
|
||||||
nextMessage = messages.getOrNull(index - 1),
|
nextMessage = messages.getOrNull(index - 1),
|
||||||
showTimeInActionMessages = show,
|
showTimeInActionMessages = show
|
||||||
conversation = screenState.value.conversation
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-5
@@ -4,7 +4,6 @@ import androidx.compose.runtime.Immutable
|
|||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import dev.meloda.fast.common.model.UiImage
|
import dev.meloda.fast.common.model.UiImage
|
||||||
import dev.meloda.fast.model.api.domain.VkAttachment
|
import dev.meloda.fast.model.api.domain.VkAttachment
|
||||||
import dev.meloda.fast.model.api.domain.VkConversation
|
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class MessagesHistoryScreenState(
|
data class MessagesHistoryScreenState(
|
||||||
@@ -19,8 +18,7 @@ data class MessagesHistoryScreenState(
|
|||||||
val isPaginating: Boolean,
|
val isPaginating: Boolean,
|
||||||
val isPaginationExhausted: Boolean,
|
val isPaginationExhausted: Boolean,
|
||||||
val actionMode: ActionMode,
|
val actionMode: ActionMode,
|
||||||
val chatImageUrl: String?,
|
val chatImageUrl: String?
|
||||||
val conversation: VkConversation
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -36,8 +34,7 @@ data class MessagesHistoryScreenState(
|
|||||||
isPaginating = false,
|
isPaginating = false,
|
||||||
isPaginationExhausted = false,
|
isPaginationExhausted = false,
|
||||||
actionMode = ActionMode.Record,
|
actionMode = ActionMode.Record,
|
||||||
chatImageUrl = null,
|
chatImageUrl = null
|
||||||
conversation = VkConversation.EMPTY
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-5
@@ -1,5 +0,0 @@
|
|||||||
package dev.meloda.fast.messageshistory.model
|
|
||||||
|
|
||||||
enum class SendingStatus {
|
|
||||||
SENDING, SENT, FAILED
|
|
||||||
}
|
|
||||||
+2
-3
@@ -22,9 +22,7 @@ sealed class UiItem(
|
|||||||
val showAvatar: Boolean,
|
val showAvatar: Boolean,
|
||||||
val showName: Boolean,
|
val showName: Boolean,
|
||||||
val avatar: UiImage,
|
val avatar: UiImage,
|
||||||
val isEdited: Boolean,
|
val isEdited: Boolean
|
||||||
val isRead: Boolean,
|
|
||||||
val sendingStatus: SendingStatus = SendingStatus.SENT
|
|
||||||
) : UiItem(id, conversationMessageId)
|
) : UiItem(id, conversationMessageId)
|
||||||
|
|
||||||
data class ActionMessage(
|
data class ActionMessage(
|
||||||
@@ -34,3 +32,4 @@ sealed class UiItem(
|
|||||||
val actionCmId: Int?
|
val actionCmId: Int?
|
||||||
) : UiItem(id, conversationMessageId)
|
) : UiItem(id, conversationMessageId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
-4
@@ -31,7 +31,6 @@ import dev.meloda.fast.messageshistory.model.UiItem
|
|||||||
fun IncomingMessageBubble(
|
fun IncomingMessageBubble(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
message: UiItem.Message,
|
message: UiItem.Message,
|
||||||
animate: Boolean
|
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
@@ -81,9 +80,6 @@ fun IncomingMessageBubble(
|
|||||||
isOut = false,
|
isOut = false,
|
||||||
date = message.date,
|
date = message.date,
|
||||||
edited = message.isEdited,
|
edited = message.isEdited,
|
||||||
animate = animate,
|
|
||||||
isRead = message.isRead,
|
|
||||||
sendingStatus = message.sendingStatus
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-72
@@ -1,35 +1,19 @@
|
|||||||
package dev.meloda.fast.messageshistory.presentation
|
package dev.meloda.fast.messageshistory.presentation
|
||||||
|
|
||||||
import androidx.compose.animation.animateContentSize
|
import androidx.compose.animation.animateContentSize
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.defaultMinSize
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.rounded.Create
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.LocalContentColor
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.surfaceColorAtElevation
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import dev.meloda.fast.messageshistory.model.SendingStatus
|
|
||||||
import dev.meloda.fast.ui.R as UiR
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MessageBubble(
|
fun MessageBubble(
|
||||||
@@ -38,9 +22,6 @@ fun MessageBubble(
|
|||||||
isOut: Boolean,
|
isOut: Boolean,
|
||||||
date: String?,
|
date: String?,
|
||||||
edited: Boolean,
|
edited: Boolean,
|
||||||
animate: Boolean,
|
|
||||||
isRead: Boolean,
|
|
||||||
sendingStatus: SendingStatus
|
|
||||||
) {
|
) {
|
||||||
val backgroundColor = if (!isOut) {
|
val backgroundColor = if (!isOut) {
|
||||||
MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
||||||
@@ -64,70 +45,44 @@ fun MessageBubble(
|
|||||||
vertical = 6.dp
|
vertical = 6.dp
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
val minDateContainerWidth = remember(edited, isOut) {
|
|
||||||
val mainPart = if (edited) 50.dp else 30.dp
|
|
||||||
val readIndicatorPart = if (isOut) 14.dp else 0.dp
|
|
||||||
|
|
||||||
mainPart + readIndicatorPart
|
|
||||||
}
|
|
||||||
|
|
||||||
val dateContainerWidth by animateDpAsState(
|
|
||||||
targetValue = minDateContainerWidth,
|
|
||||||
label = "dateContainerWidth"
|
|
||||||
)
|
|
||||||
|
|
||||||
if (text != null) {
|
if (text != null) {
|
||||||
Text(
|
Text(
|
||||||
text = text,
|
text = text,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(2.dp)
|
.padding(2.dp)
|
||||||
.align(Alignment.Center)
|
.align(Alignment.Center)
|
||||||
.padding(end = 4.dp)
|
.animateContentSize(),
|
||||||
.padding(end = dateContainerWidth)
|
|
||||||
.padding(end = 4.dp)
|
|
||||||
.then(if (animate) Modifier.animateContentSize() else Modifier),
|
|
||||||
color = textColor
|
color = textColor
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.BottomEnd)
|
|
||||||
.defaultMinSize(minWidth = dateContainerWidth)
|
|
||||||
) {
|
|
||||||
if (edited) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Rounded.Create,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(14.dp)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
}
|
|
||||||
Text(
|
|
||||||
text = date.orEmpty(),
|
|
||||||
style = MaterialTheme.typography.labelSmall,
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
|
|
||||||
if (isOut) {
|
// val dateContainerWidth by animateDpAsState(
|
||||||
Icon(
|
// targetValue = if (edited) 50.dp else 30.dp,
|
||||||
modifier = Modifier.size(14.dp),
|
// label = "dateContainerWidth"
|
||||||
painter = painterResource(
|
// )
|
||||||
when (sendingStatus) {
|
|
||||||
SendingStatus.SENDING -> UiR.drawable.round_access_time_24
|
|
||||||
SendingStatus.SENT -> {
|
|
||||||
if (isRead) UiR.drawable.round_done_all_24
|
|
||||||
else UiR.drawable.ic_round_done_24
|
|
||||||
}
|
|
||||||
|
|
||||||
SendingStatus.FAILED -> UiR.drawable.round_error_outline_24
|
// AnimatedVisibility(
|
||||||
}
|
// date != null,
|
||||||
),
|
// modifier = Modifier
|
||||||
tint = if (sendingStatus == SendingStatus.FAILED) Color.Red
|
// .width(dateContainerWidth)
|
||||||
else LocalContentColor.current,
|
// .align(Alignment.BottomEnd)
|
||||||
contentDescription = null
|
// ) {
|
||||||
)
|
// Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
}
|
// if (edited) {
|
||||||
}
|
// Icon(
|
||||||
|
// imageVector = Icons.Rounded.Create,
|
||||||
|
// contentDescription = null,
|
||||||
|
// modifier = Modifier.size(14.dp)
|
||||||
|
// )
|
||||||
|
// Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
// }
|
||||||
|
// Text(
|
||||||
|
// text = date.orEmpty(),
|
||||||
|
// style = MaterialTheme.typography.labelSmall
|
||||||
|
// )
|
||||||
|
// Spacer(modifier = Modifier.width(2.dp))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-2
@@ -159,7 +159,7 @@ fun MessagesHistoryScreen(
|
|||||||
|
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
val paginationConditionMet by remember(canPaginate, listState) {
|
val paginationConditionMet by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
canPaginate &&
|
canPaginate &&
|
||||||
(listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index
|
(listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index
|
||||||
@@ -282,7 +282,6 @@ fun MessagesHistoryScreen(
|
|||||||
|
|
||||||
// TODO: 11/07/2024, Danil Nikolaev: to VM
|
// TODO: 11/07/2024, Danil Nikolaev: to VM
|
||||||
|
|
||||||
// TODO: 23-Mar-25, Danil Nikolaev: crash if not messages (ex. new chat)
|
|
||||||
onChatMaterialsDropdownItemClicked(
|
onChatMaterialsDropdownItemClicked(
|
||||||
screenState.conversationId,
|
screenState.conversationId,
|
||||||
screenState.messages.firstMessage().conversationMessageId
|
screenState.messages.firstMessage().conversationMessageId
|
||||||
|
|||||||
+5
-8
@@ -98,7 +98,6 @@ fun MessagesList(
|
|||||||
else Modifier
|
else Modifier
|
||||||
),
|
),
|
||||||
message = item,
|
message = item,
|
||||||
animate = enableAnimations
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
IncomingMessageBubble(
|
IncomingMessageBubble(
|
||||||
@@ -111,7 +110,6 @@ fun MessagesList(
|
|||||||
else Modifier
|
else Modifier
|
||||||
),
|
),
|
||||||
message = item,
|
message = item,
|
||||||
animate = enableAnimations
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,17 +128,16 @@ fun MessagesList(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(Modifier.height(8.dp))
|
|
||||||
Spacer(
|
|
||||||
modifier = Modifier
|
|
||||||
.height(64.dp)
|
|
||||||
.fillMaxWidth()
|
|
||||||
)
|
|
||||||
Spacer(
|
Spacer(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.statusBarsPadding()
|
.statusBarsPadding()
|
||||||
)
|
)
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(64.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-5
@@ -3,8 +3,12 @@ package dev.meloda.fast.messageshistory.presentation
|
|||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -16,7 +20,6 @@ import dev.meloda.fast.messageshistory.model.UiItem
|
|||||||
fun OutgoingMessageBubble(
|
fun OutgoingMessageBubble(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
message: UiItem.Message,
|
message: UiItem.Message,
|
||||||
animate: Boolean
|
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
@@ -34,12 +37,18 @@ fun OutgoingMessageBubble(
|
|||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
text = message.text.orDots(),
|
text = message.text.orDots(),
|
||||||
isOut = true,
|
isOut = true,
|
||||||
date = message.date,
|
date = null,
|
||||||
edited = message.isEdited,
|
edited = message.isEdited,
|
||||||
animate = animate,
|
)
|
||||||
isRead = message.isRead,
|
|
||||||
sendingStatus = message.sendingStatus
|
if (message.showDate) {
|
||||||
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(end = 12.dp),
|
||||||
|
text = message.date,
|
||||||
|
style = MaterialTheme.typography.labelSmall
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,11 @@ import dev.meloda.fast.messageshistory.model.UiItem
|
|||||||
|
|
||||||
fun List<UiItem>.firstMessage(): UiItem.Message = filterIsInstance<UiItem.Message>().first()
|
fun List<UiItem>.firstMessage(): UiItem.Message = filterIsInstance<UiItem.Message>().first()
|
||||||
|
|
||||||
fun List<UiItem>.firstMessageOrNull(): UiItem.Message? = filterIsInstance<UiItem.Message>().firstOrNull()
|
|
||||||
|
|
||||||
fun List<UiItem>.indexOfMessageById(messageId: Int): Int =
|
fun List<UiItem>.indexOfMessageById(messageId: Int): Int =
|
||||||
indexOfFirst { it.id == messageId }
|
indexOfFirst { it.id == messageId }
|
||||||
|
|
||||||
fun List<UiItem>.findMessageById(messageId: Int): UiItem.Message? =
|
fun List<UiItem>.findMessageById(messageId: Int): UiItem.Message =
|
||||||
firstOrNull { it.id == messageId } as UiItem.Message?
|
first { it.id == messageId } as UiItem.Message
|
||||||
|
|
||||||
fun List<UiItem>.indexOfMessageByCmId(cmId: Int): Int =
|
fun List<UiItem>.indexOfMessageByCmId(cmId: Int): Int =
|
||||||
indexOfFirst { it.cmId == cmId }
|
indexOfFirst { it.cmId == cmId }
|
||||||
|
|||||||
+4
-9
@@ -12,7 +12,6 @@ import dev.meloda.fast.common.model.parseString
|
|||||||
import dev.meloda.fast.common.provider.ResourceProvider
|
import dev.meloda.fast.common.provider.ResourceProvider
|
||||||
import dev.meloda.fast.data.UserConfig
|
import dev.meloda.fast.data.UserConfig
|
||||||
import dev.meloda.fast.data.VkMemoryCache
|
import dev.meloda.fast.data.VkMemoryCache
|
||||||
import dev.meloda.fast.messageshistory.model.SendingStatus
|
|
||||||
import dev.meloda.fast.messageshistory.model.UiItem
|
import dev.meloda.fast.messageshistory.model.UiItem
|
||||||
import dev.meloda.fast.model.api.PeerType
|
import dev.meloda.fast.model.api.PeerType
|
||||||
import dev.meloda.fast.model.api.domain.VkConversation
|
import dev.meloda.fast.model.api.domain.VkConversation
|
||||||
@@ -91,8 +90,8 @@ fun VkConversation.extractTitle(
|
|||||||
}.parseString(resources).orDots()
|
}.parseString(resources).orDots()
|
||||||
|
|
||||||
fun VkMessage.asPresentation(
|
fun VkMessage.asPresentation(
|
||||||
conversation: VkConversation,
|
|
||||||
resourceProvider: ResourceProvider,
|
resourceProvider: ResourceProvider,
|
||||||
|
showDate: Boolean,
|
||||||
showName: Boolean,
|
showName: Boolean,
|
||||||
prevMessage: VkMessage?,
|
prevMessage: VkMessage?,
|
||||||
nextMessage: VkMessage?,
|
nextMessage: VkMessage?,
|
||||||
@@ -119,19 +118,15 @@ fun VkMessage.asPresentation(
|
|||||||
randomId = randomId,
|
randomId = randomId,
|
||||||
isInChat = isPeerChat(),
|
isInChat = isPeerChat(),
|
||||||
name = extractTitle(),
|
name = extractTitle(),
|
||||||
showDate = true,
|
showDate = showDate,
|
||||||
showAvatar = extractShowAvatar(nextMessage),
|
showAvatar = extractShowAvatar(nextMessage),
|
||||||
showName = showName && extractShowName(prevMessage),
|
showName = showName && extractShowName(prevMessage),
|
||||||
avatar = extractAvatar(),
|
avatar = extractAvatar(),
|
||||||
isEdited = updateTime != null,
|
isEdited = updateTime != null
|
||||||
isRead = isRead(conversation),
|
|
||||||
sendingStatus = when {
|
|
||||||
id <= 0 -> SendingStatus.SENDING
|
|
||||||
else -> SendingStatus.SENT
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun VkMessage.extractShowAvatar(nextMessage: VkMessage?): Boolean {
|
fun VkMessage.extractShowAvatar(nextMessage: VkMessage?): Boolean {
|
||||||
if (isOut) return false
|
if (isOut) return false
|
||||||
return nextMessage == null || nextMessage.fromId != fromId
|
return nextMessage == null || nextMessage.fromId != fromId
|
||||||
|
|||||||
@@ -278,14 +278,14 @@ class SettingsViewModelImpl(
|
|||||||
)
|
)
|
||||||
val generalShowEmojiButton = SettingsItem.Switch(
|
val generalShowEmojiButton = SettingsItem.Switch(
|
||||||
key = SettingsKeys.KEY_SHOW_EMOJI_BUTTON,
|
key = SettingsKeys.KEY_SHOW_EMOJI_BUTTON,
|
||||||
title = UiText.Resource(UiR.string.settings_general_show_emoji_button_title),
|
title = UiText.Simple("Show emoji button"),
|
||||||
text = UiText.Resource(UiR.string.settings_general_show_emoji_button_summary),
|
text = UiText.Simple("Show emoji button in chat panel"),
|
||||||
defaultValue = SettingsKeys.DEFAULT_VALUE_KEY_SHOW_EMOJI_BUTTON
|
defaultValue = SettingsKeys.DEFAULT_VALUE_KEY_SHOW_EMOJI_BUTTON
|
||||||
)
|
)
|
||||||
val generalEnableHaptic = SettingsItem.Switch(
|
val generalEnableHaptic = SettingsItem.Switch(
|
||||||
key = SettingsKeys.KEY_ENABLE_HAPTIC,
|
key = SettingsKeys.KEY_ENABLE_HAPTIC,
|
||||||
defaultValue = SettingsKeys.DEFAULT_ENABLE_HAPTIC,
|
defaultValue = SettingsKeys.DEFAULT_ENABLE_HAPTIC,
|
||||||
title = UiText.Resource(UiR.string.settings_general_enable_haptic_title)
|
title = UiText.Simple("Enable haptic")
|
||||||
)
|
)
|
||||||
|
|
||||||
val appearanceTitle = SettingsItem.Title(
|
val appearanceTitle = SettingsItem.Title(
|
||||||
@@ -342,7 +342,7 @@ class SettingsViewModelImpl(
|
|||||||
val appearanceUseSystemFont = SettingsItem.Switch(
|
val appearanceUseSystemFont = SettingsItem.Switch(
|
||||||
key = SettingsKeys.KEY_USE_SYSTEM_FONT,
|
key = SettingsKeys.KEY_USE_SYSTEM_FONT,
|
||||||
defaultValue = SettingsKeys.DEFAULT_USE_SYSTEM_FONT,
|
defaultValue = SettingsKeys.DEFAULT_USE_SYSTEM_FONT,
|
||||||
title = UiText.Resource(UiR.string.settings_appearance_use_system_font_title)
|
title = UiText.Simple("Use system font")
|
||||||
)
|
)
|
||||||
val appearanceLanguage = SettingsItem.TitleText(
|
val appearanceLanguage = SettingsItem.TitleText(
|
||||||
key = SettingsKeys.KEY_APPEARANCE_LANGUAGE,
|
key = SettingsKeys.KEY_APPEARANCE_LANGUAGE,
|
||||||
@@ -379,7 +379,7 @@ class SettingsViewModelImpl(
|
|||||||
|
|
||||||
val experimentalTitle = SettingsItem.Title(
|
val experimentalTitle = SettingsItem.Title(
|
||||||
key = "experimental",
|
key = "experimental",
|
||||||
title = UiText.Resource(UiR.string.settings_experimental_title)
|
title = UiText.Simple("Experimental - VERY unstable")
|
||||||
)
|
)
|
||||||
val experimentalLongPollBackground = SettingsItem.Switch(
|
val experimentalLongPollBackground = SettingsItem.Switch(
|
||||||
key = SettingsKeys.KEY_LONG_POLL_IN_BACKGROUND,
|
key = SettingsKeys.KEY_LONG_POLL_IN_BACKGROUND,
|
||||||
@@ -390,20 +390,19 @@ class SettingsViewModelImpl(
|
|||||||
val experimentalShowTimeInActionMessages = SettingsItem.Switch(
|
val experimentalShowTimeInActionMessages = SettingsItem.Switch(
|
||||||
key = SettingsKeys.KEY_SHOW_TIME_IN_ACTION_MESSAGES,
|
key = SettingsKeys.KEY_SHOW_TIME_IN_ACTION_MESSAGES,
|
||||||
defaultValue = SettingsKeys.DEFAULT_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)
|
title = UiText.Simple("Show time in action messages")
|
||||||
)
|
)
|
||||||
val experimentalUseBlur = SettingsItem.Switch(
|
val experimentalUseBlur = SettingsItem.Switch(
|
||||||
key = SettingsKeys.KEY_USE_BLUR,
|
key = SettingsKeys.KEY_USE_BLUR,
|
||||||
defaultValue = SettingsKeys.DEFAULT_USE_BLUR,
|
defaultValue = SettingsKeys.DEFAULT_USE_BLUR,
|
||||||
title = UiText.Resource(UiR.string.settings_experimental_use_blur_title),
|
title = UiText.Simple("Use blur"),
|
||||||
text = UiText.Resource(UiR.string.settings_experimental_use_blur_summary),
|
text = UiText.Simple("Adds blur wherever possible\nWorks on android 12 and newer"),
|
||||||
isVisible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
|
||||||
)
|
)
|
||||||
val enableAnimations = SettingsItem.Switch(
|
val enableAnimations = SettingsItem.Switch(
|
||||||
key = SettingsKeys.KEY_MORE_ANIMATIONS,
|
key = SettingsKeys.KEY_MORE_ANIMATIONS,
|
||||||
defaultValue = SettingsKeys.DEFAULT_MORE_ANIMATIONS,
|
defaultValue = SettingsKeys.DEFAULT_MORE_ANIMATIONS,
|
||||||
title = UiText.Resource(UiR.string.settings_experimental_more_animations_title),
|
title = UiText.Simple("More animations"),
|
||||||
text = UiText.Resource(UiR.string.settings_experimental_more_animations_summary)
|
text = UiText.Simple("Use animations wherever possible")
|
||||||
)
|
)
|
||||||
|
|
||||||
val debugTitle = SettingsItem.Title(
|
val debugTitle = SettingsItem.Title(
|
||||||
|
|||||||
+14
-14
@@ -2,25 +2,25 @@
|
|||||||
minSdk = "23"
|
minSdk = "23"
|
||||||
targetSdk = "35"
|
targetSdk = "35"
|
||||||
compileSdk = "35"
|
compileSdk = "35"
|
||||||
versionCode = "9"
|
versionCode = "8"
|
||||||
versionName = "0.1.6"
|
versionName = "0.1.5"
|
||||||
|
|
||||||
agp = "8.9.0"
|
agp = "8.7.3"
|
||||||
converterMoshi = "2.11.0"
|
converterMoshi = "2.11.0"
|
||||||
eithernet = "2.0.0"
|
eithernet = "2.0.0"
|
||||||
haze = "1.5.1"
|
haze = "1.1.1"
|
||||||
kotlin = "2.1.10"
|
kotlin = "2.1.0"
|
||||||
ksp = "2.1.10-1.0.31"
|
ksp = "2.1.0-1.0.29"
|
||||||
|
|
||||||
compose-bom = "2025.03.00"
|
compose-bom = "2024.12.01"
|
||||||
koin = "4.0.2"
|
koin = "4.0.0"
|
||||||
|
|
||||||
accompanist = "0.37.2"
|
accompanist = "0.37.0"
|
||||||
coil = "2.7.0"
|
coil = "2.7.0"
|
||||||
coroutines = "1.10.1"
|
coroutines = "1.9.0"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
chucker = "4.1.0"
|
chucker = "4.1.0"
|
||||||
guava = "33.4.5-jre"
|
guava = "33.3.1-jre"
|
||||||
lifecycle = "2.8.7"
|
lifecycle = "2.8.7"
|
||||||
core-ktx = "1.15.0"
|
core-ktx = "1.15.0"
|
||||||
material = "1.12.0"
|
material = "1.12.0"
|
||||||
@@ -33,10 +33,10 @@ nanokt = "1.2.0"
|
|||||||
junitVersion = "1.2.1"
|
junitVersion = "1.2.1"
|
||||||
espressoCore = "3.6.1"
|
espressoCore = "3.6.1"
|
||||||
appcompat = "1.7.0"
|
appcompat = "1.7.0"
|
||||||
androidx-navigation = "2.8.9"
|
androidx-navigation = "2.8.5"
|
||||||
serialization = "1.8.0"
|
serialization = "1.7.3"
|
||||||
rebugger = "1.0.0-rc03"
|
rebugger = "1.0.0-rc03"
|
||||||
moduleGraph = "2.8.0"
|
moduleGraph = "2.7.1"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" }
|
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" }
|
||||||
|
|||||||
Vendored
BIN
Binary file not shown.
+2
-3
@@ -1,7 +1,6 @@
|
|||||||
|
#Mon Oct 28 18:41:43 MSK 2024
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
||||||
networkTimeout=10000
|
|
||||||
validateDistributionUrl=true
|
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
@@ -15,8 +15,6 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
#
|
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
#
|
#
|
||||||
@@ -57,7 +55,7 @@
|
|||||||
# Darwin, MinGW, and NonStop.
|
# Darwin, MinGW, and NonStop.
|
||||||
#
|
#
|
||||||
# (3) This script is generated from the Groovy template
|
# (3) This script is generated from the Groovy template
|
||||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
# within the Gradle project.
|
# within the Gradle project.
|
||||||
#
|
#
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
@@ -82,12 +80,13 @@ do
|
|||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
# This is normally unused
|
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||||
# shellcheck disable=SC2034
|
|
||||||
|
APP_NAME="Gradle"
|
||||||
APP_BASE_NAME=${0##*/}
|
APP_BASE_NAME=${0##*/}
|
||||||
# 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
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
' "$PWD" ) || exit
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD=maximum
|
MAX_FD=maximum
|
||||||
@@ -134,29 +133,22 @@ location of your Java installation."
|
|||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
JAVACMD=java
|
JAVACMD=java
|
||||||
if ! command -v java >/dev/null 2>&1
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
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
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
location of your Java installation."
|
location of your Java installation."
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Increase the maximum file descriptors if we can.
|
# Increase the maximum file descriptors if we can.
|
||||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
case $MAX_FD in #(
|
case $MAX_FD in #(
|
||||||
max*)
|
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 ) ||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
warn "Could not query maximum file descriptor limit"
|
warn "Could not query maximum file descriptor limit"
|
||||||
esac
|
esac
|
||||||
case $MAX_FD in #(
|
case $MAX_FD in #(
|
||||||
'' | soft) :;; #(
|
'' | 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" ||
|
ulimit -n "$MAX_FD" ||
|
||||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
esac
|
esac
|
||||||
@@ -201,15 +193,11 @@ if "$cygwin" || "$msys" ; then
|
|||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command;
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
# shell script including quotes and variable substitutions, so put them in
|
||||||
|
# double quotes to make sure that they get re-expanded; and
|
||||||
# Collect all arguments for the java command:
|
# * put everything else in single quotes, so that it's not re-expanded.
|
||||||
# * 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 -- \
|
set -- \
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
@@ -217,12 +205,6 @@ set -- \
|
|||||||
org.gradle.wrapper.GradleWrapperMain \
|
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.
|
# Use "xargs" to parse quoted args.
|
||||||
#
|
#
|
||||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user