Chat creation feature (#138)
This commit is contained in:
@@ -77,6 +77,7 @@ 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)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ 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
|
||||||
@@ -46,7 +47,8 @@ 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
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ fun NavGraphBuilder.mainScreen(
|
|||||||
onConversationClicked: (conversationId: Int) -> Unit,
|
onConversationClicked: (conversationId: Int) -> Unit,
|
||||||
onPhotoClicked: (url: String) -> Unit,
|
onPhotoClicked: (url: String) -> Unit,
|
||||||
onMessageClicked: (userId: Int) -> Unit,
|
onMessageClicked: (userId: Int) -> Unit,
|
||||||
|
onCreateChatClicked: () -> Unit,
|
||||||
viewModel: MainViewModel
|
viewModel: MainViewModel
|
||||||
) {
|
) {
|
||||||
val navigationItems = ImmutableList.of(
|
val navigationItems = ImmutableList.of(
|
||||||
@@ -56,6 +57,7 @@ fun NavGraphBuilder.mainScreen(
|
|||||||
onConversationItemClicked = onConversationClicked,
|
onConversationItemClicked = onConversationClicked,
|
||||||
onPhotoClicked = onPhotoClicked,
|
onPhotoClicked = onPhotoClicked,
|
||||||
onMessageClicked = onMessageClicked,
|
onMessageClicked = onMessageClicked,
|
||||||
|
onCreateChatClicked = onCreateChatClicked,
|
||||||
viewModel = viewModel
|
viewModel = viewModel
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ fun MainScreen(
|
|||||||
onConversationItemClicked: (conversationId: Int) -> Unit = {},
|
onConversationItemClicked: (conversationId: Int) -> Unit = {},
|
||||||
onPhotoClicked: (url: String) -> Unit = {},
|
onPhotoClicked: (url: String) -> Unit = {},
|
||||||
onMessageClicked: (userId: Int) -> Unit = {},
|
onMessageClicked: (userId: Int) -> Unit = {},
|
||||||
|
onCreateChatClicked: () -> Unit = {},
|
||||||
viewModel: MainViewModel
|
viewModel: MainViewModel
|
||||||
) {
|
) {
|
||||||
val currentTheme = LocalThemeConfig.current
|
val currentTheme = LocalThemeConfig.current
|
||||||
@@ -172,6 +173,7 @@ fun MainScreen(
|
|||||||
onError = onError,
|
onError = onError,
|
||||||
onConversationItemClicked = onConversationItemClicked,
|
onConversationItemClicked = onConversationItemClicked,
|
||||||
onPhotoClicked = onPhotoClicked,
|
onPhotoClicked = onPhotoClicked,
|
||||||
|
onCreateChatClicked = onCreateChatClicked,
|
||||||
navController = navController,
|
navController = navController,
|
||||||
)
|
)
|
||||||
profileScreen(
|
profileScreen(
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ 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
|
||||||
@@ -125,6 +127,7 @@ fun RootScreen(
|
|||||||
onConversationClicked = navController::navigateToMessagesHistory,
|
onConversationClicked = navController::navigateToMessagesHistory,
|
||||||
onPhotoClicked = { url -> navController.navigateToPhotoView(listOf(url)) },
|
onPhotoClicked = { url -> navController.navigateToPhotoView(listOf(url)) },
|
||||||
onMessageClicked = navController::navigateToMessagesHistory,
|
onMessageClicked = navController::navigateToMessagesHistory,
|
||||||
|
onCreateChatClicked = navController::navigateToCreateChat,
|
||||||
viewModel = viewModel
|
viewModel = viewModel
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -137,6 +140,13 @@ 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,
|
||||||
|
|||||||
@@ -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,6 +41,11 @@ 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(
|
||||||
|
|||||||
+19
-1
@@ -1,5 +1,6 @@
|
|||||||
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
|
||||||
@@ -14,6 +15,7 @@ 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
|
||||||
@@ -23,7 +25,6 @@ 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
|
||||||
|
|
||||||
@@ -198,6 +199,23 @@ 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,4 +20,8 @@ class GetLocalUserByIdUseCase(private val repository: UsersRepository) {
|
|||||||
|
|
||||||
emit(newState)
|
emit(newState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun proceed(userId: Int): VkUser? {
|
||||||
|
return repository.getLocalUsers(userIds = listOf(userId)).singleOrNull()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,11 @@ 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,6 +100,14 @@ 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))
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-3
@@ -1,9 +1,9 @@
|
|||||||
package dev.meloda.fast.friends.util
|
package dev.meloda.fast.domain.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,5 +16,7 @@ fun VkUser.asPresentation(
|
|||||||
fullName
|
fullName
|
||||||
},
|
},
|
||||||
onlineStatus = onlineStatus,
|
onlineStatus = onlineStatus,
|
||||||
photo400Orig = photo400Orig?.let(UiImage::Url)
|
photo400Orig = photo400Orig?.let(UiImage::Url),
|
||||||
|
firstName = firstName,
|
||||||
|
lastName = lastName
|
||||||
)
|
)
|
||||||
@@ -267,3 +267,14 @@ 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,5 +1,7 @@
|
|||||||
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
|
||||||
@@ -7,8 +9,6 @@ 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,3 +44,9 @@ 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>
|
||||||
|
)
|
||||||
|
|||||||
+8
-1
@@ -1,12 +1,13 @@
|
|||||||
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
|
||||||
@@ -49,6 +50,12 @@ 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,4 +19,5 @@ 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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
/build
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
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 {
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@@ -11,6 +11,7 @@ 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)
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package dev.meloda.fast.conversations.model
|
package dev.meloda.fast.ui.model.api
|
||||||
|
|
||||||
enum class ActionState {
|
enum class ActionState {
|
||||||
PHANTOM, CALL_IN_PROGRESS, NONE;
|
PHANTOM, CALL_IN_PROGRESS, NONE;
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
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
-1
@@ -1,4 +1,4 @@
|
|||||||
package dev.meloda.fast.conversations.model
|
package dev.meloda.fast.ui.model.api
|
||||||
|
|
||||||
data class ConversationsShowOptions(
|
data class ConversationsShowOptions(
|
||||||
val showDeleteDialog: Int?,
|
val showDeleteDialog: Int?,
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package dev.meloda.fast.conversations.model
|
package dev.meloda.fast.ui.model.api
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
+5
-1
@@ -1,11 +1,15 @@
|
|||||||
package dev.meloda.fast.friends.model
|
package dev.meloda.fast.ui.model.api
|
||||||
|
|
||||||
|
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?
|
||||||
@@ -215,4 +215,7 @@
|
|||||||
<string name="no_online_friends">Никого в сети</string>
|
<string name="no_online_friends">Никого в сети</string>
|
||||||
<string name="try_again">Попробовать ещё раз</string>
|
<string name="try_again">Попробовать ещё раз</string>
|
||||||
<string name="session_expired">Срок действия сессии истёк</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>
|
||||||
|
|||||||
@@ -280,4 +280,7 @@
|
|||||||
<string name="no_online_friends">No one is online</string>
|
<string name="no_online_friends">No one is online</string>
|
||||||
<string name="try_again">Try again</string>
|
<string name="try_again">Try again</string>
|
||||||
<string name="session_expired">Session expired</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>
|
||||||
|
|||||||
+22
-18
@@ -11,10 +11,7 @@ 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
|
||||||
@@ -29,6 +26,9 @@ 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.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -84,10 +84,7 @@ class ConversationsViewModelImpl(
|
|||||||
override val currentOffset = MutableStateFlow(0)
|
override val currentOffset = MutableStateFlow(0)
|
||||||
override val canPaginate = MutableStateFlow(false)
|
override val canPaginate = MutableStateFlow(false)
|
||||||
|
|
||||||
// TODO: 22-Dec-24, Danil Nikolaev: rewrite
|
private val useContactNames: Boolean get() = userSettings.useContactNames.value
|
||||||
private val useContactNames = {
|
|
||||||
userSettings.useContactNames.value
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPaginationConditionsMet() {
|
override fun onPaginationConditionsMet() {
|
||||||
currentOffset.update { screenState.value.conversations.size }
|
currentOffset.update { screenState.value.conversations.size }
|
||||||
@@ -191,7 +188,10 @@ class ConversationsViewModelImpl(
|
|||||||
onPinDialogDismissed()
|
onPinDialogDismissed()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionClicked(conversation: UiConversation, option: ConversationOption) {
|
override fun onOptionClicked(
|
||||||
|
conversation: UiConversation,
|
||||||
|
option: ConversationOption
|
||||||
|
) {
|
||||||
when (option) {
|
when (option) {
|
||||||
ConversationOption.Delete -> {
|
ConversationOption.Delete -> {
|
||||||
emitShowOptions { old ->
|
emitShowOptions { old ->
|
||||||
@@ -322,21 +322,25 @@ class ConversationsViewModelImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
State.Error.ConnectionError -> {
|
State.Error.ConnectionError -> {
|
||||||
baseError.setValue {
|
baseError.setValue {
|
||||||
BaseError.SimpleError(message = "Connection error")
|
BaseError.SimpleError(message = "Connection error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
State.Error.InternalError -> {
|
State.Error.InternalError -> {
|
||||||
baseError.setValue {
|
baseError.setValue {
|
||||||
BaseError.SimpleError(message = "Internal error")
|
BaseError.SimpleError(message = "Internal error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
State.Error.UnknownError -> {
|
State.Error.UnknownError -> {
|
||||||
baseError.setValue {
|
baseError.setValue {
|
||||||
BaseError.SimpleError(message = "Unknown error")
|
BaseError.SimpleError(message = "Unknown error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> Unit
|
else -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -360,7 +364,7 @@ class ConversationsViewModelImpl(
|
|||||||
conversations = newConversations.map {
|
conversations = newConversations.map {
|
||||||
it.asPresentation(
|
it.asPresentation(
|
||||||
resources = resources,
|
resources = resources,
|
||||||
useContactName = useContactNames()
|
useContactName = useContactNames
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -424,7 +428,7 @@ class ConversationsViewModelImpl(
|
|||||||
conversations = newConversations.map {
|
conversations = newConversations.map {
|
||||||
it.asPresentation(
|
it.asPresentation(
|
||||||
resources = resources,
|
resources = resources,
|
||||||
useContactName = useContactNames()
|
useContactName = useContactNames
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -475,7 +479,7 @@ class ConversationsViewModelImpl(
|
|||||||
conversations = newConversations.map {
|
conversations = newConversations.map {
|
||||||
it.asPresentation(
|
it.asPresentation(
|
||||||
resources = resources,
|
resources = resources,
|
||||||
useContactName = useContactNames()
|
useContactName = useContactNames
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -504,7 +508,7 @@ class ConversationsViewModelImpl(
|
|||||||
conversations = newConversations.map {
|
conversations = newConversations.map {
|
||||||
it.asPresentation(
|
it.asPresentation(
|
||||||
resources = resources,
|
resources = resources,
|
||||||
useContactName = useContactNames()
|
useContactName = useContactNames
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -534,7 +538,7 @@ class ConversationsViewModelImpl(
|
|||||||
conversations = newConversations.map {
|
conversations = newConversations.map {
|
||||||
it.asPresentation(
|
it.asPresentation(
|
||||||
resources = resources,
|
resources = resources,
|
||||||
useContactName = useContactNames()
|
useContactName = useContactNames
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -563,7 +567,7 @@ class ConversationsViewModelImpl(
|
|||||||
conversations = newConversations.map {
|
conversations = newConversations.map {
|
||||||
it.asPresentation(
|
it.asPresentation(
|
||||||
resources = resources,
|
resources = resources,
|
||||||
useContactName = useContactNames()
|
useContactName = useContactNames
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -609,7 +613,7 @@ class ConversationsViewModelImpl(
|
|||||||
old.copy(conversations = newConversations.map {
|
old.copy(conversations = newConversations.map {
|
||||||
it.asPresentation(
|
it.asPresentation(
|
||||||
resources = resources,
|
resources = resources,
|
||||||
useContactName = useContactNames()
|
useContactName = useContactNames
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -650,7 +654,7 @@ class ConversationsViewModelImpl(
|
|||||||
conversations = newConversations.map {
|
conversations = newConversations.map {
|
||||||
it.asPresentation(
|
it.asPresentation(
|
||||||
resources = resources,
|
resources = resources,
|
||||||
useContactName = useContactNames()
|
useContactName = useContactNames
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -701,7 +705,7 @@ class ConversationsViewModelImpl(
|
|||||||
conversations = newConversations.map {
|
conversations = newConversations.map {
|
||||||
it.asPresentation(
|
it.asPresentation(
|
||||||
resources = resources,
|
resources = resources,
|
||||||
useContactName = useContactNames()
|
useContactName = useContactNames
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -735,7 +739,7 @@ class ConversationsViewModelImpl(
|
|||||||
conversations = newConversations.map {
|
conversations = newConversations.map {
|
||||||
it.asPresentation(
|
it.asPresentation(
|
||||||
resources = resources,
|
resources = resources,
|
||||||
useContactName = useContactNames()
|
useContactName = useContactNames
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
-31
@@ -1,31 +0,0 @@
|
|||||||
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,6 +1,8 @@
|
|||||||
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(
|
||||||
|
|||||||
+2
@@ -17,6 +17,7 @@ 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,
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
) {
|
) {
|
||||||
composable<Conversations> {
|
composable<Conversations> {
|
||||||
@@ -27,6 +28,7 @@ fun NavGraphBuilder.conversationsScreen(
|
|||||||
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,
|
text = conversation.interactionText.orEmpty(),
|
||||||
color = MaterialTheme.colorScheme.primary
|
color = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
|||||||
+2
-2
@@ -22,10 +22,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
|
||||||
|
|||||||
+8
-37
@@ -2,7 +2,6 @@ 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
|
||||||
@@ -48,12 +47,10 @@ 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.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
|
||||||
@@ -62,29 +59,26 @@ 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 dev.chrisbanes.haze.hazeEffect
|
||||||
import dev.chrisbanes.haze.hazeSource
|
import dev.chrisbanes.haze.hazeSource
|
||||||
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.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
|
||||||
@@ -92,6 +86,7 @@ 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 screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||||
@@ -113,6 +108,7 @@ 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
|
||||||
)
|
)
|
||||||
@@ -132,7 +128,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 = { _, _ -> },
|
||||||
@@ -140,6 +136,7 @@ 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 = {}
|
||||||
) {
|
) {
|
||||||
@@ -284,37 +281,13 @@ 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(
|
FloatingActionButton(onClick = onCreateChatButtonClicked) {
|
||||||
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"
|
||||||
@@ -417,9 +390,7 @@ fun HandleDialogs(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showOptions.showPinDialog != null) {
|
showOptions.showPinDialog?.let { conversation ->
|
||||||
val conversation = showOptions.showPinDialog
|
|
||||||
|
|
||||||
MaterialDialog(
|
MaterialDialog(
|
||||||
onDismissRequest = viewModel::onPinDialogDismissed,
|
onDismissRequest = viewModel::onPinDialogDismissed,
|
||||||
title = stringResource(
|
title = stringResource(
|
||||||
|
|||||||
+2
-2
@@ -14,8 +14,6 @@ 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
|
||||||
@@ -24,6 +22,8 @@ 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
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
/build
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
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
@@ -0,0 +1,243 @@
|
|||||||
|
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
@@ -0,0 +1,9 @@
|
|||||||
|
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
@@ -0,0 +1,25 @@
|
|||||||
|
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
@@ -0,0 +1,36 @@
|
|||||||
|
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
@@ -0,0 +1,110 @@
|
|||||||
|
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
@@ -0,0 +1,101 @@
|
|||||||
|
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
@@ -0,0 +1,342 @@
|
|||||||
|
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
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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(
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ 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(
|
||||||
|
|||||||
@@ -22,8 +22,7 @@ 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.friends.model.UiFriend
|
import dev.meloda.fast.ui.model.api.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
|
||||||
@@ -49,8 +48,6 @@ fun FriendsList(
|
|||||||
|
|
||||||
val friends = uiFriends.toList()
|
val friends = uiFriends.toList()
|
||||||
|
|
||||||
val bottomPadding = LocalBottomPadding.current
|
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
state = listState
|
state = listState
|
||||||
|
|||||||
+3
@@ -29,6 +29,7 @@ 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
|
||||||
@@ -160,7 +161,9 @@ 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
|
||||||
|
|||||||
+1
@@ -282,6 +282,7 @@ 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
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ 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? =
|
||||||
first { it.id == messageId } as UiItem.Message
|
firstOrNull { 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 }
|
||||||
|
|||||||
+5
-1
@@ -19,13 +19,16 @@ dependencyResolutionManagement {
|
|||||||
rootProject.name = "fast-messenger"
|
rootProject.name = "fast-messenger"
|
||||||
|
|
||||||
include(":app")
|
include(":app")
|
||||||
|
|
||||||
include(":core:network")
|
include(":core:network")
|
||||||
include(":core:data")
|
include(":core:data")
|
||||||
include(":core:database")
|
include(":core:database")
|
||||||
include(":core:datastore")
|
include(":core:datastore")
|
||||||
include(":core:ui")
|
include(":core:ui")
|
||||||
include(":core:common")
|
include(":core:common")
|
||||||
|
include(":core:domain")
|
||||||
include(":core:model")
|
include(":core:model")
|
||||||
|
|
||||||
include(":feature:messageshistory")
|
include(":feature:messageshistory")
|
||||||
include(":feature:conversations")
|
include(":feature:conversations")
|
||||||
include(":feature:auth")
|
include(":feature:auth")
|
||||||
@@ -35,4 +38,5 @@ include(":feature:photoviewer")
|
|||||||
include(":feature:settings")
|
include(":feature:settings")
|
||||||
include(":feature:friends")
|
include(":feature:friends")
|
||||||
include(":feature:profile")
|
include(":feature:profile")
|
||||||
include(":core:domain")
|
include(":feature:createchat")
|
||||||
|
include(":core:presentation")
|
||||||
|
|||||||
Reference in New Issue
Block a user