diff --git a/src/app/src/main/java/fr/iut/alldev/allin/ui/friends/FriendsScreen.kt b/src/app/src/main/java/fr/iut/alldev/allin/ui/friends/FriendsScreen.kt index fa7da99..7d8f026 100644 --- a/src/app/src/main/java/fr/iut/alldev/allin/ui/friends/FriendsScreen.kt +++ b/src/app/src/main/java/fr/iut/alldev/allin/ui/friends/FriendsScreen.kt @@ -7,7 +7,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import fr.iut.alldev.allin.data.model.User +import fr.iut.alldev.allin.data.model.FriendStatus import fr.iut.alldev.allin.ui.core.AllInLoading import fr.iut.alldev.allin.ui.friends.components.FriendsScreenContent @@ -15,12 +15,13 @@ import fr.iut.alldev.allin.ui.friends.components.FriendsScreenContent fun FriendsScreen( viewModel: FriendsScreenViewModel = hiltViewModel() ) { - var search by remember { viewModel.search } + val search by viewModel.search.collectAsStateWithLifecycle() val state by viewModel.state.collectAsStateWithLifecycle() when (val s = state) { is FriendsScreenViewModel.State.Loaded -> { - var deleted by remember { mutableStateOf(emptyList()) } + var deleted by remember { mutableStateOf(emptyList()) } + var requested by remember { mutableStateOf(emptyList()) } val filteredFriends = remember(search) { s.friends.filter { it.username.contains(search, ignoreCase = true) @@ -29,16 +30,21 @@ fun FriendsScreen( FriendsScreenContent( friends = filteredFriends, - deleted = deleted, + deletedUsers = deleted, + requestedUsers = requested, search = search, - setSearch = { search = it }, + setSearch = { viewModel.setSearch(it) }, onToggleDeleteFriend = { - deleted = if (deleted.contains(it)) { + deleted = if (deleted.contains(it.id) || it.friendStatus == FriendStatus.NOT_FRIEND) { viewModel.addFriend(it.username) - deleted - it + requested = requested + it.id + deleted - it.id } else { viewModel.removeFriend(it.username) - deleted + it + if (requested.contains(it.id)) { + requested = requested - it.id + } + deleted + it.id } } ) diff --git a/src/app/src/main/java/fr/iut/alldev/allin/ui/friends/FriendsScreenViewModel.kt b/src/app/src/main/java/fr/iut/alldev/allin/ui/friends/FriendsScreenViewModel.kt index 0b35979..83b9054 100644 --- a/src/app/src/main/java/fr/iut/alldev/allin/ui/friends/FriendsScreenViewModel.kt +++ b/src/app/src/main/java/fr/iut/alldev/allin/ui/friends/FriendsScreenViewModel.kt @@ -1,6 +1,5 @@ package fr.iut.alldev.allin.ui.friends -import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel @@ -19,7 +18,9 @@ class FriendsScreenViewModel @Inject constructor( private val keystoreManager: AllInKeystoreManager ) : ViewModel() { - val search by lazy { mutableStateOf("") } + private val _search by lazy { MutableStateFlow("") } + val search get() = _search.asStateFlow() + private val _state by lazy { MutableStateFlow(State.Loading) } val state get() = _state.asStateFlow() @@ -36,6 +37,35 @@ class FriendsScreenViewModel @Inject constructor( } catch (e: Exception) { Timber.e(e) } + + _search.collect { itSearch -> + try { + _state.emit( + if (itSearch.isNotBlank()) { + State.Loaded( + friends = friendRepository.searchNew( + token = keystoreManager.getTokenOrEmpty(), + search = itSearch + ) + ) + } else { + State.Loaded( + friends = friendRepository.getFriends( + token = keystoreManager.getTokenOrEmpty() + ) + ) + } + ) + } catch (e: Exception) { + Timber.e(e) + } + } + } + } + + fun setSearch(search: String) { + viewModelScope.launch { + _search.emit(search) } } diff --git a/src/app/src/main/java/fr/iut/alldev/allin/ui/friends/components/FriendsScreenContent.kt b/src/app/src/main/java/fr/iut/alldev/allin/ui/friends/components/FriendsScreenContent.kt index 954e7f6..3847710 100644 --- a/src/app/src/main/java/fr/iut/alldev/allin/ui/friends/components/FriendsScreenContent.kt +++ b/src/app/src/main/java/fr/iut/alldev/allin/ui/friends/components/FriendsScreenContent.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import fr.iut.alldev.allin.R +import fr.iut.alldev.allin.data.model.FriendStatus import fr.iut.alldev.allin.data.model.User import fr.iut.alldev.allin.ext.asPaddingValues import fr.iut.alldev.allin.theme.AllInColorToken @@ -32,7 +33,8 @@ import fr.iut.alldev.allin.ui.core.AllInTextField @Composable fun FriendsScreenContent( friends: List, - deleted: List, + deletedUsers: List, + requestedUsers: List, search: String, onToggleDeleteFriend: (User) -> Unit, setSearch: (String) -> Unit, @@ -73,7 +75,11 @@ fun FriendsScreenContent( items(friends) { FriendsScreenLine( username = it.username, - isFriend = it !in deleted, + status = if (it.id in deletedUsers) { + FriendStatus.NOT_FRIEND + } else if (it.id in requestedUsers) { + FriendStatus.REQUESTED + } else it.friendStatus ?: FriendStatus.NOT_FRIEND, toggleIsFriend = { onToggleDeleteFriend(it) } ) } @@ -87,7 +93,8 @@ private fun FriendsScreenContentPreview() { AllInTheme { FriendsScreenContent( friends = emptyList(), - deleted = emptyList(), + deletedUsers = emptyList(), + requestedUsers = emptyList(), search = "", setSearch = {}, onToggleDeleteFriend = {} diff --git a/src/app/src/main/java/fr/iut/alldev/allin/ui/friends/components/FriendsScreenLine.kt b/src/app/src/main/java/fr/iut/alldev/allin/ui/friends/components/FriendsScreenLine.kt index 79b6247..a691d3c 100644 --- a/src/app/src/main/java/fr/iut/alldev/allin/ui/friends/components/FriendsScreenLine.kt +++ b/src/app/src/main/java/fr/iut/alldev/allin/ui/friends/components/FriendsScreenLine.kt @@ -1,5 +1,6 @@ package fr.iut.alldev.allin.ui.friends.components +import android.content.res.Configuration import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth @@ -13,6 +14,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import fr.iut.alldev.allin.R +import fr.iut.alldev.allin.data.model.FriendStatus import fr.iut.alldev.allin.ext.asFallbackProfileUsername import fr.iut.alldev.allin.theme.AllInColorToken import fr.iut.alldev.allin.theme.AllInTheme @@ -22,7 +24,7 @@ import fr.iut.alldev.allin.ui.core.ProfilePicture @Composable fun FriendsScreenLine( username: String, - isFriend: Boolean, + status: FriendStatus, toggleIsFriend: () -> Unit, modifier: Modifier = Modifier ) { @@ -47,25 +49,31 @@ fun FriendsScreenLine( ) AllInButton( - color = if (isFriend) { - AllInTheme.colors.background - } else { - AllInColorToken.allInPurple + color = when (status) { + FriendStatus.FRIEND -> AllInTheme.colors.background + FriendStatus.NOT_FRIEND -> AllInColorToken.allInPurple + FriendStatus.REQUESTED -> AllInTheme.colors.border }, - text = if (isFriend) { - stringResource(id = R.string.generic_delete) - } else { - stringResource(id = R.string.generic_add) + text = when (status) { + FriendStatus.FRIEND -> { + stringResource(id = R.string.generic_delete) + } + FriendStatus.NOT_FRIEND -> { + stringResource(id = R.string.generic_add) + } + FriendStatus.REQUESTED -> { + stringResource(id = R.string.friends_request_sent) + } }, - textColor = if (isFriend) { - AllInTheme.colors.onBackground - } else { - AllInColorToken.white + textColor = when (status) { + FriendStatus.FRIEND -> AllInTheme.colors.onBackground + FriendStatus.NOT_FRIEND -> AllInColorToken.white + FriendStatus.REQUESTED -> AllInTheme.colors.onBackground2 }, isSmall = true, textStyle = AllInTheme.typography.sm2, onClick = toggleIsFriend, - modifier = Modifier.weight(.5f) + modifier = Modifier.weight(.8f) ) } } @@ -76,19 +84,33 @@ private fun FriendsScreenLinePreview() { AllInTheme { FriendsScreenLine( username = "Random", - isFriend = false, + status = FriendStatus.NOT_FRIEND, toggleIsFriend = { } ) } } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun FriendsScreenLineRequestedPreview() { + AllInTheme { + FriendsScreenLine( + username = "Random", + status = FriendStatus.REQUESTED, + toggleIsFriend = { } + ) + } +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun FriendsScreenLineIsFriendPreview() { AllInTheme { FriendsScreenLine( username = "Random", - isFriend = true, + status = FriendStatus.FRIEND, toggleIsFriend = { } ) } diff --git a/src/app/src/main/java/fr/iut/alldev/allin/ui/ranking/RankingViewModel.kt b/src/app/src/main/java/fr/iut/alldev/allin/ui/ranking/RankingViewModel.kt index 8dd8f96..c3c18b4 100644 --- a/src/app/src/main/java/fr/iut/alldev/allin/ui/ranking/RankingViewModel.kt +++ b/src/app/src/main/java/fr/iut/alldev/allin/ui/ranking/RankingViewModel.kt @@ -3,6 +3,7 @@ package fr.iut.alldev.allin.ui.ranking import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import fr.iut.alldev.allin.data.model.FriendStatus import fr.iut.alldev.allin.data.model.User import fr.iut.alldev.allin.data.repository.FriendRepository import fr.iut.alldev.allin.keystore.AllInKeystoreManager @@ -27,7 +28,7 @@ class RankingViewModel @Inject constructor( State.Loaded( friends = friendRepository.getFriends( token = keystoreManager.getTokenOrEmpty() - ) + ).filter { it.friendStatus == FriendStatus.FRIEND } ) ) } catch (e: Exception) { diff --git a/src/app/src/main/res/values-fr/strings.xml b/src/app/src/main/res/values-fr/strings.xml index 74e4a4e..2159105 100644 --- a/src/app/src/main/res/values-fr/strings.xml +++ b/src/app/src/main/res/values-fr/strings.xml @@ -167,7 +167,7 @@ Amis - + Requête envoyée Récompense quotidienne Votre récompense quotidienne est débloquée tous les jours à 00:00 UTC et vous permets d’obtenir entre 10 et 150 Allcoins. diff --git a/src/app/src/main/res/values/strings.xml b/src/app/src/main/res/values/strings.xml index 1fa2282..9c9e173 100644 --- a/src/app/src/main/res/values/strings.xml +++ b/src/app/src/main/res/values/strings.xml @@ -164,6 +164,7 @@ Friends + Request sent Daily reward diff --git a/src/data/src/main/java/fr/iut/alldev/allin/data/api/AllInApi.kt b/src/data/src/main/java/fr/iut/alldev/allin/data/api/AllInApi.kt index 4d6067b..9783063 100644 --- a/src/data/src/main/java/fr/iut/alldev/allin/data/api/AllInApi.kt +++ b/src/data/src/main/java/fr/iut/alldev/allin/data/api/AllInApi.kt @@ -45,7 +45,7 @@ interface AllInApi { // FRIENDS // --------------------- - @GET("friends/add") + @GET("friends/gets") suspend fun getFriends(@Header("Authorization") token: String): List @POST("friends/add") @@ -60,6 +60,12 @@ interface AllInApi { @Body request: RequestFriend ) + @GET("friends/search/{search}") + suspend fun searchFriend( + @Header("Authorization") token: String, + @Path("search") search: String + ): List + // BETS // --------------------- diff --git a/src/data/src/main/java/fr/iut/alldev/allin/data/api/MockAllInApi.kt b/src/data/src/main/java/fr/iut/alldev/allin/data/api/MockAllInApi.kt index 0f7a228..58754eb 100644 --- a/src/data/src/main/java/fr/iut/alldev/allin/data/api/MockAllInApi.kt +++ b/src/data/src/main/java/fr/iut/alldev/allin/data/api/MockAllInApi.kt @@ -13,6 +13,8 @@ import fr.iut.alldev.allin.data.api.model.ResponseBetResult import fr.iut.alldev.allin.data.api.model.ResponseBetResultDetail import fr.iut.alldev.allin.data.api.model.ResponseParticipation import fr.iut.alldev.allin.data.api.model.ResponseUser +import fr.iut.alldev.allin.data.model.FriendStatus +import fr.iut.alldev.allin.data.model.bet.BetFilter import fr.iut.alldev.allin.data.model.bet.BetStatus import fr.iut.alldev.allin.data.model.bet.BetType import fr.iut.alldev.allin.data.model.bet.NO_VALUE @@ -71,6 +73,20 @@ class MockAllInApi : AllInApi { } } + private fun getFriendStatus(userId: String, withId: String): FriendStatus { + return mockFriends.filter { + it.first == userId && it.second == withId + }.let { + if (it.isEmpty()) FriendStatus.NOT_FRIEND + else mockFriends.filter { + it.second == userId && it.first == withId + }.let { + if (it.isEmpty()) FriendStatus.REQUESTED + else FriendStatus.FRIEND + } + } + } + override suspend fun login(body: CheckUser): ResponseUser { return mockUsers.find { it.first.username == body.login && it.second == body.password }?.first ?: throw MockAllInApiException("Invalid login/password.") @@ -139,7 +155,13 @@ class MockAllInApi : AllInApi { val user = getUserFromToken(token) ?: throw MockAllInApiException("Invalid login/password.") return mockFriends .filter { it.first == user.first.id } - .mapNotNull { mockUsers.find { usr -> usr.first.id == it.second }?.first } + .mapNotNull { itUser -> + mockUsers.find { usr -> usr.first.id == itUser.second } + ?.first + ?.copy( + friendStatus = getFriendStatus(userId = user.first.id, withId = itUser.second) + ) + } } override suspend fun addFriend(token: String, request: RequestFriend) { @@ -156,9 +178,51 @@ class MockAllInApi : AllInApi { mockFriends.remove(user.first.id to requestUser.first.id) } + override suspend fun searchFriend(token: String, search: String): List { + val user = getUserFromToken(token) ?: throw MockAllInApiException("Invalid login/password.") + return mockUsers.filter { it.first.username.contains(search, ignoreCase = true) } + .map { itUser -> + itUser.first.copy( + friendStatus = getFriendStatus(userId = user.first.id, withId = itUser.first.id) + ) + } + } + override suspend fun getAllBets(token: String, body: RequestBetFilters): List { getUserFromToken(token) ?: throw MockAllInApiException("Invalid login/password.") - return mockBets + val filters = body.filters + return when { + filters.isEmpty() -> mockBets + + filters.size == 1 -> { + val filter = filters[0] + + when (filter) { + BetFilter.PUBLIC -> mockBets.filter { !it.isPrivate } + BetFilter.INVITATION -> mockBets.filter { it.isPrivate } + BetFilter.FINISHED -> mockBets.filter { it.status == BetStatus.FINISHED } + BetFilter.IN_PROGRESS -> mockBets.filter { + it.status in listOf(BetStatus.IN_PROGRESS, BetStatus.WAITING, BetStatus.CLOSING) + } + }.map { it } + } + + else -> { + mockBets.filter { bet -> + val public = (BetFilter.PUBLIC in filters) && !bet.isPrivate + val invitation = (BetFilter.INVITATION in filters) && bet.isPrivate + val finished = + (BetFilter.FINISHED in filters) and ((bet.status == BetStatus.FINISHED) or (bet.status == BetStatus.CANCELLED)) + val inProgress = (BetFilter.IN_PROGRESS in filters) and (bet.status in listOf( + BetStatus.IN_PROGRESS, + BetStatus.WAITING, + BetStatus.CLOSING + )) + + (public || invitation) && (finished or inProgress) + }.map { it } + } + } } override suspend fun getToConfirm(token: String): List { diff --git a/src/data/src/main/java/fr/iut/alldev/allin/data/api/model/ApiUser.kt b/src/data/src/main/java/fr/iut/alldev/allin/data/api/model/ApiUser.kt index 79562d4..a2562dc 100644 --- a/src/data/src/main/java/fr/iut/alldev/allin/data/api/model/ApiUser.kt +++ b/src/data/src/main/java/fr/iut/alldev/allin/data/api/model/ApiUser.kt @@ -1,6 +1,7 @@ package fr.iut.alldev.allin.data.api.model import androidx.annotation.Keep +import fr.iut.alldev.allin.data.model.FriendStatus import fr.iut.alldev.allin.data.model.User import kotlinx.serialization.Serializable @@ -19,13 +20,15 @@ data class ResponseUser( val username: String, val email: String, var nbCoins: Int, - var token: String? = null + var token: String? = null, + val friendStatus: FriendStatus? = null ) { fun toUser() = User( id = id, username = username, email = email, - coins = nbCoins + coins = nbCoins, + friendStatus = friendStatus ) } diff --git a/src/data/src/main/java/fr/iut/alldev/allin/data/model/FriendStatus.kt b/src/data/src/main/java/fr/iut/alldev/allin/data/model/FriendStatus.kt new file mode 100644 index 0000000..f50f709 --- /dev/null +++ b/src/data/src/main/java/fr/iut/alldev/allin/data/model/FriendStatus.kt @@ -0,0 +1,7 @@ +package fr.iut.alldev.allin.data.model + +enum class FriendStatus { + FRIEND, + REQUESTED, + NOT_FRIEND +} \ No newline at end of file diff --git a/src/data/src/main/java/fr/iut/alldev/allin/data/model/User.kt b/src/data/src/main/java/fr/iut/alldev/allin/data/model/User.kt index 21d650b..08044e1 100644 --- a/src/data/src/main/java/fr/iut/alldev/allin/data/model/User.kt +++ b/src/data/src/main/java/fr/iut/alldev/allin/data/model/User.kt @@ -4,5 +4,6 @@ data class User( val id: String, val username: String, val email: String, - val coins: Int + val coins: Int, + val friendStatus: FriendStatus? = null ) \ No newline at end of file diff --git a/src/data/src/main/java/fr/iut/alldev/allin/data/repository/FriendRepository.kt b/src/data/src/main/java/fr/iut/alldev/allin/data/repository/FriendRepository.kt index 2acad60..7e47967 100644 --- a/src/data/src/main/java/fr/iut/alldev/allin/data/repository/FriendRepository.kt +++ b/src/data/src/main/java/fr/iut/alldev/allin/data/repository/FriendRepository.kt @@ -6,4 +6,5 @@ abstract class FriendRepository { abstract suspend fun getFriends(token: String): List abstract suspend fun add(token: String, username: String) abstract suspend fun remove(token: String, username: String) + abstract suspend fun searchNew(token: String, search: String): List } \ No newline at end of file diff --git a/src/data/src/main/java/fr/iut/alldev/allin/data/repository/impl/FriendRepositoryImpl.kt b/src/data/src/main/java/fr/iut/alldev/allin/data/repository/impl/FriendRepositoryImpl.kt index d9ec122..c201b9e 100644 --- a/src/data/src/main/java/fr/iut/alldev/allin/data/repository/impl/FriendRepositoryImpl.kt +++ b/src/data/src/main/java/fr/iut/alldev/allin/data/repository/impl/FriendRepositoryImpl.kt @@ -27,4 +27,11 @@ class FriendRepositoryImpl @Inject constructor( request = RequestFriend(username) ) } + + override suspend fun searchNew(token: String, search: String): List { + return api.searchFriend( + token = token.formatBearerToken(), + search = search + ).map { it.toUser() } + } } \ No newline at end of file