update package name (even bigger one)

This commit is contained in:
2024-07-16 07:02:50 +03:00
parent 4f9e49003b
commit c8b1d72f08
367 changed files with 12 additions and 25 deletions
@@ -0,0 +1,159 @@
package dev.meloda.fast.friends
import androidx.lifecycle.ViewModel
import dev.meloda.fast.common.extensions.listenValue
import dev.meloda.fast.common.extensions.setValue
import dev.meloda.fast.data.State
import dev.meloda.fast.data.api.friends.FriendsUseCase
import dev.meloda.fast.data.processState
import dev.meloda.fast.datastore.UserSettings
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
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
// TODO: 13/07/2024, Danil Nikolaev: separate two lists and their pagination
interface FriendsViewModel {
val screenState: StateFlow<FriendsScreenState>
val baseError: StateFlow<BaseError?>
val imagesToPreload: StateFlow<List<String>>
val currentOffset: StateFlow<Int>
val canPaginate: StateFlow<Boolean>
fun onPaginationConditionsMet()
fun onRefresh()
fun onErrorConsumed()
}
class FriendsViewModelImpl(
private val friendsUseCase: FriendsUseCase,
private val userSettings: UserSettings
) : ViewModel(), FriendsViewModel {
override val screenState = MutableStateFlow(FriendsScreenState.EMPTY)
override val baseError = MutableStateFlow<BaseError?>(null)
override val imagesToPreload = MutableStateFlow<List<String>>(emptyList())
override val currentOffset = MutableStateFlow(0)
override val canPaginate = MutableStateFlow(false)
private val friends = MutableStateFlow<List<VkUser>>(emptyList())
init {
userSettings.useContactNames.listenValue(::updateFriendsNames)
loadFriends()
}
override fun onPaginationConditionsMet() {
currentOffset.update { screenState.value.friends.size }
loadFriends()
}
override fun onRefresh() {
loadFriends(offset = 0)
}
override fun onErrorConsumed() {
baseError.setValue { null }
}
private fun loadFriends(offset: Int = currentOffset.value) {
friendsUseCase.getAllFriends(count = LOAD_COUNT, offset = offset).listenValue { state ->
state.processState(
error = { error ->
if (error is State.Error.ApiError) {
when (error.errorCode) {
VkErrorCode.USER_AUTHORIZATION_FAILED -> {
baseError.setValue { BaseError.SessionExpired }
}
else -> Unit
}
}
},
success = { info ->
val response = info.friends
val itemsCountSufficient = response.size == LOAD_COUNT
canPaginate.setValue { itemsCountSufficient }
val paginationExhausted = !itemsCountSufficient &&
screenState.value.friends.size >= LOAD_COUNT
imagesToPreload.setValue {
response.mapNotNull(VkUser::photo100)
}
friendsUseCase.storeUsers(response)
val loadedFriends = response.map {
it.asPresentation(userSettings.useContactNames.value)
}
val loadedOnlineFriends = info.onlineFriends.map {
it.asPresentation(userSettings.useContactNames.value)
}
val newState = screenState.value.copy(
isPaginationExhausted = paginationExhausted
)
if (offset == 0) {
friends.emit(response)
screenState.setValue {
newState.copy(
friends = loadedFriends,
onlineFriends = loadedOnlineFriends
)
}
} else {
friends.emit(friends.value.plus(response))
screenState.setValue {
newState.copy(
friends = newState.friends.plus(loadedFriends),
onlineFriends = newState.onlineFriends.plus(loadedOnlineFriends)
)
}
}
}
)
screenState.setValue { old ->
old.copy(
isLoading = offset == 0 && state.isLoading(),
isPaginating = offset > 0 && state.isLoading()
)
}
}
}
private fun updateFriendsNames(useContactNames: Boolean) {
val friends = friends.value
if (friends.isEmpty()) return
val uiFriends = friends.map { conversation ->
conversation.asPresentation(useContactNames)
}
val onlineUiFriends = screenState.value.onlineFriends.mapNotNull { friend ->
uiFriends.find { it.userId == friend.userId }
}
screenState.setValue { old ->
old.copy(
friends = uiFriends,
onlineFriends = onlineUiFriends
)
}
}
companion object {
const val LOAD_COUNT = 60
}
}
@@ -0,0 +1,15 @@
package dev.meloda.fast.friends.di
import dev.meloda.fast.data.api.friends.FriendsUseCase
import dev.meloda.fast.friends.FriendsViewModelImpl
import dev.meloda.fast.friends.domain.FriendsUseCaseImpl
import org.koin.androidx.viewmodel.dsl.viewModelOf
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.bind
import org.koin.dsl.module
val friendsModule = module {
singleOf(::FriendsUseCaseImpl) bind FriendsUseCase::class
viewModelOf(::FriendsViewModelImpl)
}
@@ -0,0 +1,42 @@
package dev.meloda.fast.friends.domain
import dev.meloda.fast.data.State
import dev.meloda.fast.data.api.friends.FriendsRepository
import dev.meloda.fast.data.api.friends.FriendsUseCase
import dev.meloda.fast.data.mapToState
import dev.meloda.fast.model.FriendsInfo
import dev.meloda.fast.model.api.domain.VkUser
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
class FriendsUseCaseImpl(private val repository: FriendsRepository) : FriendsUseCase {
override fun getAllFriends(count: Int?, offset: Int?): Flow<State<FriendsInfo>> = flow {
emit(State.Loading)
val newState = repository.getAllFriends(count, offset).mapToState()
emit(newState)
}
override fun getFriends(
count: Int?, offset: Int?
): Flow<State<List<VkUser>>> = flow {
emit(State.Loading)
val newState = repository.getFriends(count, offset).mapToState()
emit(newState)
}
override fun getOnlineFriends(
count: Int?, offset: Int?
): Flow<State<List<Int>>> = flow {
emit(State.Loading)
val newState = repository.getOnlineFriends(count, offset).mapToState()
emit(newState)
}
override suspend fun storeUsers(users: List<VkUser>) {
repository.storeUsers(users)
}
}
@@ -0,0 +1,23 @@
package dev.meloda.fast.friends.model
import androidx.compose.runtime.Immutable
@Immutable
data class FriendsScreenState(
val isLoading: Boolean,
val friends: List<UiFriend>,
val onlineFriends: List<UiFriend>,
val isPaginating: Boolean,
val isPaginationExhausted: Boolean
) {
companion object {
val EMPTY: FriendsScreenState = FriendsScreenState(
isLoading = true,
friends = emptyList(),
onlineFriends = emptyList(),
isPaginating = false,
isPaginationExhausted = false
)
}
}
@@ -0,0 +1,5 @@
package dev.meloda.fast.friends.model
enum class OnlineState {
OFFLINE, ONLINE, ONLINE_MOBILE
}
@@ -0,0 +1,11 @@
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
)
@@ -0,0 +1,29 @@
package dev.meloda.fast.friends.navigation
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import dev.meloda.fast.common.extensions.navigation.sharedViewModel
import dev.meloda.fast.friends.FriendsViewModel
import dev.meloda.fast.friends.FriendsViewModelImpl
import dev.meloda.fast.friends.presentation.FriendsRoute
import dev.meloda.fast.model.BaseError
import kotlinx.serialization.Serializable
@Serializable
object Friends
fun NavGraphBuilder.friendsScreen(
onError: (BaseError) -> Unit,
navController: NavController
) {
composable<Friends> {
val viewModel: FriendsViewModel =
it.sharedViewModel<FriendsViewModelImpl>(navController = navController)
FriendsRoute(
onError = onError,
viewModel = viewModel
)
}
}
@@ -0,0 +1,96 @@
package dev.meloda.fast.friends.presentation
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import dev.meloda.fast.ui.R
import dev.meloda.fast.friends.model.UiFriend
@Composable
fun FriendItem(
modifier: Modifier = Modifier,
friend: UiFriend,
maxLines: Int
) {
val context = LocalContext.current
Row(
modifier = modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Spacer(modifier = Modifier.width(16.dp))
val friendAvatar = friend.avatar?.extractUrl()
Box(modifier = Modifier.size(56.dp)) {
if (friendAvatar == null) {
Image(
modifier = Modifier
.fillMaxSize()
.clip(CircleShape),
painter = painterResource(id = R.drawable.ic_account_circle_cut),
contentDescription = "Avatar",
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground)
)
} else {
AsyncImage(
model = friendAvatar,
contentDescription = null,
modifier = Modifier
.fillMaxSize()
.clip(CircleShape),
placeholder = painterResource(id = R.drawable.ic_account_circle_cut)
)
}
if (friend.onlineStatus.isOnline()) {
Box(
modifier = Modifier
.clip(CircleShape)
.size(18.dp)
.background(MaterialTheme.colorScheme.background)
.padding(2.dp)
.align(Alignment.BottomEnd)
) {
Box(
modifier = Modifier
.clip(CircleShape)
.matchParentSize()
.background(MaterialTheme.colorScheme.primary)
)
}
}
}
Spacer(modifier = Modifier.width(16.dp))
Text(
text = friend.title,
minLines = 1,
maxLines = maxLines,
style = MaterialTheme.typography.headlineSmall.copy(fontSize = 20.sp)
)
Spacer(modifier = Modifier.width(16.dp))
}
}
@@ -0,0 +1,102 @@
package dev.meloda.fast.friends.presentation
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.KeyboardArrowUp
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.runtime.Composable
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.friends.model.FriendsScreenState
import dev.meloda.fast.friends.model.UiFriend
import dev.meloda.fast.ui.theme.LocalBottomPadding
import dev.meloda.fast.ui.util.ImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable
fun FriendsList(
modifier: Modifier = Modifier,
screenState: FriendsScreenState,
uiFriends: ImmutableList<UiFriend>,
listState: LazyListState,
maxLines: Int,
padding: PaddingValues
) {
val coroutineScope = rememberCoroutineScope()
val friends = uiFriends.toList()
val bottomPadding = LocalBottomPadding.current
LazyColumn(
modifier = modifier,
state = listState
) {
item {
Spacer(modifier = Modifier.height(padding.calculateTopPadding()))
Spacer(modifier = Modifier.height(16.dp))
}
items(
items = friends,
key = UiFriend::userId,
) { friend ->
FriendItem(
friend = friend,
maxLines = maxLines
)
Spacer(modifier = Modifier.height(16.dp))
}
item {
Column(
modifier = Modifier
.fillMaxWidth()
.animateItem(fadeInSpec = null, fadeOutSpec = null)
.navigationBarsPadding(),
horizontalAlignment = Alignment.CenterHorizontally
) {
if (screenState.isPaginating) {
CircularProgressIndicator()
}
if (screenState.isPaginationExhausted) {
IconButton(
onClick = {
coroutineScope.launch(Dispatchers.Main) {
listState.scrollToItem(14)
listState.animateScrollToItem(0)
}
},
colors = IconButtonDefaults.filledIconButtonColors()
) {
Icon(
imageVector = Icons.Rounded.KeyboardArrowUp,
contentDescription = null
)
}
}
}
}
item {
Spacer(modifier = Modifier.height(bottomPadding))
}
}
}
@@ -0,0 +1,349 @@
package dev.meloda.fast.friends.presentation
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.FastOutLinearInEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryTabRow
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Tab
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.pulltorefresh.PullToRefreshContainer
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil.imageLoader
import coil.request.ImageRequest
import dev.meloda.fast.datastore.UserSettings
import dev.meloda.fast.friends.FriendsViewModel
import dev.meloda.fast.friends.FriendsViewModelImpl
import dev.meloda.fast.friends.model.FriendsScreenState
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.NoItemsView
import dev.meloda.fast.ui.model.TabItem
import dev.meloda.fast.ui.theme.LocalHazeState
import dev.meloda.fast.ui.theme.LocalThemeConfig
import dev.meloda.fast.ui.util.ImmutableList
import dev.chrisbanes.haze.haze
import dev.chrisbanes.haze.hazeChild
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import dev.chrisbanes.haze.materials.HazeMaterials
import org.koin.androidx.compose.koinViewModel
import org.koin.compose.koinInject
import dev.meloda.fast.ui.R as UiR
@Composable
fun FriendsRoute(
onError: (BaseError) -> Unit,
viewModel: FriendsViewModel = koinViewModel<FriendsViewModelImpl>()
) {
val context = LocalContext.current
val userSettings: UserSettings = koinInject()
val enablePullToRefresh by userSettings.enablePullToRefresh.collectAsStateWithLifecycle()
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
val baseError by viewModel.baseError.collectAsStateWithLifecycle()
val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle()
val imagesToPreload by viewModel.imagesToPreload.collectAsStateWithLifecycle()
LaunchedEffect(imagesToPreload) {
imagesToPreload.forEach { url ->
context.imageLoader.enqueue(
ImageRequest.Builder(context)
.data(url)
.build()
)
}
}
FriendsScreen(
screenState = screenState,
baseError = baseError,
enablePullToRefresh = enablePullToRefresh,
canPaginate = canPaginate,
onSessionExpiredLogOutButtonClicked = { onError(BaseError.SessionExpired) },
onPaginationConditionsMet = viewModel::onPaginationConditionsMet,
onRefresh = viewModel::onRefresh
)
}
// TODO: 13/07/2024, Danil Nikolaev: support for online
@OptIn(
ExperimentalMaterial3Api::class,
ExperimentalHazeMaterialsApi::class
)
@Composable
fun FriendsScreen(
screenState: FriendsScreenState = FriendsScreenState.EMPTY,
baseError: BaseError? = null,
enablePullToRefresh: Boolean = false,
canPaginate: Boolean = false,
onSessionExpiredLogOutButtonClicked: () -> Unit = {},
onPaginationConditionsMet: () -> Unit = {},
onRefresh: () -> Unit = {}
) {
val currentTheme = LocalThemeConfig.current
val maxLines by remember {
derivedStateOf {
if (currentTheme.enableMultiline) 2 else 1
}
}
val listState = rememberLazyListState()
val paginationConditionMet by remember {
derivedStateOf {
canPaginate &&
(listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index
?: -9) >= (listState.layoutInfo.totalItemsCount - 6)
}
}
LaunchedEffect(paginationConditionMet) {
if (paginationConditionMet && !screenState.isPaginating) {
onPaginationConditionsMet()
}
}
val hazeState = LocalHazeState.current
val topBarContainerColorAlpha by animateFloatAsState(
targetValue = if (!currentTheme.enableBlur || !listState.canScrollBackward) 1f else 0f,
label = "toolbarColorAlpha",
animationSpec = tween(
durationMillis = 200,
easing = FastOutLinearInEasing
)
)
val topBarContainerColor by animateColorAsState(
targetValue =
if (currentTheme.enableBlur || !listState.canScrollBackward)
MaterialTheme.colorScheme.surface
else
MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp),
label = "toolbarColorAlpha",
animationSpec = tween(
durationMillis = 200,
easing = FastOutLinearInEasing
)
)
var selectedTabIndex by rememberSaveable {
mutableIntStateOf(0)
}
val tabItems = remember {
listOf(
TabItem(
titleResId = UiR.string.title_friends_all,
unselectedIconResId = null,
selectedIconResId = null
),
TabItem(
titleResId = UiR.string.title_friends_online,
unselectedIconResId = null,
selectedIconResId = null
)
)
}
Scaffold(
modifier = Modifier.fillMaxSize(),
contentWindowInsets = WindowInsets.statusBars,
topBar = {
Column(
modifier = Modifier
.then(
if (currentTheme.enableBlur) {
Modifier.hazeChild(
state = hazeState,
style = HazeMaterials.thick()
)
} else {
Modifier
}
)
.background(topBarContainerColor.copy(alpha = topBarContainerColorAlpha))
.fillMaxWidth()
) {
TopAppBar(
title = {
Text(
text = stringResource(id = UiR.string.title_friends),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent
),
modifier = Modifier.fillMaxWidth(),
)
PrimaryTabRow(
selectedTabIndex = selectedTabIndex,
modifier = Modifier,
containerColor = Color.Transparent
) {
tabItems.forEachIndexed { index, item ->
Tab(
selected = index == selectedTabIndex,
onClick = {
if (selectedTabIndex != index) {
selectedTabIndex = index
}
},
text = {
item.titleResId?.let { resId ->
Text(text = stringResource(id = resId))
}
}
)
}
}
}
}
) { padding ->
when {
baseError is BaseError.SessionExpired -> {
ErrorView(
text = "Session expired",
buttonText = "Log out",
onButtonClick = onSessionExpiredLogOutButtonClicked
)
}
screenState.isLoading && screenState.friends.isEmpty() -> FullScreenLoader()
else -> {
val pagerState = rememberPagerState { tabItems.size }
LaunchedEffect(selectedTabIndex) {
pagerState.animateScrollToPage(selectedTabIndex)
}
LaunchedEffect(pagerState) {
snapshotFlow {
pagerState.currentPage
}.collect { page ->
selectedTabIndex = page
}
}
val pullToRefreshState = rememberPullToRefreshState()
Box(modifier = Modifier.fillMaxSize()) {
HorizontalPager(
state = pagerState,
modifier = Modifier.fillMaxSize(),
) { index ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(start = padding.calculateStartPadding(LayoutDirection.Ltr))
.padding(end = padding.calculateEndPadding(LayoutDirection.Ltr))
.padding(bottom = padding.calculateBottomPadding())
.then(
if (enablePullToRefresh) {
Modifier.nestedScroll(
pullToRefreshState.nestedScrollConnection
)
} else Modifier
)
) {
val friendsToDisplay = screenState.friends
FriendsList(
modifier = if (currentTheme.enableBlur) {
Modifier.haze(
state = hazeState,
style = HazeMaterials.thick()
)
} else {
Modifier
}.fillMaxSize(),
screenState = screenState,
uiFriends = ImmutableList.copyOf(friendsToDisplay),
listState = listState,
maxLines = maxLines,
padding = padding
)
if (friendsToDisplay.isEmpty()) {
NoItemsView(
modifier = Modifier
.padding(padding.calculateTopPadding())
.padding(top = 16.dp),
customText = "No${if (index == 1) " online" else ""} friends :("
)
}
if (enablePullToRefresh) {
if (pullToRefreshState.isRefreshing) {
LaunchedEffect(true) {
onRefresh()
}
}
LaunchedEffect(screenState.isLoading) {
if (!screenState.isLoading) {
pullToRefreshState.endRefresh()
}
}
PullToRefreshContainer(
state = pullToRefreshState,
modifier = Modifier
.padding(top = padding.calculateTopPadding())
.align(Alignment.TopCenter),
contentColor = MaterialTheme.colorScheme.primary
)
}
}
}
}
}
}
}
}
@@ -0,0 +1,19 @@
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
)