Upstream changes (#23)

This commit is contained in:
2024-07-11 02:12:32 +03:00
committed by GitHub
parent 8a6378f509
commit 3503ecffab
906 changed files with 23577 additions and 24115 deletions
@@ -0,0 +1,175 @@
package com.meloda.app.fast.friends
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.meloda.app.fast.common.extensions.listenValue
import com.meloda.app.fast.common.extensions.setValue
import com.meloda.app.fast.data.State
import com.meloda.app.fast.data.api.friends.FriendsUseCase
import com.meloda.app.fast.data.processState
import com.meloda.app.fast.datastore.UserSettings
import com.meloda.app.fast.friends.model.FriendsScreenState
import com.meloda.app.fast.friends.model.UiFriend
import com.meloda.app.fast.friends.util.asPresentation
import com.meloda.app.fast.model.BaseError
import com.meloda.app.fast.model.api.domain.VkUser
import com.meloda.app.fast.network.VkErrorCodes
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
interface FriendsViewModel {
val screenState: StateFlow<FriendsScreenState>
val uiFriends: StateFlow<List<UiFriend>>
val uiOnlineFriends: StateFlow<List<UiFriend>>
val baseError: StateFlow<BaseError?>
val imagesToPreload: StateFlow<List<String>>
val currentOffset: StateFlow<Int>
val canPaginate: StateFlow<Boolean>
fun onMetPaginationCondition()
fun onRefresh()
fun onErrorConsumed()
}
class FriendsViewModelImpl(
private val friendsUseCase: FriendsUseCase,
private val userSettings: UserSettings
) : ViewModel(), FriendsViewModel {
override val screenState = MutableStateFlow(FriendsScreenState.EMPTY)
override val uiFriends = screenState.map { it.friends }
.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
override val uiOnlineFriends = MutableStateFlow<List<UiFriend>>(emptyList())
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 onMetPaginationCondition() {
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 = 30, offset = offset).listenValue { state ->
state.processState(
error = { error ->
when (error) {
is State.Error.ApiError -> {
val (code, message) = error
when (code) {
VkErrorCodes.UserAuthorizationFailed -> {
baseError.setValue { BaseError.SessionExpired }
}
else -> {
Unit
}
}
}
State.Error.ConnectionError -> TODO()
State.Error.InternalError -> TODO()
is State.Error.OAuthError -> TODO()
State.Error.Unknown -> TODO()
}
},
success = { info ->
val response = info.friends
val itemsCountSufficient = response.size == 30
canPaginate.setValue { itemsCountSufficient }
val paginationExhausted = !itemsCountSufficient &&
screenState.value.friends.size >= 30
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)
}
uiOnlineFriends.setValue { loadedOnlineFriends }
} else {
friends.emit(friends.value.plus(response))
screenState.setValue {
newState.copy(
friends = newState.friends.plus(loadedFriends)
)
}
uiOnlineFriends.setValue { old ->
old.plus(loadedFriends)
}
}
}
)
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 = uiOnlineFriends.value.mapNotNull { friend ->
uiFriends.find { it.userId == friend.userId }
}
screenState.setValue { old ->
old.copy(friends = uiFriends)
}
uiOnlineFriends.setValue { onlineUiFriends }
}
}
@@ -0,0 +1,15 @@
package com.meloda.app.fast.friends.di
import com.meloda.app.fast.data.api.friends.FriendsUseCase
import com.meloda.app.fast.friends.FriendsViewModelImpl
import com.meloda.app.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 com.meloda.app.fast.friends.domain
import com.meloda.app.fast.data.State
import com.meloda.app.fast.data.api.friends.FriendsRepository
import com.meloda.app.fast.data.api.friends.FriendsUseCase
import com.meloda.app.fast.data.mapToState
import com.meloda.app.fast.model.FriendsInfo
import com.meloda.app.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,21 @@
package com.meloda.app.fast.friends.model
import androidx.compose.runtime.Immutable
@Immutable
data class FriendsScreenState(
val isLoading: Boolean,
val friends: List<UiFriend>,
val isPaginating: Boolean,
val isPaginationExhausted: Boolean
) {
companion object {
val EMPTY: FriendsScreenState = FriendsScreenState(
isLoading = true,
friends = emptyList(),
isPaginating = false,
isPaginationExhausted = false
)
}
}
@@ -0,0 +1,5 @@
package com.meloda.app.fast.friends.model
enum class OnlineState {
OFFLINE, ONLINE, ONLINE_MOBILE
}
@@ -0,0 +1,11 @@
package com.meloda.app.fast.friends.model
import com.meloda.app.fast.common.UiImage
import com.meloda.app.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 com.meloda.app.fast.friends.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.friends.FriendsViewModel
import com.meloda.app.fast.friends.FriendsViewModelImpl
import com.meloda.app.fast.friends.presentation.FriendsScreen
import com.meloda.app.fast.model.BaseError
import kotlinx.serialization.Serializable
@Serializable
object Friends
fun NavGraphBuilder.friendsRoute(
onError: (BaseError) -> Unit,
navController: NavController
) {
composable<Friends> {
val viewModel: FriendsViewModel =
it.sharedViewModel<FriendsViewModelImpl>(navController = navController)
FriendsScreen(
onError = onError,
viewModel = viewModel
)
}
}
@@ -0,0 +1,100 @@
package com.meloda.app.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 coil.request.ImageRequest
import com.meloda.app.fast.designsystem.R
import com.meloda.app.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 = ImageRequest.Builder(context)
.data(friendAvatar)
.crossfade(true)
.build(),
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,95 @@
package com.meloda.app.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 com.meloda.app.fast.designsystem.ImmutableList
import com.meloda.app.fast.friends.model.FriendsScreenState
import com.meloda.app.fast.friends.model.UiFriend
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()
LazyColumn(
modifier = modifier,
state = listState
) {
item {
Spacer(modifier = Modifier.height(padding.calculateTopPadding()))
Spacer(modifier = Modifier.height(64.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
)
}
}
}
}
}
}
@@ -0,0 +1,308 @@
package com.meloda.app.fast.friends.presentation
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
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.height
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.Scaffold
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.TabRowDefaults
import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
import androidx.compose.material3.Text
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.draw.alpha
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
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 com.meloda.app.fast.designsystem.ImmutableList
import com.meloda.app.fast.designsystem.LocalTheme
import com.meloda.app.fast.designsystem.TabItem
import com.meloda.app.fast.designsystem.components.BlurrableTopAppBar
import com.meloda.app.fast.designsystem.components.FullScreenLoader
import com.meloda.app.fast.designsystem.components.NoItemsView
import com.meloda.app.fast.friends.FriendsViewModel
import com.meloda.app.fast.friends.FriendsViewModelImpl
import com.meloda.app.fast.model.BaseError
import com.meloda.app.fast.ui.ErrorView
import dev.chrisbanes.haze.HazeState
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 com.meloda.app.fast.designsystem.R as UiR
@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class)
@Composable
fun FriendsScreen(
onError: (BaseError) -> Unit,
viewModel: FriendsViewModel = koinViewModel<FriendsViewModelImpl>()
) {
val baseError by viewModel.baseError.collectAsStateWithLifecycle()
val context = LocalContext.current
val imagesToPreload by viewModel.imagesToPreload.collectAsStateWithLifecycle()
imagesToPreload.forEach { url ->
context.imageLoader.enqueue(
ImageRequest.Builder(context)
.data(url)
.build()
)
}
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
val friends by viewModel.uiFriends.collectAsStateWithLifecycle()
val onlineFriends by viewModel.uiOnlineFriends.collectAsStateWithLifecycle()
val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle()
val currentTheme = LocalTheme.current
val maxLines by remember {
derivedStateOf {
if (currentTheme.multiline) 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) {
viewModel.onMetPaginationCondition()
}
}
val hazeState = remember { HazeState() }
val pullToRefreshAlpha by animateFloatAsState(
targetValue = if (!listState.canScrollBackward) 1f else 0f,
label = "pullToRefreshAlpha",
animationSpec = tween(durationMillis = 50)
)
val tabsColorAlpha by animateFloatAsState(
targetValue = if (!listState.canScrollBackward) 1f else 0f,
label = "toolbarColorAlpha",
animationSpec = tween(durationMillis = 50)
)
val tabsContainerColor by animateColorAsState(
targetValue =
if (currentTheme.usingBlur || !listState.canScrollBackward)
MaterialTheme.colorScheme.surface
else
MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp),
label = "toolbarColorAlpha",
animationSpec = tween(durationMillis = 50)
)
Scaffold(
modifier = Modifier.fillMaxSize(),
contentWindowInsets = WindowInsets.statusBars,
topBar = {
Column(modifier = Modifier.fillMaxWidth()) {
BlurrableTopAppBar(
title = stringResource(id = UiR.string.title_friends),
listState = listState,
hazeState = hazeState
)
}
}
) { padding ->
when {
baseError is BaseError.SessionExpired -> {
ErrorView(
text = "Session expired",
buttonText = "Log out",
onButtonClick = { onError(BaseError.SessionExpired) }
)
}
screenState.isLoading && friends.isEmpty() -> FullScreenLoader()
else -> {
val pullToRefreshState = rememberPullToRefreshState()
var selectedTabIndex by rememberSaveable {
mutableIntStateOf(0)
}
val tabItems = listOf(
TabItem(
titleResId = UiR.string.title_friends_all,
unselectedIconResId = null,
selectedIconResId = null
),
TabItem(
titleResId = UiR.string.title_friends_online,
unselectedIconResId = null,
selectedIconResId = null
)
)
val pagerState = rememberPagerState { tabItems.size }
LaunchedEffect(selectedTabIndex) {
pagerState.animateScrollToPage(selectedTabIndex)
}
LaunchedEffect(pagerState) {
snapshotFlow {
pagerState.currentPage
}.collect { page ->
selectedTabIndex = page
}
}
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())
.nestedScroll(pullToRefreshState.nestedScrollConnection)
) {
val friendsToDisplay = if (index == 0) friends
else onlineFriends
FriendsList(
modifier = if (currentTheme.usingBlur) {
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 = 64.dp),
customText = "No${if (index == 1) " online" else ""} friends :("
)
}
if (pullToRefreshState.isRefreshing) {
LaunchedEffect(true) {
viewModel.onRefresh()
}
}
LaunchedEffect(screenState.isLoading) {
if (!screenState.isLoading) {
pullToRefreshState.endRefresh()
}
}
PullToRefreshContainer(
state = pullToRefreshState,
modifier = Modifier
.padding(top = padding.calculateTopPadding())
.padding(top = 46.dp)
.alpha(pullToRefreshAlpha)
.align(Alignment.TopCenter),
contentColor = MaterialTheme.colorScheme.primary
)
}
}
TabRow(
selectedTabIndex = selectedTabIndex,
modifier = Modifier
.padding(top = padding.calculateTopPadding() - 4.dp)
.height(56.dp)
.then(
if (currentTheme.usingBlur) {
Modifier.hazeChild(
state = hazeState,
style = HazeMaterials.thick()
)
} else {
Modifier
}
),
containerColor = tabsContainerColor.copy(
alpha = if (currentTheme.usingBlur) tabsColorAlpha else 1f
),
indicator = { tabPositions ->
TabRowDefaults.PrimaryIndicator(
modifier = Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex]),
width = 48.dp
)
}
) {
tabItems.forEachIndexed { index, item ->
Tab(
selected = index == selectedTabIndex,
onClick = {
if (selectedTabIndex != index) {
selectedTabIndex = index
}
},
text = {
item.titleResId?.let { resId ->
Text(text = stringResource(id = resId))
}
}
)
}
}
}
}
}
}
}
@@ -0,0 +1,19 @@
package com.meloda.app.fast.friends.util
import com.meloda.app.fast.common.UiImage
import com.meloda.app.fast.data.VkMemoryCache
import com.meloda.app.fast.friends.model.UiFriend
import com.meloda.app.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
)