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:
@@ -9,8 +9,8 @@ import dev.meloda.fast.data.processState
|
||||
import dev.meloda.fast.datastore.UserSettings
|
||||
import dev.meloda.fast.domain.FriendsUseCase
|
||||
import dev.meloda.fast.domain.LoadUsersByIdsUseCase
|
||||
import dev.meloda.fast.domain.util.asPresentation
|
||||
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.api.domain.VkUser
|
||||
import dev.meloda.fast.network.VkErrorCode
|
||||
@@ -68,6 +68,7 @@ class FriendsViewModelImpl(
|
||||
}
|
||||
|
||||
override fun onRefresh() {
|
||||
onErrorConsumed()
|
||||
loadFriends(offset = 0)
|
||||
}
|
||||
|
||||
@@ -99,32 +100,12 @@ class FriendsViewModelImpl(
|
||||
friendsUseCase.getOnlineFriends(null, null)
|
||||
.listenValue(viewModelScope) { state ->
|
||||
state.processState(
|
||||
error = { error ->
|
||||
if (error is State.Error.ApiError) {
|
||||
when (error.errorCode) {
|
||||
VkErrorCode.USER_AUTHORIZATION_FAILED -> {
|
||||
baseError.setValue { BaseError.SessionExpired }
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
},
|
||||
error = ::handleError,
|
||||
success = { userIds ->
|
||||
loadUsersByIdsUseCase(userIds = userIds)
|
||||
.listenValue(viewModelScope) { state ->
|
||||
state.processState(
|
||||
error = { error ->
|
||||
if (error is State.Error.ApiError) {
|
||||
when (error.errorCode) {
|
||||
VkErrorCode.USER_AUTHORIZATION_FAILED -> {
|
||||
baseError.setValue { BaseError.SessionExpired }
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
},
|
||||
error = ::handleError,
|
||||
success = { onlineFriends ->
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
@@ -142,17 +123,7 @@ class FriendsViewModelImpl(
|
||||
friendsUseCase.getFriends(count = LOAD_COUNT, offset = offset)
|
||||
.listenValue(viewModelScope) { state ->
|
||||
state.processState(
|
||||
error = { error ->
|
||||
if (error is State.Error.ApiError) {
|
||||
when (error.errorCode) {
|
||||
VkErrorCode.USER_AUTHORIZATION_FAILED -> {
|
||||
baseError.setValue { BaseError.SessionExpired }
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
},
|
||||
error = ::handleError,
|
||||
success = { response ->
|
||||
val itemsCountSufficient = response.size == LOAD_COUNT
|
||||
canPaginate.setValue { itemsCountSufficient }
|
||||
@@ -197,6 +168,40 @@ class FriendsViewModelImpl(
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleError(error: State.Error) {
|
||||
when (error) {
|
||||
is State.Error.ApiError -> {
|
||||
when (error.errorCode) {
|
||||
VkErrorCode.USER_AUTHORIZATION_FAILED -> {
|
||||
baseError.setValue { BaseError.SessionExpired }
|
||||
}
|
||||
|
||||
else -> {
|
||||
baseError.setValue {
|
||||
BaseError.SimpleError(message = error.errorMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
State.Error.ConnectionError -> {
|
||||
baseError.setValue {
|
||||
BaseError.SimpleError(message = "Connection error")
|
||||
}
|
||||
}
|
||||
State.Error.InternalError -> {
|
||||
baseError.setValue {
|
||||
BaseError.SimpleError(message = "Internal error")
|
||||
}
|
||||
}
|
||||
State.Error.UnknownError -> {
|
||||
baseError.setValue {
|
||||
BaseError.SimpleError(message = "Unknown error")
|
||||
}
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateFriendsNames(useContactNames: Boolean) {
|
||||
val friends = friends.value
|
||||
if (friends.isEmpty()) return
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package dev.meloda.fast.friends.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import dev.meloda.fast.ui.model.api.UiFriend
|
||||
|
||||
@Immutable
|
||||
data class FriendsScreenState(
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
package dev.meloda.fast.friends.model
|
||||
|
||||
import dev.meloda.fast.common.model.UiImage
|
||||
import dev.meloda.fast.model.api.domain.OnlineStatus
|
||||
|
||||
data class UiFriend(
|
||||
val userId: Int,
|
||||
val avatar: UiImage?,
|
||||
val title: String,
|
||||
val onlineStatus: OnlineStatus,
|
||||
val photo400Orig: UiImage?
|
||||
)
|
||||
+4
-2
@@ -16,7 +16,8 @@ object Friends
|
||||
fun NavGraphBuilder.friendsScreen(
|
||||
onError: (BaseError) -> Unit,
|
||||
navController: NavController,
|
||||
onPhotoClicked: (url: String) -> Unit
|
||||
onPhotoClicked: (url: String) -> Unit,
|
||||
onMessageClicked: (userId: Int) -> Unit
|
||||
) {
|
||||
composable<Friends> {
|
||||
val viewModel: FriendsViewModel =
|
||||
@@ -25,7 +26,8 @@ fun NavGraphBuilder.friendsScreen(
|
||||
FriendsRoute(
|
||||
onError = onError,
|
||||
viewModel = viewModel,
|
||||
onPhotoClicked = onPhotoClicked
|
||||
onPhotoClicked = onPhotoClicked,
|
||||
onMessageClicked = onMessageClicked
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,10 @@ 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.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.MailOutline
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -23,15 +27,16 @@ 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.friends.model.UiFriend
|
||||
import dev.meloda.fast.ui.R
|
||||
import dev.meloda.fast.ui.model.api.UiFriend
|
||||
|
||||
@Composable
|
||||
fun FriendItem(
|
||||
modifier: Modifier = Modifier,
|
||||
friend: UiFriend,
|
||||
maxLines: Int,
|
||||
onPhotoClicked: (url: String) -> Unit
|
||||
onPhotoClicked: (url: String) -> Unit,
|
||||
onMessageClicked: (userId: Int) -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
@@ -92,9 +97,24 @@ fun FriendItem(
|
||||
text = friend.title,
|
||||
minLines = 1,
|
||||
maxLines = maxLines,
|
||||
style = MaterialTheme.typography.headlineSmall.copy(fontSize = 20.sp)
|
||||
style = MaterialTheme.typography.headlineSmall.copy(fontSize = 20.sp),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
||||
IconButton(
|
||||
onClick = {
|
||||
onMessageClicked(friend.userId)
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.MailOutline,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
}
|
||||
}
|
||||
|
||||
+7
-12
@@ -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,8 +22,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.meloda.fast.friends.model.FriendsScreenState
|
||||
import dev.meloda.fast.friends.model.UiFriend
|
||||
import dev.meloda.fast.ui.theme.LocalBottomPadding
|
||||
import dev.meloda.fast.ui.model.api.UiFriend
|
||||
import dev.meloda.fast.ui.util.ImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -38,6 +36,7 @@ fun FriendsList(
|
||||
maxLines: Int,
|
||||
padding: PaddingValues,
|
||||
onPhotoClicked: (url: String) -> Unit,
|
||||
onMessageClicked: (userId: Int) -> Unit,
|
||||
setCanScrollBackward: (Boolean) -> Unit
|
||||
) {
|
||||
LaunchedEffect(listState) {
|
||||
@@ -49,8 +48,6 @@ fun FriendsList(
|
||||
|
||||
val friends = uiFriends.toList()
|
||||
|
||||
val bottomPadding = LocalBottomPadding.current
|
||||
|
||||
LazyColumn(
|
||||
modifier = modifier,
|
||||
state = listState
|
||||
@@ -67,7 +64,8 @@ fun FriendsList(
|
||||
FriendItem(
|
||||
friend = friend,
|
||||
maxLines = maxLines,
|
||||
onPhotoClicked = onPhotoClicked
|
||||
onPhotoClicked = onPhotoClicked,
|
||||
onMessageClicked = onMessageClicked
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
@@ -77,8 +75,7 @@ fun FriendsList(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.animateItem(fadeInSpec = null, fadeOutSpec = null)
|
||||
.navigationBarsPadding(),
|
||||
.animateItem(fadeInSpec = null, fadeOutSpec = null),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
if (screenState.isPaginating) {
|
||||
@@ -101,11 +98,9 @@ fun FriendsList(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(bottomPadding))
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+37
-20
@@ -48,8 +48,8 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import coil.imageLoader
|
||||
import coil.request.ImageRequest
|
||||
import dev.chrisbanes.haze.haze
|
||||
import dev.chrisbanes.haze.hazeChild
|
||||
import dev.chrisbanes.haze.hazeEffect
|
||||
import dev.chrisbanes.haze.hazeSource
|
||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||
import dev.chrisbanes.haze.materials.HazeMaterials
|
||||
import dev.meloda.fast.friends.FriendsViewModel
|
||||
@@ -72,6 +72,7 @@ import dev.meloda.fast.ui.R as UiR
|
||||
fun FriendsRoute(
|
||||
onError: (BaseError) -> Unit,
|
||||
onPhotoClicked: (url: String) -> Unit,
|
||||
onMessageClicked: (userId: Int) -> Unit,
|
||||
viewModel: FriendsViewModel = koinViewModel<FriendsViewModelImpl>()
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
@@ -99,11 +100,12 @@ fun FriendsRoute(
|
||||
onPaginationConditionsMet = viewModel::onPaginationConditionsMet,
|
||||
onRefresh = viewModel::onRefresh,
|
||||
onPhotoClicked = onPhotoClicked,
|
||||
onMessageClicked = onMessageClicked,
|
||||
setSelectedTabIndex = viewModel::onTabSelected,
|
||||
setScrollIndex = viewModel::setScrollIndex,
|
||||
setScrollOffset = viewModel::setScrollOffset,
|
||||
setScrollIndexOnline = viewModel::setScrollIndexOnline,
|
||||
setScrollOffsetOnline = viewModel::setScrollOffsetOnline,
|
||||
setScrollOffsetOnline = viewModel::setScrollOffsetOnline
|
||||
)
|
||||
}
|
||||
|
||||
@@ -120,11 +122,12 @@ fun FriendsScreen(
|
||||
onPaginationConditionsMet: () -> Unit = {},
|
||||
onRefresh: () -> Unit = {},
|
||||
onPhotoClicked: (url: String) -> Unit = {},
|
||||
onMessageClicked: (userId: Int) -> Unit = {},
|
||||
setSelectedTabIndex: (Int) -> Unit = {},
|
||||
setScrollIndex: (Int) -> Unit = {},
|
||||
setScrollOffset: (Int) -> Unit = {},
|
||||
setScrollIndexOnline: (Int) -> Unit = {},
|
||||
setScrollOffsetOnline: (Int) -> Unit = {},
|
||||
setScrollOffsetOnline: (Int) -> Unit = {}
|
||||
) {
|
||||
val currentTheme = LocalThemeConfig.current
|
||||
|
||||
@@ -231,7 +234,7 @@ fun FriendsScreen(
|
||||
modifier = Modifier
|
||||
.then(
|
||||
if (currentTheme.enableBlur) {
|
||||
Modifier.hazeChild(
|
||||
Modifier.hazeEffect(
|
||||
state = hazeState,
|
||||
style = HazeMaterials.thick()
|
||||
)
|
||||
@@ -281,12 +284,24 @@ fun FriendsScreen(
|
||||
}
|
||||
) { padding ->
|
||||
when {
|
||||
baseError is BaseError.SessionExpired -> {
|
||||
ErrorView(
|
||||
text = "Session expired",
|
||||
buttonText = "Log out",
|
||||
onButtonClick = onSessionExpiredLogOutButtonClicked
|
||||
)
|
||||
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()
|
||||
@@ -333,15 +348,17 @@ fun FriendsScreen(
|
||||
)
|
||||
}
|
||||
) {
|
||||
val friendsToDisplay = if (index == 0) {
|
||||
screenState.friends
|
||||
} else {
|
||||
screenState.onlineFriends
|
||||
val friendsToDisplay = remember(index) {
|
||||
if (index == 0) {
|
||||
screenState.friends
|
||||
} else {
|
||||
screenState.onlineFriends
|
||||
}
|
||||
}
|
||||
|
||||
FriendsList(
|
||||
modifier = if (currentTheme.enableBlur) {
|
||||
Modifier.haze(state = hazeState)
|
||||
Modifier.hazeSource(state = hazeState)
|
||||
} else {
|
||||
Modifier
|
||||
}.fillMaxSize(),
|
||||
@@ -351,6 +368,7 @@ fun FriendsScreen(
|
||||
maxLines = maxLines,
|
||||
padding = padding,
|
||||
onPhotoClicked = onPhotoClicked,
|
||||
onMessageClicked = onMessageClicked,
|
||||
setCanScrollBackward = { can ->
|
||||
canScrollBackward = can
|
||||
}
|
||||
@@ -358,10 +376,9 @@ fun FriendsScreen(
|
||||
|
||||
if (friendsToDisplay.isEmpty()) {
|
||||
NoItemsView(
|
||||
modifier = Modifier
|
||||
.padding(padding.calculateTopPadding())
|
||||
.padding(top = 16.dp),
|
||||
customText = "No${if (index == 1) " online" else ""} friends :("
|
||||
customText = if (index == 1) stringResource(UiR.string.no_online_friends) else null,
|
||||
buttonText = stringResource(UiR.string.action_refresh),
|
||||
onButtonClick = onRefresh
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package dev.meloda.fast.friends.util
|
||||
|
||||
import dev.meloda.fast.common.model.UiImage
|
||||
import dev.meloda.fast.data.VkMemoryCache
|
||||
import dev.meloda.fast.friends.model.UiFriend
|
||||
import dev.meloda.fast.model.api.domain.VkUser
|
||||
|
||||
fun VkUser.asPresentation(
|
||||
useContactNames: Boolean = false
|
||||
): UiFriend = UiFriend(
|
||||
userId = id,
|
||||
avatar = photo100?.let(UiImage::Url),
|
||||
title = if (useContactNames) {
|
||||
VkMemoryCache.getContact(id)?.name ?: fullName
|
||||
} else {
|
||||
fullName
|
||||
},
|
||||
onlineStatus = onlineStatus,
|
||||
photo400Orig = photo400Orig?.let(UiImage::Url)
|
||||
)
|
||||
Reference in New Issue
Block a user