diff --git a/app/src/main/kotlin/com/meloda/app/fast/MainGraph.kt b/app/src/main/kotlin/com/meloda/app/fast/MainGraph.kt index c5f47284..09e45559 100644 --- a/app/src/main/kotlin/com/meloda/app/fast/MainGraph.kt +++ b/app/src/main/kotlin/com/meloda/app/fast/MainGraph.kt @@ -7,21 +7,14 @@ import androidx.compose.animation.fadeOut import androidx.compose.animation.slideIn import androidx.compose.animation.slideOut import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Settings -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarDefaults import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf @@ -31,7 +24,6 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.navigation.NavGraphBuilder @@ -47,6 +39,8 @@ import com.meloda.app.fast.designsystem.LocalTheme import com.meloda.app.fast.friends.navigation.Friends import com.meloda.app.fast.friends.navigation.friendsRoute import com.meloda.app.fast.model.BaseError +import com.meloda.app.fast.profile.navigation.Profile +import com.meloda.app.fast.profile.navigation.profileRoute import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.hazeChild import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi @@ -60,9 +54,6 @@ object MainGraph @Serializable object Main -@Serializable -object Profile - data class BottomNavigationItem( val titleResId: Int, val selectedIconResId: Int, @@ -70,7 +61,7 @@ data class BottomNavigationItem( val route: Any, ) -@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class) +@OptIn(ExperimentalHazeMaterialsApi::class) fun NavGraphBuilder.mainScreen( onError: (BaseError) -> Unit, onNavigateToSettings: () -> Unit, @@ -192,34 +183,11 @@ fun NavGraphBuilder.mainScreen( // isBottomBarVisible = isScrolling } ) - - composable { - Scaffold( - topBar = { - TopAppBar( - title = { - Text(text = stringResource(id = UiR.string.title_profile)) - }, - actions = { - IconButton(onClick = onNavigateToSettings) { - Icon( - imageVector = Icons.Rounded.Settings, - contentDescription = null - ) - } - } - ) - } - ) { padding -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(padding) - ) { - - } - } - } + profileRoute( + onError = onError, + onNavigateToSettings = onNavigateToSettings, + navController = navController + ) } } } diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/State.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/State.kt index bf3b663d..18c726d9 100644 --- a/core/data/src/main/kotlin/com/meloda/app/fast/data/State.kt +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/State.kt @@ -42,12 +42,22 @@ inline fun State.processState( success: (data: T) -> (Unit), idle: (() -> (Unit)) = {}, loading: (() -> (Unit)) = {}, + any: () -> Unit = {} ) { when (this) { - is State.Error -> error(this) + is State.Error -> { + error(this) + any() + } + State.Idle -> idle() + State.Loading -> loading() - is State.Success -> success(data) + + is State.Success -> { + success(data) + any() + } } } diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/users/UsersRepository.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/users/UsersRepository.kt index 2430593e..90b48048 100644 --- a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/users/UsersRepository.kt +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/users/UsersRepository.kt @@ -1,8 +1,18 @@ package com.meloda.app.fast.data.api.users -import com.meloda.app.fast.model.api.data.VkUserData -import com.meloda.app.fast.model.api.requests.UsersGetRequest +import com.meloda.app.fast.model.api.domain.VkUser +import com.meloda.app.fast.network.RestApiErrorDomain +import com.slack.eithernet.ApiResult interface UsersRepository { - suspend fun getById(params: UsersGetRequest): List + + suspend fun get( + userIds: List?, + fields: String?, + nomCase: String? + ): ApiResult, RestApiErrorDomain> + + suspend fun getLocalUsers(userIds: List): List + + suspend fun storeUsers(users: List) } diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/users/UsersRepositoryImpl.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/users/UsersRepositoryImpl.kt index b6ce26fe..e4ca3860 100644 --- a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/users/UsersRepositoryImpl.kt +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/users/UsersRepositoryImpl.kt @@ -1,16 +1,62 @@ package com.meloda.app.fast.data.api.users +import com.meloda.app.fast.data.VkMemoryCache +import com.meloda.app.fast.database.dao.UsersDao import com.meloda.app.fast.model.api.data.VkUserData +import com.meloda.app.fast.model.api.domain.VkUser +import com.meloda.app.fast.model.api.domain.asEntity import com.meloda.app.fast.model.api.requests.UsersGetRequest +import com.meloda.app.fast.model.database.VkUserEntity +import com.meloda.app.fast.model.database.asExternalModel +import com.meloda.app.fast.network.RestApiErrorDomain +import com.meloda.app.fast.network.mapApiResult import com.meloda.app.fast.network.service.users.UsersService +import com.slack.eithernet.ApiResult +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext class UsersRepositoryImpl( - private val usersService: UsersService + private val service: UsersService, + private val dao: UsersDao ) : UsersRepository { - override suspend fun getById(params: UsersGetRequest): List { - // TODO: 05/05/2024, Danil Nikolaev: implement + override suspend fun get( + userIds: List?, + fields: String?, + nomCase: String? + ): ApiResult, RestApiErrorDomain> = withContext(Dispatchers.IO) { + val requestModel = UsersGetRequest( + userIds = userIds, + fields = fields, + nomCase = nomCase + ) - return emptyList() + service.get(requestModel.map).mapApiResult( + successMapper = { apiResponse -> + val response = apiResponse.requireResponse() + + val users = response.map(VkUserData::mapToDomain) + + launch { storeUsers(users) } + + VkMemoryCache.appendUsers(users) + + users + }, + errorMapper = { error -> + error?.toDomain() + } + ) + } + + override suspend fun getLocalUsers( + userIds: List + ): List = withContext(Dispatchers.IO) { + dao.getAllByIds(userIds).map(VkUserEntity::asExternalModel) + } + + override suspend fun storeUsers(users: List) { + dao.insertAll(users.map(VkUser::asEntity)) } } diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/users/UsersUseCase.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/users/UsersUseCase.kt index 7a47aa06..569cd9a2 100644 --- a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/users/UsersUseCase.kt +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/users/UsersUseCase.kt @@ -6,18 +6,14 @@ import kotlinx.coroutines.flow.Flow interface UsersUseCase { - fun getUserById( - userId: Int, - fields: String?, - nomCase: String? - ): Flow> - - fun getUsersByIds( - userIds: List, + fun get( + userIds: List?, fields: String?, nomCase: String? ): Flow>> + fun getLocalUser(userId: Int): Flow> + suspend fun storeUser(user: VkUser) suspend fun storeUsers(users: List) } diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/users/UsersUseCaseImpl.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/users/UsersUseCaseImpl.kt index ca274362..ddc26ba2 100644 --- a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/users/UsersUseCaseImpl.kt +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/users/UsersUseCaseImpl.kt @@ -1,67 +1,39 @@ package com.meloda.app.fast.data.api.users import com.meloda.app.fast.data.State +import com.meloda.app.fast.data.mapToState import com.meloda.app.fast.model.api.domain.VkUser import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow - -// TODO: 05/05/2024, Danil Nikolaev: implement class UsersUseCaseImpl( - private val usersRepository: UsersRepository, + private val repository: UsersRepository, ) : UsersUseCase { - override fun getUserById( - userId: Int, - fields: String?, - nomCase: String? - ): Flow> = flow { -// emit(State.Loading) -// -// val newState = usersRepository.getById( -// UsersGetRequest( -// userIds = listOf(userId), -// fields = fields, -// nomCase = nomCase -// ) -// ).fold( -// onSuccess = { response -> State.Success(response.singleOrNull()?.mapToDomain()) }, -// onNetworkFailure = { State.Error.ConnectionError }, -// onUnknownFailure = { State.UNKNOWN_ERROR }, -// onHttpFailure = { result -> result.error.toStateApiError() }, -// onApiFailure = { result -> result.error.toStateApiError() } -// ) -// emit(newState) - } - - override fun getUsersByIds( - userIds: List, + override fun get( + userIds: List?, fields: String?, nomCase: String? ): Flow>> = flow { -// emit(State.Loading) -// -// val newState = usersRepository.getById( -// UsersGetRequest( -// userIds = userIds, -// fields = fields, -// nomCase = nomCase -// ) -// ).fold( -// onSuccess = { response -> State.Success(response.map(VkUserData::mapToDomain)) }, -// onNetworkFailure = { State.Error.ConnectionError }, -// onUnknownFailure = { State.UNKNOWN_ERROR }, -// onHttpFailure = { result -> result.error.toStateApiError() }, -// onApiFailure = { result -> result.error.toStateApiError() } -// ) -// emit(newState) + emit(State.Loading) + + val newState = repository.get(userIds, fields, nomCase).mapToState() + emit(newState) } - override suspend fun storeUser(user: VkUser) { + override fun getLocalUser(userId: Int): Flow> = flow { + emit(State.Loading) + val newState = kotlin.runCatching { + repository.getLocalUsers(listOf(userId)).singleOrNull() + }.fold( + onSuccess = { user -> State.Success(user) }, + onFailure = { State.Error.InternalError } + ) + + emit(newState) } - override suspend fun storeUsers(users: List) { - - } + override suspend fun storeUser(user: VkUser) = repository.storeUsers(listOf(user)) + override suspend fun storeUsers(users: List) = repository.storeUsers(users) } diff --git a/core/model/src/main/kotlin/com/meloda/app/fast/model/api/requests/UsersRequest.kt b/core/model/src/main/kotlin/com/meloda/app/fast/model/api/requests/UsersRequest.kt index 84ae8a3f..155b9260 100644 --- a/core/model/src/main/kotlin/com/meloda/app/fast/model/api/requests/UsersRequest.kt +++ b/core/model/src/main/kotlin/com/meloda/app/fast/model/api/requests/UsersRequest.kt @@ -9,7 +9,7 @@ data class UsersGetRequest( val map get() = mutableMapOf() .apply { - userIds?.let { this["user_ids"] = it.joinToString() } + userIds?.let { this["user_ids"] = it.joinToString(",") } fields?.let { this["fields"] = it } nomCase?.let { this["nom_case"] = it } } diff --git a/core/model/src/main/kotlin/com/meloda/app/fast/model/database/VkUserEntity.kt b/core/model/src/main/kotlin/com/meloda/app/fast/model/database/VkUserEntity.kt index d096e307..17756159 100644 --- a/core/model/src/main/kotlin/com/meloda/app/fast/model/database/VkUserEntity.kt +++ b/core/model/src/main/kotlin/com/meloda/app/fast/model/database/VkUserEntity.kt @@ -2,6 +2,8 @@ package com.meloda.app.fast.model.database import androidx.room.Entity import androidx.room.PrimaryKey +import com.meloda.app.fast.model.api.domain.OnlineStatus +import com.meloda.app.fast.model.api.domain.VkUser @Entity(tableName = "users") data class VkUserEntity( @@ -18,3 +20,20 @@ data class VkUserEntity( val photo100: String?, val photo200: String? ) + +fun VkUserEntity.asExternalModel(): VkUser = VkUser( + id = id, + firstName = firstName, + lastName = lastName, + onlineStatus = when { + !isOnline -> OnlineStatus.Offline + !isOnlineMobile -> OnlineStatus.Online(onlineAppId) + else -> OnlineStatus.OnlineMobile(onlineAppId) + }, + photo50 = photo50, + photo100 = photo100, + photo200 = photo200, + lastSeen = lastSeen, + lastSeenStatus = lastSeenStatus, + birthday = birthday +) diff --git a/core/network/src/main/kotlin/com/meloda/app/fast/network/service/users/UsersService.kt b/core/network/src/main/kotlin/com/meloda/app/fast/network/service/users/UsersService.kt index 0e7ec196..372f2c26 100644 --- a/core/network/src/main/kotlin/com/meloda/app/fast/network/service/users/UsersService.kt +++ b/core/network/src/main/kotlin/com/meloda/app/fast/network/service/users/UsersService.kt @@ -12,7 +12,7 @@ interface UsersService { @FormUrlEncoded @POST(UsersUrls.GET_BY_ID) - suspend fun getById( + suspend fun get( @FieldMap params: Map? ): ApiResult>, RestApiError> } diff --git a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/LoginViewModel.kt b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/LoginViewModel.kt index 68262daa..e69b975e 100644 --- a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/LoginViewModel.kt +++ b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/LoginViewModel.kt @@ -135,11 +135,26 @@ class LoginViewModelImpl( UserConfig.trustedHash = account.trustedHash } - viewModelScope.launch(Dispatchers.IO) { - accountsRepository.storeAccounts(listOf(currentAccount)) + usersUseCase.get( + userIds = null, + fields = VkConstants.USER_FIELDS, + nomCase = null + ).listenValue { state -> + state.processState( + error = { error -> - delay(350) - screenState.setValue { old -> old.copy(isNeedToNavigateToMain = true) } + }, + success = { response -> + viewModelScope.launch(Dispatchers.IO) { + accountsRepository.storeAccounts(listOf(currentAccount)) + + delay(350) + screenState.setValue { old -> old.copy(isNeedToNavigateToMain = true) } + } + } + ) + + screenState.setValue { old -> old.copy(isLoading = state.isLoading()) } } } @@ -177,16 +192,11 @@ class LoginViewModelImpl( return@processState } - usersUseCase.getUserById( - userId = userId, + usersUseCase.get( + userIds = listOf(userId), fields = VkConstants.USER_FIELDS, nomCase = null - ).listenValue { state -> - state.processState( - error = {}, - success = { user -> user?.let { usersUseCase.storeUser(user) } } - ) - } + ) val currentAccount = AccountEntity( userId = userId, diff --git a/feature/profile/src/main/kotlin/com/meloda/app/fast/profile/ProfileViewModel.kt b/feature/profile/src/main/kotlin/com/meloda/app/fast/profile/ProfileViewModel.kt index d7942b2e..fb0cf5bd 100644 --- a/feature/profile/src/main/kotlin/com/meloda/app/fast/profile/ProfileViewModel.kt +++ b/feature/profile/src/main/kotlin/com/meloda/app/fast/profile/ProfileViewModel.kt @@ -1,12 +1,78 @@ package com.meloda.app.fast.profile import androidx.lifecycle.ViewModel +import com.meloda.app.fast.common.UserConfig +import com.meloda.app.fast.common.VkConstants +import com.meloda.app.fast.common.extensions.listenValue +import com.meloda.app.fast.common.extensions.setValue +import com.meloda.app.fast.data.api.users.UsersUseCase +import com.meloda.app.fast.data.processState +import com.meloda.app.fast.profile.model.ProfileScreenState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow interface ProfileViewModel { + val screenState: StateFlow } class ProfileViewModelImpl( - + private val usersUseCase: UsersUseCase ) : ViewModel(), ProfileViewModel { + override val screenState = MutableStateFlow(ProfileScreenState.EMPTY) + + init { + getLocalAccountInfo() + } + + private fun getLocalAccountInfo() { + usersUseCase.getLocalUser(UserConfig.userId) + .listenValue { state -> + state.processState( + error = { error -> + screenState.setValue { old -> + old.copy( + avatarUrl = null, + fullName = null + ) + } + }, + success = { user -> + screenState.setValue { old -> + old.copy( + avatarUrl = user?.photo200, + fullName = user?.fullName + ) + } + }, + any = ::loadAccountInfo + ) + } + } + + private fun loadAccountInfo() { + usersUseCase.get( + userIds = null, + fields = VkConstants.USER_FIELDS, + nomCase = null + ).listenValue { state -> + state.processState( + error = { error -> + // TODO: 12/07/2024, Danil Nikolaev: if local info is null then show error view + }, + success = { response -> + val user = response.single() + + screenState.setValue { old -> + old.copy( + avatarUrl = user.photo200, + fullName = user.fullName + ) + } + } + ) + + screenState.setValue { old -> old.copy(isLoading = state.isLoading()) } + } + } } diff --git a/feature/profile/src/main/kotlin/com/meloda/app/fast/profile/model/ProfileScreenState.kt b/feature/profile/src/main/kotlin/com/meloda/app/fast/profile/model/ProfileScreenState.kt new file mode 100644 index 00000000..daa6133f --- /dev/null +++ b/feature/profile/src/main/kotlin/com/meloda/app/fast/profile/model/ProfileScreenState.kt @@ -0,0 +1,16 @@ +package com.meloda.app.fast.profile.model + +data class ProfileScreenState( + val isLoading: Boolean, + val avatarUrl: String?, + val fullName: String? +) { + + companion object { + val EMPTY: ProfileScreenState = ProfileScreenState( + isLoading = false, + avatarUrl = null, + fullName = null + ) + } +} diff --git a/feature/profile/src/main/kotlin/com/meloda/app/fast/profile/navigation/ProfileRoute.kt b/feature/profile/src/main/kotlin/com/meloda/app/fast/profile/navigation/ProfileRoute.kt new file mode 100644 index 00000000..703765cf --- /dev/null +++ b/feature/profile/src/main/kotlin/com/meloda/app/fast/profile/navigation/ProfileRoute.kt @@ -0,0 +1,31 @@ +package com.meloda.app.fast.profile.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import com.meloda.app.fast.common.extensions.navigation.sharedViewModel +import com.meloda.app.fast.model.BaseError +import com.meloda.app.fast.profile.ProfileViewModel +import com.meloda.app.fast.profile.ProfileViewModelImpl +import com.meloda.app.fast.profile.presentation.ProfileScreen +import kotlinx.serialization.Serializable + +@Serializable +object Profile + +fun NavGraphBuilder.profileRoute( + onError: (BaseError) -> Unit, + onNavigateToSettings: () -> Unit, + navController: NavController +) { + composable { + val viewModel: ProfileViewModel = + it.sharedViewModel(navController = navController) + + ProfileScreen( + onError = onError, + onNavigateToSettings = onNavigateToSettings, + viewModel = viewModel + ) + } +} diff --git a/feature/profile/src/main/kotlin/com/meloda/app/fast/profile/presentation/ProfileScreen.kt b/feature/profile/src/main/kotlin/com/meloda/app/fast/profile/presentation/ProfileScreen.kt new file mode 100644 index 00000000..6c2d71b9 --- /dev/null +++ b/feature/profile/src/main/kotlin/com/meloda/app/fast/profile/presentation/ProfileScreen.kt @@ -0,0 +1,110 @@ +package com.meloda.app.fast.profile.presentation + +import android.util.Log +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Settings +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import coil.compose.AsyncImage +import com.meloda.app.fast.model.BaseError +import com.meloda.app.fast.profile.ProfileViewModel +import com.meloda.app.fast.profile.ProfileViewModelImpl +import org.koin.androidx.compose.koinViewModel + +import com.meloda.app.fast.designsystem.R as UiR + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ProfileScreen( + onError: (BaseError) -> Unit, + onNavigateToSettings: () -> Unit, + viewModel: ProfileViewModel = koinViewModel() +) { + val screenState by viewModel.screenState.collectAsStateWithLifecycle() + + Log.d("ProfileScreen", "isLoading: ${screenState.isLoading}") + + Scaffold( + topBar = { + TopAppBar( + title = {}, + actions = { + IconButton(onClick = onNavigateToSettings) { + Icon( + imageVector = Icons.Rounded.Settings, + contentDescription = null + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent) + ) + } + ) { padding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(start = padding.calculateStartPadding(LayoutDirection.Ltr)) + .padding(end = padding.calculateEndPadding(LayoutDirection.Ltr)) + .padding(bottom = padding.calculateBottomPadding()), + horizontalAlignment = Alignment.CenterHorizontally + ) { + if (screenState.fullName == null && screenState.isLoading) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } else { + Spacer(modifier = Modifier.statusBarsPadding()) + Spacer(modifier = Modifier.height(24.dp)) + + AsyncImage( + modifier = Modifier + .size(120.dp) + .clip(CircleShape), + model = screenState.avatarUrl, + contentDescription = null, + contentScale = ContentScale.Crop, + placeholder = painterResource(id = UiR.drawable.ic_account_circle_cut) + ) + + Spacer(modifier = Modifier.height(18.dp)) + + Text( + text = screenState.fullName.orEmpty(), + style = MaterialTheme.typography.headlineLarge + ) + } + } + } +}