From 36a119ffa976a2fd1b476dab770d59220536241b Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Fri, 21 Mar 2025 12:43:22 +0300 Subject: [PATCH] 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`. --- .../dev/meloda/fast/navigation/MainGraph.kt | 2 + .../meloda/fast/presentation/MainScreen.kt | 17 +---- .../meloda/fast/presentation/RootScreen.kt | 1 + .../fast/domain/LongPollUpdatesParser.kt | 10 +-- .../kotlin/dev/meloda/fast/model/ApiEvent.kt | 1 + .../meloda/fast/ui/components/ErrorView.kt | 4 +- .../meloda/fast/ui/components/NoItemsView.kt | 39 +++++++--- core/ui/src/main/res/values-ru/strings.xml | 5 +- core/ui/src/main/res/values/strings.xml | 6 +- .../conversations/ConversationsViewModel.kt | 25 +------ .../navigation/ConversationsNavigation.kt | 3 - .../presentation/ConversationsScreen.kt | 39 +++++----- .../meloda/fast/friends/FriendsViewModel.kt | 71 ++++++++++--------- .../friends/navigation/FriendsNavigation.kt | 6 +- .../fast/friends/presentation/FriendItem.kt | 24 ++++++- .../fast/friends/presentation/FriendsList.kt | 4 +- .../friends/presentation/FriendsScreen.kt | 57 +++++++++------ 17 files changed, 173 insertions(+), 141 deletions(-) diff --git a/app/src/main/kotlin/dev/meloda/fast/navigation/MainGraph.kt b/app/src/main/kotlin/dev/meloda/fast/navigation/MainGraph.kt index 4f20efcd..d3166245 100644 --- a/app/src/main/kotlin/dev/meloda/fast/navigation/MainGraph.kt +++ b/app/src/main/kotlin/dev/meloda/fast/navigation/MainGraph.kt @@ -24,6 +24,7 @@ fun NavGraphBuilder.mainScreen( onSettingsButtonClicked: () -> Unit, onConversationClicked: (conversationId: Int) -> Unit, onPhotoClicked: (url: String) -> Unit, + onMessageClicked: (userId: Int) -> Unit, viewModel: MainViewModel ) { val navigationItems = ImmutableList.of( @@ -54,6 +55,7 @@ fun NavGraphBuilder.mainScreen( onSettingsButtonClicked = onSettingsButtonClicked, onConversationItemClicked = onConversationClicked, onPhotoClicked = onPhotoClicked, + onMessageClicked = onMessageClicked, viewModel = viewModel ) } diff --git a/app/src/main/kotlin/dev/meloda/fast/presentation/MainScreen.kt b/app/src/main/kotlin/dev/meloda/fast/presentation/MainScreen.kt index 778bd9a0..42019a82 100644 --- a/app/src/main/kotlin/dev/meloda/fast/presentation/MainScreen.kt +++ b/app/src/main/kotlin/dev/meloda/fast/presentation/MainScreen.kt @@ -47,8 +47,6 @@ import dev.meloda.fast.ui.theme.LocalBottomPadding import dev.meloda.fast.ui.theme.LocalHazeState import dev.meloda.fast.ui.theme.LocalThemeConfig import dev.meloda.fast.ui.util.ImmutableList -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.flow.MutableSharedFlow @OptIn(ExperimentalHazeMaterialsApi::class) @Composable @@ -58,6 +56,7 @@ fun MainScreen( onSettingsButtonClicked: () -> Unit = {}, onConversationItemClicked: (conversationId: Int) -> Unit = {}, onPhotoClicked: (url: String) -> Unit = {}, + onMessageClicked: (userId: Int) -> Unit = {}, viewModel: MainViewModel ) { val currentTheme = LocalThemeConfig.current @@ -70,14 +69,6 @@ fun MainScreen( mutableIntStateOf(1) } - val sharedFlow = remember { - MutableSharedFlow( - replay = 0, - extraBufferCapacity = 1, - onBufferOverflow = BufferOverflow.DROP_OLDEST - ) - } - Scaffold( bottomBar = { NavigationBar( @@ -108,8 +99,6 @@ fun MainScreen( inclusive = true } } - } else { - sharedFlow.tryEmit(index) } }, icon = { @@ -176,13 +165,13 @@ fun MainScreen( friendsScreen( onError = onError, navController = navController, - onPhotoClicked = onPhotoClicked + onPhotoClicked = onPhotoClicked, + onMessageClicked = onMessageClicked ) conversationsScreen( onError = onError, onConversationItemClicked = onConversationItemClicked, onPhotoClicked = onPhotoClicked, - scrollToTopFlow = sharedFlow, navController = navController, ) profileScreen( diff --git a/app/src/main/kotlin/dev/meloda/fast/presentation/RootScreen.kt b/app/src/main/kotlin/dev/meloda/fast/presentation/RootScreen.kt index 4678a203..5fd28fe5 100644 --- a/app/src/main/kotlin/dev/meloda/fast/presentation/RootScreen.kt +++ b/app/src/main/kotlin/dev/meloda/fast/presentation/RootScreen.kt @@ -124,6 +124,7 @@ fun RootScreen( onSettingsButtonClicked = navController::navigateToSettings, onConversationClicked = navController::navigateToMessagesHistory, onPhotoClicked = { url -> navController.navigateToPhotoView(listOf(url)) }, + onMessageClicked = navController::navigateToMessagesHistory, viewModel = viewModel ) diff --git a/core/domain/src/main/kotlin/dev/meloda/fast/domain/LongPollUpdatesParser.kt b/core/domain/src/main/kotlin/dev/meloda/fast/domain/LongPollUpdatesParser.kt index 4a5ab80f..65037ee5 100644 --- a/core/domain/src/main/kotlin/dev/meloda/fast/domain/LongPollUpdatesParser.kt +++ b/core/domain/src/main/kotlin/dev/meloda/fast/domain/LongPollUpdatesParser.kt @@ -41,15 +41,9 @@ class LongPollUpdatesParser( fun parseNextUpdate(event: List) { val eventId = event.first().asInt() - val eventType: ApiEvent = try { - ApiEvent.parse(eventId) - } catch (e: Exception) { - e.printStackTrace() - Log.d("LongPollUpdatesParser", "parseNextUpdate: unknownEvent: $event") - return - } + when (val eventType = ApiEvent.parseOrNull(eventId)) { + null -> Log.d("LongPollUpdatesParser", "parseNextUpdate: unknownEvent: $event") - when (eventType) { ApiEvent.MESSAGE_SET_FLAGS -> parseMessageSetFlags(eventType, event) ApiEvent.MESSAGE_CLEAR_FLAGS -> parseMessageClearFlags(eventType, event) ApiEvent.MESSAGE_NEW -> parseMessageNew(eventType, event) diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/ApiEvent.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/ApiEvent.kt index 2fce295e..74067e29 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/ApiEvent.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/ApiEvent.kt @@ -18,5 +18,6 @@ enum class ApiEvent(val value: Int) { companion object { fun parse(value: Int) = entries.first { it.value == value } + fun parseOrNull(value: Int) = entries.firstOrNull { it.value == value } } } diff --git a/core/ui/src/main/kotlin/dev/meloda/fast/ui/components/ErrorView.kt b/core/ui/src/main/kotlin/dev/meloda/fast/ui/components/ErrorView.kt index 32b3db31..a7140fec 100644 --- a/core/ui/src/main/kotlin/dev/meloda/fast/ui/components/ErrorView.kt +++ b/core/ui/src/main/kotlin/dev/meloda/fast/ui/components/ErrorView.kt @@ -12,6 +12,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -31,7 +32,8 @@ fun ErrorView( ) { Text( text = text, - style = MaterialTheme.typography.titleLarge + style = MaterialTheme.typography.titleLarge, + textAlign = TextAlign.Center ) buttonText?.let { diff --git a/core/ui/src/main/kotlin/dev/meloda/fast/ui/components/NoItemsView.kt b/core/ui/src/main/kotlin/dev/meloda/fast/ui/components/NoItemsView.kt index b87ca10c..3c7a0d84 100644 --- a/core/ui/src/main/kotlin/dev/meloda/fast/ui/components/NoItemsView.kt +++ b/core/ui/src/main/kotlin/dev/meloda/fast/ui/components/NoItemsView.kt @@ -1,29 +1,51 @@ package dev.meloda.fast.ui.components -import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import dev.meloda.fast.ui.R @Composable fun NoItemsView( modifier: Modifier = Modifier, - customText: String? = null + customText: String? = null, + buttonText: String? = null, + onButtonClick: (() -> Unit)? = null, ) { - Box( - modifier = modifier.fillMaxSize(), - contentAlignment = Alignment.Center + Column( + modifier = modifier + .fillMaxSize() + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally ) { Text( - text = customText ?: stringResource(id = R.string.no_items), - style = MaterialTheme.typography.titleLarge + text = customText ?: stringResource(R.string.no_items), + style = MaterialTheme.typography.titleLarge, + textAlign = TextAlign.Center ) + + buttonText?.let { + Spacer(modifier = Modifier.height(12.dp)) + Button( + onClick = { onButtonClick?.invoke() } + ) { + Text(text = buttonText) + } + } } } @@ -31,6 +53,7 @@ fun NoItemsView( @Composable private fun NoItemsViewPreview() { NoItemsView( - customText = "Nothing here..." + customText = "Nothing here...", + buttonText = "Refresh" ) } diff --git a/core/ui/src/main/res/values-ru/strings.xml b/core/ui/src/main/res/values-ru/strings.xml index ec25a7be..379b9ba6 100644 --- a/core/ui/src/main/res/values-ru/strings.xml +++ b/core/ui/src/main/res/values-ru/strings.xml @@ -128,7 +128,7 @@ Запись сообщества Запись пользователя Запись на стене - Выйти + Выйти Подтверждение Ваша история Динамические цвета @@ -212,4 +212,7 @@ Вы уверены? Процесс ввода капчи будет отменён Вы уверены? Процесс ввода кода-подтверждения будет отменён Авторизоваться + Никого в сети + Попробовать ещё раз + Срок действия сессии истёк diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml index d5a48e61..59b6edc8 100644 --- a/core/ui/src/main/res/values/strings.xml +++ b/core/ui/src/main/res/values/strings.xml @@ -119,7 +119,7 @@ Community post User post Post - Log out + Log out Confirmation Signing out will delete all data related to this account from this device. Continue? Yes @@ -276,6 +276,8 @@ Confirmation Are you sure? Captcha process will be cancelled Are you sure? Validation process will be cancelled - Enable pull to refresh Authorize + No one is online + Try again + Session expired diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/ConversationsViewModel.kt b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/ConversationsViewModel.kt index 5806134d..1540e0a0 100644 --- a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/ConversationsViewModel.kt +++ b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/ConversationsViewModel.kt @@ -30,9 +30,7 @@ import dev.meloda.fast.model.LongPollEvent import dev.meloda.fast.model.api.domain.VkConversation import dev.meloda.fast.network.VkErrorCode import dev.meloda.fast.ui.util.ImmutableList -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -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 val currentOffset: StateFlow val canPaginate: StateFlow - val scrollToTop: StateFlow fun onPaginationConditionsMet() @@ -70,10 +66,6 @@ interface ConversationsViewModel { fun setScrollIndex(index: Int) fun setScrollOffset(offset: Int) - - - fun setScrollToTopFlow(scrollToTopFlow: Flow) - fun onScrolledToTop() } class ConversationsViewModelImpl( @@ -91,7 +83,6 @@ class ConversationsViewModelImpl( override val baseError = MutableStateFlow(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 = { @@ -134,7 +125,7 @@ class ConversationsViewModelImpl( } override fun onRefresh() { - baseError.setValue { null } + onErrorConsumed() loadConversations(offset = 0) } @@ -237,20 +228,6 @@ class ConversationsViewModelImpl( screenState.setValue { old -> old.copy(scrollOffset = offset) } } - override fun setScrollToTopFlow(scrollToTopFlow: Flow) { - 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( diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/navigation/ConversationsNavigation.kt b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/navigation/ConversationsNavigation.kt index ccc91b6c..66805ed8 100644 --- a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/navigation/ConversationsNavigation.kt +++ b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/navigation/ConversationsNavigation.kt @@ -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,13 +17,11 @@ fun NavGraphBuilder.conversationsScreen( onError: (BaseError) -> Unit, onConversationItemClicked: (id: Int) -> Unit, onPhotoClicked: (url: String) -> Unit, - scrollToTopFlow: Flow, navController: NavController, ) { composable { val viewModel: ConversationsViewModel = it.sharedViewModel(navController = navController) - viewModel.setScrollToTopFlow(scrollToTopFlow) ConversationsRoute( onError = onError, diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsScreen.kt b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsScreen.kt index c038e352..0312b5d4 100644 --- a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsScreen.kt +++ b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsScreen.kt @@ -77,6 +77,7 @@ 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.theme.LocalBottomPadding import dev.meloda.fast.ui.theme.LocalHazeState import dev.meloda.fast.ui.theme.LocalThemeConfig @@ -96,7 +97,6 @@ fun ConversationsRoute( 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, @@ -114,9 +114,7 @@ fun ConversationsRoute( onRefresh = viewModel::onRefresh, onConversationPhotoClicked = onConversationPhotoClicked, setScrollIndex = viewModel::setScrollIndex, - setScrollOffset = viewModel::setScrollOffset, - isNeedToScrollToTop = isNeedToScrollToTop, - onScrolledToTop = viewModel::onScrolledToTop + setScrollOffset = viewModel::setScrollOffset ) HandleDialogs( @@ -143,9 +141,7 @@ fun ConversationsScreen( onRefresh: () -> Unit = {}, onConversationPhotoClicked: (url: String) -> 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 +155,6 @@ fun ConversationsScreen( initialFirstVisibleItemScrollOffset = screenState.scrollOffset ) - LaunchedEffect(isNeedToScrollToTop) { - if (isNeedToScrollToTop) { - listState.scrollToItem(0) - onScrolledToTop() - - } - } - LaunchedEffect(listState) { snapshotFlow { listState.firstVisibleItemIndex } .debounce(500L) @@ -207,10 +195,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) ) @@ -343,8 +331,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 +340,7 @@ fun ConversationsScreen( is BaseError.SimpleError -> { ErrorView( text = baseError.message, - buttonText = "Try again", + buttonText = stringResource(UiR.string.try_again), onButtonClick = onRefresh ) } @@ -398,6 +386,13 @@ fun ConversationsScreen( padding = padding, onPhotoClicked = onConversationPhotoClicked ) + + if (screenState.conversations.isEmpty()) { + NoItemsView( + buttonText = stringResource(UiR.string.action_refresh), + onButtonClick = onRefresh + ) + } } } } diff --git a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/FriendsViewModel.kt b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/FriendsViewModel.kt index c01de124..0bac308f 100644 --- a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/FriendsViewModel.kt +++ b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/FriendsViewModel.kt @@ -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 diff --git a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/navigation/FriendsNavigation.kt b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/navigation/FriendsNavigation.kt index 261718e1..9fdaa30e 100644 --- a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/navigation/FriendsNavigation.kt +++ b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/navigation/FriendsNavigation.kt @@ -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 { val viewModel: FriendsViewModel = @@ -25,7 +26,8 @@ fun NavGraphBuilder.friendsScreen( FriendsRoute( onError = onError, viewModel = viewModel, - onPhotoClicked = onPhotoClicked + onPhotoClicked = onPhotoClicked, + onMessageClicked = onMessageClicked ) } } diff --git a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendItem.kt b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendItem.kt index 98f482a4..376a8b68 100644 --- a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendItem.kt +++ b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendItem.kt @@ -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 @@ -31,7 +35,8 @@ 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)) } } diff --git a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendsList.kt b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendsList.kt index 7ada64dd..752e8997 100644 --- a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendsList.kt +++ b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendsList.kt @@ -37,6 +37,7 @@ fun FriendsList( maxLines: Int, padding: PaddingValues, onPhotoClicked: (url: String) -> Unit, + onMessageClicked: (userId: Int) -> Unit, setCanScrollBackward: (Boolean) -> Unit ) { LaunchedEffect(listState) { @@ -66,7 +67,8 @@ fun FriendsList( FriendItem( friend = friend, maxLines = maxLines, - onPhotoClicked = onPhotoClicked + onPhotoClicked = onPhotoClicked, + onMessageClicked = onMessageClicked ) Spacer(modifier = Modifier.height(16.dp)) diff --git a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendsScreen.kt b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendsScreen.kt index c6655c4e..d3560226 100644 --- a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendsScreen.kt +++ b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendsScreen.kt @@ -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() ) { 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 ) } }