Add user stats and pull to refresh in friends screen
continuous-integration/drone/push Build is passing Details

pull/5/head
avalin 8 months ago
parent c4525ad97e
commit 38a42582f3

@ -111,7 +111,15 @@ private fun BetScreenCardPreview() {
time = "13:00",
totalParticipants = 25,
players = listOf(
User(id = "", username = "Lucas D", email = "", coins = 0),
User(
id = "",
username = "Lucas D",
email = "",
coins = 0,
nbBets = 0,
nbFriends = 0,
bestWin = 0,
)
),
onClickParticipate = {},
onClickCard = {}

@ -2,9 +2,11 @@ package fr.iut.alldev.allin.ui.betCreation.tabs.sections
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
@ -18,11 +20,13 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
@ -34,6 +38,7 @@ import fr.iut.alldev.allin.ui.betCreation.components.BetCreationScreenFriendLine
import fr.iut.alldev.allin.ui.core.AllInIconChip
import fr.iut.alldev.allin.ui.core.AllInRetractableCard
import fr.iut.alldev.allin.ui.core.AllInTitleInfo
import fr.iut.alldev.allin.ui.core.bet.AllInEmptyView
@Composable
fun QuestionTabPrivacySection(
@ -99,39 +104,53 @@ fun QuestionTabPrivacySection(
isOpen = isOpen,
setIsOpen = { isOpen = it }
) {
LazyColumn(
modifier = Modifier
.height(440.dp)
.nestedScroll(object : NestedScrollConnection {
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
) = available.copy(x = 0f)
})
) {
itemsIndexed(friends, key = { _, it -> it.id }) { idx, it ->
var isSelected by remember {
mutableStateOf(selectedFriends.contains(it.id))
}
Box {
LazyColumn(
modifier = Modifier
.heightIn(max = 440.dp)
.nestedScroll(object : NestedScrollConnection {
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
) = available.copy(x = 0f)
})
) {
itemsIndexed(friends, key = { _, it -> it.id }) { idx, it ->
var isSelected by remember {
mutableStateOf(selectedFriends.contains(it.id))
}
if (idx != 0) {
HorizontalDivider(color = AllInTheme.colors.border)
}
BetCreationScreenFriendLine(
username = "Dave",
allCoinsAmount = 542,
isSelected = isSelected
) {
if (isSelected) {
selectedFriends.remove(it.id)
} else {
selectedFriends.add(it.id)
if (idx != 0) {
HorizontalDivider(color = AllInTheme.colors.border)
}
BetCreationScreenFriendLine(
username = it.username,
allCoinsAmount = it.coins,
isSelected = isSelected
) {
if (isSelected) {
selectedFriends.remove(it.id)
} else {
selectedFriends.add(it.id)
}
isSelected = !isSelected
}
isSelected = !isSelected
}
}
if (friends.isEmpty()) {
AllInEmptyView(
text = stringResource(id = R.string.friends_empty_text),
subtext = stringResource(id = R.string.friends_empty_subtext),
image = painterResource(id = R.drawable.silhouettes),
modifier = Modifier
.fillMaxWidth()
.align(Alignment.Center)
)
}
}
}
Column(
modifier = Modifier.padding(vertical = 20.dp),

@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
@ -422,7 +421,15 @@ private fun BetStatusBottomSheetPreview(
openParticipateSheet = {}
).DisplayBet(
betDetail = bet,
currentUser = User(id = "x", username = "aaa", email = "aaa", coins = 150)
currentUser = User(
id = "x",
username = "aaa",
email = "aaa",
coins = 150,
nbBets = 0,
nbFriends = 0,
bestWin = 0,
)
)
}
}

@ -40,7 +40,7 @@ fun AllInButton(
enabled = enabled,
onClick = onClick,
contentPadding = if (isSmall) {
PaddingValues(horizontal = 8.dp)
PaddingValues(horizontal = 12.dp)
} else ButtonDefaults.ContentPadding
) {
Text(

@ -2,6 +2,7 @@ package fr.iut.alldev.allin.ui.friends
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalFocusManager
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import fr.iut.alldev.allin.data.model.FriendStatus
@ -14,7 +15,10 @@ fun FriendsScreen(
val search by viewModel.search.collectAsStateWithLifecycle()
val addTabState by viewModel.addTabState.collectAsStateWithLifecycle()
val requestsTabState by viewModel.requestsTabState.collectAsStateWithLifecycle()
val refreshing by viewModel.refreshing.collectAsStateWithLifecycle()
val focus = LocalFocusManager.current
FriendsScreenContent(
addTabState = addTabState,
search = search,
@ -28,6 +32,11 @@ fun FriendsScreen(
},
requestsTabState = requestsTabState,
acceptRequest = { viewModel.addFriend(it.username) },
declineRequest = { viewModel.removeFriend(it.username) }
declineRequest = { viewModel.removeFriend(it.username) },
refresh = {
focus.clearFocus()
viewModel.refreshAll()
},
isRefreshing = refreshing
)
}

@ -30,6 +30,9 @@ class FriendsScreenViewModel @Inject constructor(
private val _requestsTabState by lazy { MutableStateFlow<RequestsTabState>(RequestsTabState.Loading) }
val requestsTabState get() = _requestsTabState.asStateFlow()
private val _refreshing by lazy { MutableStateFlow(false) }
val refreshing by lazy { _refreshing.asStateFlow() }
init {
viewModelScope.launch {
try {
@ -57,6 +60,27 @@ class FriendsScreenViewModel @Inject constructor(
}
}
fun refreshAll() {
viewModelScope.launch {
try {
_refreshing.emit(true)
_addTabState.emit(
_search.value.let { itSearch ->
if (itSearch.isNotBlank()) {
loadSearch(itSearch)
} else {
loadFriends()
}
}
)
_requestsTabState.emit(loadRequests())
} catch (e: Exception) {
Timber.e(e)
}
_refreshing.emit(false)
}
}
private suspend fun loadFriends() =
AddTabState.Loaded(
users = friendRepository.getFriends(

@ -1,12 +1,25 @@
package fr.iut.alldev.allin.ui.friends.components
import androidx.compose.animation.core.animateFloatAsState
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.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
@ -23,75 +36,113 @@ import fr.iut.alldev.allin.ui.core.SectionElement
import fr.iut.alldev.allin.ui.friends.FriendsScreenViewModel
import fr.iut.alldev.allin.ui.friends.tabs.FriendsScreenAddTab
import fr.iut.alldev.allin.ui.friends.tabs.FriendsScreenRequestsTab
import kotlinx.coroutines.launch
@Composable
fun FriendsScreenContent(
addTabState: FriendsScreenViewModel.AddTabState,
search: String,
isRefreshing: Boolean,
onToggleDeleteFriend: (User) -> Unit,
setSearch: (String) -> Unit,
requestsTabState: FriendsScreenViewModel.RequestsTabState,
acceptRequest: (User) -> Unit,
declineRequest: (User) -> Unit
declineRequest: (User) -> Unit,
refresh: () -> Unit
) {
val focus = LocalFocusManager.current
val pullRefreshState = rememberPullRefreshState(isRefreshing, refresh)
val progressAnimation by animateFloatAsState(pullRefreshState.progress * 15, label = "")
val scope = rememberCoroutineScope()
Column(Modifier.fillMaxSize()) {
Box(
modifier = Modifier
.pullRefresh(pullRefreshState)
.padding(top = with(LocalDensity.current) {
progressAnimation.toDp()
})
) {
Column(Modifier.fillMaxSize()) {
Text(
text = stringResource(id = R.string.friends_title),
style = AllInTheme.typography.h1,
color = AllInColorToken.allInGrey,
fontSize = 24.sp,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 11.dp)
)
Text(
text = stringResource(id = R.string.friends_title),
style = AllInTheme.typography.h1,
color = AllInColorToken.allInGrey,
fontSize = 24.sp,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 11.dp)
)
AllInSections(
onLoadSection = { focus.clearFocus() },
sections = listOf(
SectionElement(stringResource(id = R.string.friends_add_tab)) {
when (addTabState) {
is FriendsScreenViewModel.AddTabState.Loaded -> {
FriendsScreenAddTab(
friends = addTabState.users,
search = search,
setSearch = setSearch,
onToggleDeleteFriend = onToggleDeleteFriend
)
}
AllInSections(
modifier = Modifier.let {
if (isRefreshing) {
it
.alpha(.5f)
.pointerInput(Unit) {
scope.launch {
awaitPointerEventScope {
while (true) {
awaitPointerEvent(pass = PointerEventPass.Initial)
.changes
.forEach(PointerInputChange::consume)
}
}
}
}
FriendsScreenViewModel.AddTabState.Loading -> {
AllInLoading(visible = true)
}
}
} else it
},
SectionElement(
stringResource(
id = R.string.friends_requests_tab,
(requestsTabState as? FriendsScreenViewModel.RequestsTabState.Loaded)
?.users?.size ?: 0
)
) {
onLoadSection = { focus.clearFocus() },
sections = listOf(
SectionElement(stringResource(id = R.string.friends_add_tab)) {
when (addTabState) {
is FriendsScreenViewModel.AddTabState.Loaded -> {
FriendsScreenAddTab(
friends = addTabState.users,
search = search,
setSearch = setSearch,
onToggleDeleteFriend = onToggleDeleteFriend
)
}
when (requestsTabState) {
is FriendsScreenViewModel.RequestsTabState.Loaded -> {
FriendsScreenRequestsTab(
requests = requestsTabState.users,
acceptRequest = acceptRequest,
declineRequest = declineRequest
)
FriendsScreenViewModel.AddTabState.Loading -> {
AllInLoading(visible = true)
}
}
},
SectionElement(
stringResource(
id = R.string.friends_requests_tab,
(requestsTabState as? FriendsScreenViewModel.RequestsTabState.Loaded)
?.users?.size ?: 0
)
) {
when (requestsTabState) {
is FriendsScreenViewModel.RequestsTabState.Loaded -> {
FriendsScreenRequestsTab(
requests = requestsTabState.users,
acceptRequest = acceptRequest,
declineRequest = declineRequest
)
}
FriendsScreenViewModel.RequestsTabState.Loading -> {
AllInLoading(visible = true)
FriendsScreenViewModel.RequestsTabState.Loading -> {
AllInLoading(visible = true)
}
}
}
}
)
)
}
PullRefreshIndicator(
modifier = Modifier.align(Alignment.TopCenter),
refreshing = isRefreshing,
state = pullRefreshState
)
}
}
@ -107,7 +158,9 @@ private fun FriendsScreenContentPreview() {
onToggleDeleteFriend = {},
requestsTabState = FriendsScreenViewModel.RequestsTabState.Loaded(emptyList()),
acceptRequest = {},
declineRequest = {}
declineRequest = {},
refresh = {},
isRefreshing = false
)
}
}

@ -40,7 +40,7 @@ fun FriendsScreenLine(
Text(
text = username,
color = AllInTheme.colors.onBackground2,
color = AllInTheme.colors.onMainSurface,
style = AllInTheme.typography.sm2,
maxLines = 1,
overflow = TextOverflow.Ellipsis,

@ -4,6 +4,10 @@ import android.content.res.Configuration
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@ -39,7 +43,7 @@ fun FriendsScreenRequestLine(
Text(
text = username,
color = AllInTheme.colors.onBackground2,
color = AllInTheme.colors.onMainSurface,
style = AllInTheme.typography.sm2,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
@ -47,34 +51,28 @@ fun FriendsScreenRequestLine(
modifier = Modifier.weight(1f)
)
Row(
modifier = Modifier.weight(1.5f),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
AllInButton(
color = AllInColorToken.allInPurple,
text = stringResource(id = R.string.friends_requests_accept),
textColor = AllInColorToken.white,
isSmall = true,
textStyle = AllInTheme.typography.sm2,
onClick = accept,
modifier = Modifier.weight(1f)
onClick = accept
)
AllInButton(
color = AllInTheme.colors.background,
text = stringResource(id = R.string.friends_requests_decline),
textColor = AllInTheme.colors.onBackground,
isSmall = true,
textStyle = AllInTheme.typography.sm2,
onClick = decline,
modifier = Modifier.weight(1f)
)
IconButton(onClick = decline) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = stringResource(id = R.string.friends_requests_decline),
tint = AllInTheme.colors.onBackground2
)
}
}
}
}
@Preview
@Preview(showBackground = true)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun FriendsScreenLinePreview() {

@ -125,9 +125,9 @@ fun MainScreen(
destinations = topLevelDestinations,
scope = scope,
username = currentUser?.username ?: "",
nbFriends = 5,
nbBets = 35,
bestWin = 362,
nbFriends = currentUser?.nbFriends ?: 0,
nbBets = currentUser?.nbBets ?: 0,
bestWin = currentUser?.bestWin ?: 0,
navigateTo = { route ->
mainViewModel.fetchEvents()
navController.popUpTo(route, startDestination)

@ -105,31 +105,46 @@ private fun RankingScreenContentPreview() {
id = "1",
username = "Owen",
email = "",
coins = 8533
coins = 8533,
nbBets = 0,
nbFriends = 0,
bestWin = 0
),
User(
id = "2",
username = "Dave",
email = "",
coins = 6942
coins = 6942,
nbBets = 0,
nbFriends = 0,
bestWin = 0
),
User(
id = "3",
username = "Lucas",
email = "",
coins = 3333
coins = 3333,
nbBets = 0,
nbFriends = 0,
bestWin = 0
),
User(
id = "4",
username = "Louison",
email = "",
coins = 1970
coins = 1970,
nbBets = 0,
nbFriends = 0,
bestWin = 0
),
User(
id = "5",
username = "Imri",
email = "",
coins = 1
coins = 1,
nbBets = 0,
nbFriends = 0,
bestWin = 0
)
)
)
@ -146,7 +161,10 @@ private fun RankingScreenContentEmptyPreview() {
id = "1",
username = "Owen",
email = "",
coins = 8533
coins = 8533,
nbBets = 0,
nbFriends = 0,
bestWin = 0
)
)
)

@ -103,7 +103,10 @@ class MockAllInApi : AllInApi {
username = body.username,
email = body.email,
nbCoins = 500,
token = "${body.username} ${mockUsers.size}"
token = "${body.username} ${mockUsers.size}",
nbBets = 0,
nbFriends = 0,
bestWin = 0
) to body.password
mockUsers.add(response)
return response.first
@ -369,49 +372,70 @@ class MockAllInApi : AllInApi {
username = "User 1",
email = "john@doe.fr",
nbCoins = 250,
token = "token 1"
token = "token 1",
nbBets = 0,
nbFriends = 0,
bestWin = 0
) to "12345",
ResponseUser(
id = "UUID 2",
username = "User 2",
email = "john@doe.fr",
nbCoins = 250,
token = "token 2"
token = "token 2",
nbBets = 0,
nbFriends = 0,
bestWin = 0
) to "12345",
ResponseUser(
id = "UUID 3",
username = "User 3",
email = "john@doe.fr",
nbCoins = 250,
token = "token 3"
token = "token 3",
nbBets = 0,
nbFriends = 0,
bestWin = 0
) to "12345",
ResponseUser(
id = "UUID 4",
username = "User 4",
email = "john@doe.fr",
nbCoins = 250,
token = "token 4"
token = "token 4",
nbBets = 0,
nbFriends = 0,
bestWin = 0
) to "12345",
ResponseUser(
id = "UUID 5",
username = "User 5",
email = "john@doe.fr",
nbCoins = 250,
token = "token 5"
token = "token 5",
nbBets = 0,
nbFriends = 0,
bestWin = 0
) to "12345",
ResponseUser(
id = "UUID 6",
username = "User 6",
email = "john@doe.fr",
nbCoins = 250,
token = "token 6"
token = "token 6",
nbBets = 0,
nbFriends = 0,
bestWin = 0
) to "12345",
ResponseUser(
id = "UUID 7",
username = "User 7",
email = "john@doe.fr",
nbCoins = 250,
token = "token 7"
token = "token 7",
nbBets = 0,
nbFriends = 0,
bestWin = 0
) to "12345"
)

@ -21,6 +21,9 @@ data class ResponseUser(
val email: String,
var nbCoins: Int,
var token: String? = null,
var nbBets: Int,
var nbFriends: Int,
var bestWin: Int,
val friendStatus: FriendStatus? = null
) {
fun toUser() = User(
@ -28,7 +31,10 @@ data class ResponseUser(
username = username,
email = email,
coins = nbCoins,
friendStatus = friendStatus
friendStatus = friendStatus,
nbBets = nbBets,
nbFriends = nbFriends,
bestWin = bestWin
)
}

@ -5,5 +5,8 @@ data class User(
val username: String,
val email: String,
val coins: Int,
var nbBets: Int,
var nbFriends: Int,
var bestWin: Int,
val friendStatus: FriendStatus? = null
)
Loading…
Cancel
Save