Release 0.1.8 (#139)
* pagination in chat fixed * other fixes and improvements * fixed visual bug in progress bar in chat history * Refactor: Enhance conversations and friends features - In `ConversationsScreen`, removed `isNeedToScrollToTop` and `onScrolledToTop`, and refactored toolbar container color logic. Added `NoItemsView` for empty conversation lists. - In `MainGraph`, added `onMessageClicked` for navigation to message history. - In `ApiEvent`, introduced `parseOrNull` for handling unknown event types. - In `ConversationsViewModel`, removed `scrollToTop` logic and refactored error handling. - In `FriendsViewModel`, refactored error handling and introduced `onErrorConsumed` and `handleError`. - In `FriendItem`, added an icon button to initiate sending a message to a friend. - In `strings.xml`, added or updated strings for session expiration, log out, refreshing, and empty friend lists. - In `RootScreen`, added `onMessageClicked` for navigating to messages. - In `FriendsList`, added `onMessageClicked` for handling message clicks. - In `MainScreen`, removed unused `MutableSharedFlow`. - In `FriendsScreen`, added support for showing errors, added `onMessageClicked`, and replaced `hazeChild` with `hazeEffect` and `hazeSource`. - In `FriendsNavigation`, added `onMessageClicked` for handling message clicks. - In `ConversationsNavigation`, removed the unused `scrollToTopFlow` parameter. - In `ErrorView`, added text alignment. - In `NoItemsView`, added support for a button and custom text. - In `LongPollUpdatesParser`, replaced try-catch with `parseOrNull`. * Chat creation feature (#138) * - read indicator, edit status and time for message in messages history * message sending status
This commit is contained in:
+28
-47
@@ -11,10 +11,7 @@ import dev.meloda.fast.common.extensions.createTimerFlow
|
||||
import dev.meloda.fast.common.extensions.findWithIndex
|
||||
import dev.meloda.fast.common.extensions.listenValue
|
||||
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.ConversationsShowOptions
|
||||
import dev.meloda.fast.conversations.model.UiConversation
|
||||
import dev.meloda.fast.conversations.util.asPresentation
|
||||
import dev.meloda.fast.conversations.util.extractAvatar
|
||||
import dev.meloda.fast.data.State
|
||||
@@ -29,10 +26,11 @@ import dev.meloda.fast.model.InteractionType
|
||||
import dev.meloda.fast.model.LongPollEvent
|
||||
import dev.meloda.fast.model.api.domain.VkConversation
|
||||
import dev.meloda.fast.network.VkErrorCode
|
||||
import dev.meloda.fast.ui.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 kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -40,7 +38,6 @@ import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
interface ConversationsViewModel {
|
||||
@@ -49,7 +46,6 @@ interface ConversationsViewModel {
|
||||
val baseError: StateFlow<BaseError?>
|
||||
val currentOffset: StateFlow<Int>
|
||||
val canPaginate: StateFlow<Boolean>
|
||||
val scrollToTop: StateFlow<Boolean>
|
||||
|
||||
fun onPaginationConditionsMet()
|
||||
|
||||
@@ -70,10 +66,6 @@ interface ConversationsViewModel {
|
||||
|
||||
fun setScrollIndex(index: Int)
|
||||
fun setScrollOffset(offset: Int)
|
||||
|
||||
|
||||
fun setScrollToTopFlow(scrollToTopFlow: Flow<Int>)
|
||||
fun onScrolledToTop()
|
||||
}
|
||||
|
||||
class ConversationsViewModelImpl(
|
||||
@@ -91,12 +83,8 @@ class ConversationsViewModelImpl(
|
||||
override val baseError = MutableStateFlow<BaseError?>(null)
|
||||
override val currentOffset = MutableStateFlow(0)
|
||||
override val canPaginate = MutableStateFlow(false)
|
||||
override val scrollToTop = MutableStateFlow(false)
|
||||
|
||||
// TODO: 22-Dec-24, Danil Nikolaev: rewrite
|
||||
private val useContactNames = {
|
||||
userSettings.useContactNames.value
|
||||
}
|
||||
private val useContactNames: Boolean get() = userSettings.useContactNames.value
|
||||
|
||||
override fun onPaginationConditionsMet() {
|
||||
currentOffset.update { screenState.value.conversations.size }
|
||||
@@ -134,7 +122,7 @@ class ConversationsViewModelImpl(
|
||||
}
|
||||
|
||||
override fun onRefresh() {
|
||||
baseError.setValue { null }
|
||||
onErrorConsumed()
|
||||
loadConversations(offset = 0)
|
||||
}
|
||||
|
||||
@@ -177,11 +165,11 @@ class ConversationsViewModelImpl(
|
||||
conversations = old.conversations.map { item ->
|
||||
item.copy(
|
||||
isExpanded =
|
||||
if (item.id == conversation.id) {
|
||||
!item.isExpanded
|
||||
} else {
|
||||
false
|
||||
},
|
||||
if (item.id == conversation.id) {
|
||||
!item.isExpanded
|
||||
} else {
|
||||
false
|
||||
},
|
||||
options = ImmutableList.copyOf(options)
|
||||
)
|
||||
}
|
||||
@@ -200,7 +188,10 @@ class ConversationsViewModelImpl(
|
||||
onPinDialogDismissed()
|
||||
}
|
||||
|
||||
override fun onOptionClicked(conversation: UiConversation, option: ConversationOption) {
|
||||
override fun onOptionClicked(
|
||||
conversation: UiConversation,
|
||||
option: ConversationOption
|
||||
) {
|
||||
when (option) {
|
||||
ConversationOption.Delete -> {
|
||||
emitShowOptions { old ->
|
||||
@@ -237,20 +228,6 @@ class ConversationsViewModelImpl(
|
||||
screenState.setValue { old -> old.copy(scrollOffset = offset) }
|
||||
}
|
||||
|
||||
override fun setScrollToTopFlow(scrollToTopFlow: Flow<Int>) {
|
||||
scrollToTopFlow.listenValue(viewModelScope) { index ->
|
||||
if (index == 1) {
|
||||
scrollToTop.emit(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onScrolledToTop() {
|
||||
viewModelScope.launch(Dispatchers.Main) {
|
||||
scrollToTop.emit(false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideOptions(conversationId: Int) {
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
@@ -345,21 +322,25 @@ class ConversationsViewModelImpl(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -383,7 +364,7 @@ class ConversationsViewModelImpl(
|
||||
conversations = newConversations.map {
|
||||
it.asPresentation(
|
||||
resources = resources,
|
||||
useContactName = useContactNames()
|
||||
useContactName = useContactNames
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -447,7 +428,7 @@ class ConversationsViewModelImpl(
|
||||
conversations = newConversations.map {
|
||||
it.asPresentation(
|
||||
resources = resources,
|
||||
useContactName = useContactNames()
|
||||
useContactName = useContactNames
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -498,7 +479,7 @@ class ConversationsViewModelImpl(
|
||||
conversations = newConversations.map {
|
||||
it.asPresentation(
|
||||
resources = resources,
|
||||
useContactName = useContactNames()
|
||||
useContactName = useContactNames
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -527,7 +508,7 @@ class ConversationsViewModelImpl(
|
||||
conversations = newConversations.map {
|
||||
it.asPresentation(
|
||||
resources = resources,
|
||||
useContactName = useContactNames()
|
||||
useContactName = useContactNames
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -557,7 +538,7 @@ class ConversationsViewModelImpl(
|
||||
conversations = newConversations.map {
|
||||
it.asPresentation(
|
||||
resources = resources,
|
||||
useContactName = useContactNames()
|
||||
useContactName = useContactNames
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -586,7 +567,7 @@ class ConversationsViewModelImpl(
|
||||
conversations = newConversations.map {
|
||||
it.asPresentation(
|
||||
resources = resources,
|
||||
useContactName = useContactNames()
|
||||
useContactName = useContactNames
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -632,7 +613,7 @@ class ConversationsViewModelImpl(
|
||||
old.copy(conversations = newConversations.map {
|
||||
it.asPresentation(
|
||||
resources = resources,
|
||||
useContactName = useContactNames()
|
||||
useContactName = useContactNames
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -673,7 +654,7 @@ class ConversationsViewModelImpl(
|
||||
conversations = newConversations.map {
|
||||
it.asPresentation(
|
||||
resources = resources,
|
||||
useContactName = useContactNames()
|
||||
useContactName = useContactNames
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -724,7 +705,7 @@ class ConversationsViewModelImpl(
|
||||
conversations = newConversations.map {
|
||||
it.asPresentation(
|
||||
resources = resources,
|
||||
useContactName = useContactNames()
|
||||
useContactName = useContactNames
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -758,7 +739,7 @@ class ConversationsViewModelImpl(
|
||||
conversations = newConversations.map {
|
||||
it.asPresentation(
|
||||
resources = resources,
|
||||
useContactName = useContactNames()
|
||||
useContactName = useContactNames
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
package dev.meloda.fast.conversations.model
|
||||
|
||||
enum class ActionState {
|
||||
PHANTOM, CALL_IN_PROGRESS, NONE;
|
||||
|
||||
// TODO: 11/04/2024, Danil Nikolaev: implement
|
||||
fun getResourceId(): Int {
|
||||
return -1
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun parse(isPhantom: Boolean, isCallInProgress: Boolean): ActionState {
|
||||
return when {
|
||||
isPhantom -> PHANTOM
|
||||
isCallInProgress -> CALL_IN_PROGRESS
|
||||
else -> NONE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-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
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import dev.meloda.fast.ui.model.api.ConversationsShowOptions
|
||||
import dev.meloda.fast.ui.model.api.UiConversation
|
||||
|
||||
@Immutable
|
||||
data class ConversationsScreenState(
|
||||
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
package dev.meloda.fast.conversations.model
|
||||
|
||||
data class ConversationsShowOptions(
|
||||
val showDeleteDialog: Int?,
|
||||
val showPinDialog: UiConversation?
|
||||
) {
|
||||
|
||||
companion object {
|
||||
val EMPTY: ConversationsShowOptions = ConversationsShowOptions(
|
||||
showDeleteDialog = null,
|
||||
showPinDialog = null
|
||||
)
|
||||
}
|
||||
}
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
package dev.meloda.fast.conversations.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import dev.meloda.fast.common.model.UiImage
|
||||
import dev.meloda.fast.model.api.PeerType
|
||||
import dev.meloda.fast.model.api.domain.VkMessage
|
||||
import dev.meloda.fast.ui.util.ImmutableList
|
||||
|
||||
@Immutable
|
||||
data class UiConversation(
|
||||
val id: Int,
|
||||
val lastMessageId: Int?,
|
||||
val avatar: UiImage?,
|
||||
val title: String,
|
||||
val unreadCount: String?,
|
||||
val date: String,
|
||||
val message: AnnotatedString,
|
||||
val attachmentImage: UiImage?,
|
||||
val isPinned: Boolean,
|
||||
val actionImageId: Int,
|
||||
val isBirthday: Boolean,
|
||||
val isUnread: Boolean,
|
||||
val isAccount: Boolean,
|
||||
val isOnline: Boolean,
|
||||
val lastMessage: VkMessage?,
|
||||
val peerType: PeerType,
|
||||
val interactionText: String?,
|
||||
val isExpanded: Boolean,
|
||||
val options: ImmutableList<ConversationOption>,
|
||||
)
|
||||
+2
-3
@@ -8,7 +8,6 @@ import dev.meloda.fast.conversations.ConversationsViewModelImpl
|
||||
import dev.meloda.fast.conversations.presentation.ConversationsRoute
|
||||
import dev.meloda.fast.model.BaseError
|
||||
import dev.meloda.fast.ui.extensions.sharedViewModel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@@ -18,18 +17,18 @@ fun NavGraphBuilder.conversationsScreen(
|
||||
onError: (BaseError) -> Unit,
|
||||
onConversationItemClicked: (id: Int) -> Unit,
|
||||
onPhotoClicked: (url: String) -> Unit,
|
||||
scrollToTopFlow: Flow<Int>,
|
||||
onCreateChatClicked: () -> Unit,
|
||||
navController: NavController,
|
||||
) {
|
||||
composable<Conversations> {
|
||||
val viewModel: ConversationsViewModel =
|
||||
it.sharedViewModel<ConversationsViewModelImpl>(navController = navController)
|
||||
viewModel.setScrollToTopFlow(scrollToTopFlow)
|
||||
|
||||
ConversationsRoute(
|
||||
onError = onError,
|
||||
onConversationItemClicked = onConversationItemClicked,
|
||||
onConversationPhotoClicked = onPhotoClicked,
|
||||
onCreateChatButtonClicked = onCreateChatClicked,
|
||||
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.sp
|
||||
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.LocalContentAlpha
|
||||
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.getResourcePainter
|
||||
import dev.meloda.fast.ui.util.getString
|
||||
@@ -256,7 +256,7 @@ fun ConversationItem(
|
||||
Row {
|
||||
if (conversation.interactionText != null) {
|
||||
Text(
|
||||
text = conversation.interactionText,
|
||||
text = conversation.interactionText.orEmpty(),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
|
||||
+5
-9
@@ -5,7 +5,6 @@ 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
|
||||
@@ -23,10 +22,10 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
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.UiConversation
|
||||
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 kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -83,8 +82,7 @@ fun ConversationsList(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.animateItem(fadeInSpec = null, fadeOutSpec = null)
|
||||
.navigationBarsPadding(),
|
||||
.animateItem(fadeInSpec = null, fadeOutSpec = null),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
if (screenState.isPaginating) {
|
||||
@@ -107,11 +105,9 @@ fun ConversationsList(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(bottomPadding))
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+29
-63
@@ -2,7 +2,6 @@ package dev.meloda.fast.conversations.presentation
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
@@ -48,12 +47,10 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -62,28 +59,26 @@ import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.view.HapticFeedbackConstantsCompat
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import dev.chrisbanes.haze.haze
|
||||
import dev.chrisbanes.haze.hazeChild
|
||||
import dev.chrisbanes.haze.hazeEffect
|
||||
import dev.chrisbanes.haze.hazeSource
|
||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||
import dev.chrisbanes.haze.materials.HazeMaterials
|
||||
import dev.meloda.fast.conversations.ConversationsViewModel
|
||||
import dev.meloda.fast.conversations.model.ConversationOption
|
||||
import dev.meloda.fast.conversations.model.ConversationsScreenState
|
||||
import dev.meloda.fast.conversations.model.UiConversation
|
||||
import dev.meloda.fast.datastore.AppSettings
|
||||
import dev.meloda.fast.model.BaseError
|
||||
import dev.meloda.fast.ui.components.ErrorView
|
||||
import dev.meloda.fast.ui.components.FullScreenLoader
|
||||
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.LocalHazeState
|
||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||
import dev.meloda.fast.ui.util.isScrollingUp
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.launch
|
||||
import dev.meloda.fast.ui.R as UiR
|
||||
|
||||
@Composable
|
||||
@@ -91,12 +86,12 @@ fun ConversationsRoute(
|
||||
onError: (BaseError) -> Unit,
|
||||
onConversationItemClicked: (conversationId: Int) -> Unit,
|
||||
onConversationPhotoClicked: (url: String) -> Unit,
|
||||
onCreateChatButtonClicked: () -> Unit,
|
||||
viewModel: ConversationsViewModel
|
||||
) {
|
||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||
val baseError by viewModel.baseError.collectAsStateWithLifecycle()
|
||||
val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle()
|
||||
val isNeedToScrollToTop by viewModel.scrollToTop.collectAsStateWithLifecycle()
|
||||
|
||||
ConversationsScreen(
|
||||
screenState = screenState,
|
||||
@@ -113,10 +108,9 @@ fun ConversationsRoute(
|
||||
onRefreshDropdownItemClicked = viewModel::onRefresh,
|
||||
onRefresh = viewModel::onRefresh,
|
||||
onConversationPhotoClicked = onConversationPhotoClicked,
|
||||
onCreateChatButtonClicked = onCreateChatButtonClicked,
|
||||
setScrollIndex = viewModel::setScrollIndex,
|
||||
setScrollOffset = viewModel::setScrollOffset,
|
||||
isNeedToScrollToTop = isNeedToScrollToTop,
|
||||
onScrolledToTop = viewModel::onScrolledToTop
|
||||
setScrollOffset = viewModel::setScrollOffset
|
||||
)
|
||||
|
||||
HandleDialogs(
|
||||
@@ -134,7 +128,7 @@ fun ConversationsScreen(
|
||||
screenState: ConversationsScreenState = ConversationsScreenState.EMPTY,
|
||||
baseError: BaseError? = null,
|
||||
canPaginate: Boolean = false,
|
||||
onSessionExpiredLogOutButtonClicked: () -> Unit,
|
||||
onSessionExpiredLogOutButtonClicked: () -> Unit = {},
|
||||
onConversationItemClicked: (conversationId: Int) -> Unit = {},
|
||||
onConversationItemLongClicked: (conversation: UiConversation) -> Unit = {},
|
||||
onOptionClicked: (UiConversation, ConversationOption) -> Unit = { _, _ -> },
|
||||
@@ -142,10 +136,9 @@ fun ConversationsScreen(
|
||||
onRefreshDropdownItemClicked: () -> Unit = {},
|
||||
onRefresh: () -> Unit = {},
|
||||
onConversationPhotoClicked: (url: String) -> Unit = {},
|
||||
onCreateChatButtonClicked: () -> Unit = {},
|
||||
setScrollIndex: (Int) -> Unit = {},
|
||||
setScrollOffset: (Int) -> Unit = {},
|
||||
isNeedToScrollToTop: Boolean = false,
|
||||
onScrolledToTop: () -> Unit = {}
|
||||
setScrollOffset: (Int) -> Unit = {}
|
||||
) {
|
||||
val view = LocalView.current
|
||||
val currentTheme = LocalThemeConfig.current
|
||||
@@ -159,14 +152,6 @@ fun ConversationsScreen(
|
||||
initialFirstVisibleItemScrollOffset = screenState.scrollOffset
|
||||
)
|
||||
|
||||
LaunchedEffect(isNeedToScrollToTop) {
|
||||
if (isNeedToScrollToTop) {
|
||||
listState.scrollToItem(0)
|
||||
onScrolledToTop()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(listState) {
|
||||
snapshotFlow { listState.firstVisibleItemIndex }
|
||||
.debounce(500L)
|
||||
@@ -207,10 +192,10 @@ fun ConversationsScreen(
|
||||
|
||||
val toolbarContainerColor by animateColorAsState(
|
||||
targetValue =
|
||||
if (currentTheme.enableBlur || !listState.canScrollBackward)
|
||||
MaterialTheme.colorScheme.surface
|
||||
else
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp),
|
||||
if (currentTheme.enableBlur || !listState.canScrollBackward)
|
||||
MaterialTheme.colorScheme.surface
|
||||
else
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp),
|
||||
label = "toolbarColorAlpha",
|
||||
animationSpec = tween(durationMillis = 50)
|
||||
)
|
||||
@@ -275,7 +260,7 @@ fun ConversationsScreen(
|
||||
modifier = Modifier
|
||||
.then(
|
||||
if (currentTheme.enableBlur) {
|
||||
Modifier.hazeChild(
|
||||
Modifier.hazeEffect(
|
||||
state = hazeState,
|
||||
style = HazeMaterials.thick()
|
||||
)
|
||||
@@ -296,37 +281,13 @@ fun ConversationsScreen(
|
||||
}
|
||||
},
|
||||
floatingActionButton = {
|
||||
val scope = rememberCoroutineScope()
|
||||
val rotation = remember { Animatable(0f) }
|
||||
|
||||
Column {
|
||||
AnimatedVisibility(
|
||||
visible = listState.isScrollingUp(),
|
||||
enter = slideIn { IntOffset(0, 600) } + fadeIn(tween(200)),
|
||||
exit = slideOut { IntOffset(0, 600) } + fadeOut(tween(200))
|
||||
) {
|
||||
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)
|
||||
) {
|
||||
FloatingActionButton(onClick = onCreateChatButtonClicked) {
|
||||
Icon(
|
||||
painter = painterResource(id = UiR.drawable.ic_baseline_create_24),
|
||||
contentDescription = "Add chat button"
|
||||
@@ -343,8 +304,8 @@ fun ConversationsScreen(
|
||||
when (baseError) {
|
||||
is BaseError.SessionExpired -> {
|
||||
ErrorView(
|
||||
text = "Session expired",
|
||||
buttonText = "Log out",
|
||||
text = stringResource(UiR.string.session_expired),
|
||||
buttonText = stringResource(UiR.string.action_log_out),
|
||||
onButtonClick = onSessionExpiredLogOutButtonClicked
|
||||
)
|
||||
}
|
||||
@@ -352,7 +313,7 @@ fun ConversationsScreen(
|
||||
is BaseError.SimpleError -> {
|
||||
ErrorView(
|
||||
text = baseError.message,
|
||||
buttonText = "Try again",
|
||||
buttonText = stringResource(UiR.string.try_again),
|
||||
onButtonClick = onRefresh
|
||||
)
|
||||
}
|
||||
@@ -390,7 +351,7 @@ fun ConversationsScreen(
|
||||
state = listState,
|
||||
maxLines = maxLines,
|
||||
modifier = if (currentTheme.enableBlur) {
|
||||
Modifier.haze(state = hazeState)
|
||||
Modifier.hazeSource(state = hazeState)
|
||||
} else {
|
||||
Modifier
|
||||
}.fillMaxSize(),
|
||||
@@ -398,6 +359,13 @@ fun ConversationsScreen(
|
||||
padding = padding,
|
||||
onPhotoClicked = onConversationPhotoClicked
|
||||
)
|
||||
|
||||
if (screenState.conversations.isEmpty()) {
|
||||
NoItemsView(
|
||||
buttonText = stringResource(UiR.string.action_refresh),
|
||||
onButtonClick = onRefresh
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -422,9 +390,7 @@ fun HandleDialogs(
|
||||
)
|
||||
}
|
||||
|
||||
if (showOptions.showPinDialog != null) {
|
||||
val conversation = showOptions.showPinDialog
|
||||
|
||||
showOptions.showPinDialog?.let { conversation ->
|
||||
MaterialDialog(
|
||||
onDismissRequest = viewModel::onPinDialogDismissed,
|
||||
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.parseString
|
||||
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.VkMemoryCache
|
||||
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.VkConversation
|
||||
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 java.util.Calendar
|
||||
import java.util.Locale
|
||||
|
||||
Reference in New Issue
Block a user