Commit f66f6925 authored by Niklas Fix's avatar Niklas Fix 🎓

add recommended users section

parent a32489c6
......@@ -36,6 +36,7 @@ class FriendsListFragment : Fragment(),
FriendsListContract.View,
FriendsListAdapter.FriendOnClickHandler,
SearchListAdapter.UserOnClickHandler,
RecommendedUsersAdapter.RecommendedUserOnClickHandler,
SearchView.OnQueryTextListener {
override lateinit var presenter: FriendsListContract.Presenter
......@@ -62,14 +63,19 @@ class FriendsListFragment : Fragment(),
me!!,
this
)
presenter.ID_ME = me?.id!!
searchView_friends.setOnQueryTextListener(this)
setSearchViewOnCloseListenerHelper(view)
nestedScollView_friends.visibility = View.GONE
searchView_friends.visibility = View.GONE
recyclerView_friends_list.visibility = View.GONE
layout_dotloader_friends_list.visibility = View.VISIBLE
nestedScollView_friends.isNestedScrollingEnabled = false
presenter.fetchAllUsers()
presenter.fetchFriends()
presenter.getRunningDuels()
}
......@@ -82,9 +88,10 @@ class FriendsListFragment : Fragment(),
}
private fun onCloseButtonClick(closeButton: View) {
recyclerView_friends_list.visibility = View.GONE
nestedScollView_friends.visibility = View.GONE
layout_dotloader_friends_list.visibility = View.VISIBLE
closeButton.visibility = View.GONE
presenter.fetchAllUsers()
presenter.fetchFriends()
presenter.getRunningDuels()
}
......@@ -93,6 +100,9 @@ class FriendsListFragment : Fragment(),
mFriends = friends
layout_dotloader_friends_list.visibility = View.GONE
layout_load_failed_friends_list.visibility = View.GONE
textView_friends_recommended_users.visibility = View.VISIBLE
textView_your_friends.visibility = View.VISIBLE
nestedScollView_friends.visibility = View.VISIBLE
searchView_friends.visibility = View.VISIBLE
recyclerView_friends_list.visibility = View.VISIBLE
friendsListAdapter = FriendsListAdapter(friends, duels, requireActivity(), presenter, this)
......@@ -106,9 +116,25 @@ class FriendsListFragment : Fragment(),
}
override fun bindSearchListAdapter(users: ArrayList<User>) {
searchListAdapter = SearchListAdapter(users, mFriends, presenter, this, requireActivity())
override fun bindRecommendedUsersAdapter(
allUsers: ArrayList<User>,
friends: ArrayList<User>,
duels: ArrayList<Duel>
) {
nestedScollView_friends.visibility = View.VISIBLE
textView_friends_recommended_users.visibility = View.VISIBLE
textView_your_friends.visibility = View.VISIBLE
recyclerView_recommended_user_list.visibility = View.VISIBLE
recyclerView_recommended_user_list.adapter = RecommendedUsersAdapter(allUsers, duels, presenter, this, requireActivity())
recyclerView_recommended_user_list.layoutManager = LinearLayoutManager(requireActivity(), RecyclerView.HORIZONTAL, false)
}
override fun bindSearchListAdapter(users: ArrayList<User>, duels: ArrayList<Duel>) {
searchListAdapter = SearchListAdapter(users, mFriends, duels, presenter, this, requireActivity())
recyclerView_friends_list.adapter = searchListAdapter
recyclerView_recommended_user_list.visibility = View.GONE
textView_friends_recommended_users.visibility = View.GONE
textView_your_friends.visibility = View.GONE
}
override fun onAddFriendClicked(user: User) {
......@@ -118,7 +144,7 @@ class FriendsListFragment : Fragment(),
override fun showFetchFriendsFailed(reason: String) {
if(this.isVisible) {
searchView_friends.visibility = View.GONE
recyclerView_friends_list.visibility = View.GONE
nestedScollView_friends.visibility = View.GONE
layout_dotloader_friends_list.visibility = View.GONE
layout_load_failed_friends_list.visibility = View.VISIBLE
val displayProblem = getString(R.string.failed_to_fetch_friends) + "\n" + reason
......@@ -126,7 +152,9 @@ class FriendsListFragment : Fragment(),
layout_load_failed_friends_list
.button_retry
.setOnClickListener {
presenter.fetchAllUsers()
presenter.fetchFriends()
presenter.getRunningDuels()
}
}
}
......@@ -134,7 +162,7 @@ class FriendsListFragment : Fragment(),
override fun showFetchDuelsFailed(reason: String) {
if(this.isVisible) {
searchView_friends.visibility = View.GONE
recyclerView_friends_list.visibility = View.GONE
nestedScollView_friends.visibility = View.GONE
layout_dotloader_friends_list.visibility = View.GONE
layout_load_failed_friends_list.visibility = View.VISIBLE
val displayProblem = getString(R.string.failed_to_fetch_duels) + "\n" + reason
......@@ -143,7 +171,7 @@ class FriendsListFragment : Fragment(),
}
override fun onChallengeClicked(friend: User, callback: FriendsListAdapter.ChallengeSentCallback) {
override fun onChallengeClicked(friend: User, callback: ChallengeSentCallback) {
presenter.sendChallengeRequest(friend, callback)
}
......@@ -175,7 +203,7 @@ class FriendsListFragment : Fragment(),
}
override fun showSendChallengeRequestFailed(reason: String) {
Toast.makeText(requireActivity(), resources.getString(R.string.error_reason, reason), Toast.LENGTH_SHORT).show()
Toast.makeText(requireActivity(), resources.getString(R.string.user_could_not_be_challenged, reason), Toast.LENGTH_SHORT).show()
}
override fun showSendChallengeRequestFailedNoPoolsSelected() {
......@@ -269,6 +297,18 @@ class FriendsListFragment : Fragment(),
searchListAdapter?.notifyDataSetChanged()
}
override fun onAddRecommendedUserClicked(user: User) {
presenter.addFriend(user)
}
override fun onChallengeRecommendedUserClicked(user: User, callback: ChallengeSentCallback) {
presenter.sendChallengeRequest(user, callback)
}
override fun onChallengeUserClicked(user: User, challengeSentCallback: ChallengeSentCallback) {
presenter.sendChallengeRequest(user, challengeSentCallback)
}
companion object {
fun newInstance() = FriendsListFragment()
}
......
package de.akamu.tudarmstadt.features.dashboard
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import de.akamu.tudarmstadt.R
import de.akamu.tudarmstadt.features.friends.ChallengeSentCallback
import de.akamu.tudarmstadt.features.friends.FriendsListContract
import de.akamu.tudarmstadt.model.Duel
import de.akamu.tudarmstadt.model.User
import de.akamu.tudarmstadt.util.AkamuResource
import de.akamu.tudarmstadt.util.Constants
class RecommendedUsersAdapter(
var recommendedUsers: ArrayList<User>,
var mDuels: ArrayList<Duel>,
var presenter: FriendsListContract.Presenter,
var mRecommendedUserOnClickHandler: RecommendedUserOnClickHandler,
var context: Context
) : RecyclerView.Adapter<RecommendedUsersAdapter.RecommendedUserViewHolder>() {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): RecommendedUsersAdapter.RecommendedUserViewHolder {
val view = LayoutInflater
.from(parent.context)
.inflate(R.layout.item_recommended_user, parent, false)
view.isFocusable = true
return RecommendedUserViewHolder(view)
}
override fun getItemCount(): Int {
return Constants.MAX_RECOMMENDATIONS
}
override fun onBindViewHolder(holder: RecommendedUsersAdapter.RecommendedUserViewHolder, position: Int) {
holder.bind(recommendedUsers[position])
}
interface RecommendedUserOnClickHandler {
fun onAddRecommendedUserClicked(user: User)
fun onChallengeRecommendedUserClicked(user: User, callback: ChallengeSentCallback)
}
inner class RecommendedUserViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView),
View.OnClickListener, ChallengeSentCallback {
lateinit var mUser: User
var mUserAvatar: ImageView = itemView.findViewById(R.id.imageView_recommended_user)
var mUsername : TextView = itemView.findViewById(R.id.textView_recommended_user_name)
var mTitle : TextView = itemView.findViewById(R.id.textView2_recommended_user_title)
var mChallenged : TextView = itemView.findViewById(R.id.textView_recommended_user_challenged)
var mAdded : TextView = itemView.findViewById(R.id.textView_recommended_user_added)
var mAddButton : Button = itemView.findViewById(R.id.button_recommended_user_add)
var mChallengeButton : Button = itemView.findViewById(R.id.button_recommended_user_challenge)
init {
mAddButton.setOnClickListener(this)
mChallengeButton.setOnClickListener(this)
}
override fun onClick(v: View?) {
if (v?.id == mAddButton.id) {
mRecommendedUserOnClickHandler.onAddRecommendedUserClicked(mUser)
mAddButton.visibility = View.INVISIBLE
mAdded.visibility = View.VISIBLE
} else if (v?.id == mChallengeButton.id) {
mRecommendedUserOnClickHandler.onChallengeRecommendedUserClicked(mUser, this)
}
}
fun bind(user: User) {
mUser = user
mUsername.text = user.username
mTitle.text = user.title.name
AkamuResource.smartLoad(mUserAvatar, user.avatar.image, context)
if (alreadyChallenged(user)) {
mChallengeButton.visibility = View.INVISIBLE
mChallenged.visibility = View.VISIBLE
}
}
private fun alreadyChallenged(user: User): Boolean {
for (duel in mDuels) {
if (duel.isRunning && duel.participants != null && duel.participants.isNotEmpty()) {
for (p in duel.participants) {
if (p.id == user.id.toInt()) {
return true
}
}
}
}
return false
}
override fun indicateChallengeSentSuccessfully() {
mChallengeButton.visibility = View.INVISIBLE
mChallenged.visibility = View.VISIBLE
}
override fun indicateChallengeSentFailed() {
// no indication necessary
}
}
}
package de.akamu.tudarmstadt.features.friends
interface ChallengeSentCallback {
fun indicateChallengeSentSuccessfully()
fun indicateChallengeSentFailed()
}
\ No newline at end of file
......@@ -66,11 +66,6 @@ class FriendsListAdapter(
fun onDeleteFriendClicked(friend: User, callback: RemoveFriendCallback)
}
interface ChallengeSentCallback {
fun indicateChallengeSentSuccessfully()
fun indicateChallengeSentFailed()
}
interface RemoveFriendCallback {
fun indicateRemoveFriendSuccessfully()
fun indicateRemoveFriendFailed()
......
......@@ -4,6 +4,7 @@ import de.akamu.tudarmstadt.BasePresenter
import de.akamu.tudarmstadt.BaseView
import de.akamu.tudarmstadt.model.Duel
import de.akamu.tudarmstadt.model.User
import java.util.*
interface FriendsListContract {
......@@ -12,6 +13,8 @@ interface FriendsListContract {
* The later can be user to indicate whether a new challenge request can be send or not.
*/
fun bindFriendListAdapter(friends: ArrayList<User>, duels: ArrayList<Duel>)
/** Bind RecommendedUsersAdapter **/
fun bindRecommendedUsersAdapter(allUsers: ArrayList<User>, friends: ArrayList<User>, duels: ArrayList<Duel>)
/** Let the user know that fetching his/her friends has failed due to [reason] **/
fun showFetchFriendsFailed(reason: String)
/** Let the user know that sending a challenge request has failed due to [reason] **/
......@@ -23,7 +26,7 @@ interface FriendsListContract {
/** Let the user know that sending a challenge request was successful **/
fun showSendChallengeRequestSuccess(friendName: String)
/** Bind the SearchListAdapter to the recycler view by passing a list of users **/
fun bindSearchListAdapter(users: ArrayList<User>)
fun bindSearchListAdapter(users: ArrayList<User>, duels: ArrayList<Duel>)
/** Let the user know that [user] was added to his/her friends **/
fun showUserAddedAsFriend(user: User)
/** Let the user know that the clicked user couldn't be added to his/her friends due to [reason] **/
......@@ -37,12 +40,15 @@ interface FriendsListContract {
}
interface Presenter : BasePresenter {
var ID_ME: Long
/** Fetch the user's friends list **/
fun fetchFriends()
/** Send a challenge request **/
fun sendChallengeRequest(friend: User, callback: FriendsListAdapter.ChallengeSentCallback)
fun sendChallengeRequest(friend: User, callback: ChallengeSentCallback)
/** Fetch a list of all available users that start with [startsWith] **/
fun findUsersThatStartWith(startsWith : String)
/** Fetch all users **/
fun fetchAllUsers()
/** Add an user to your friends list **/
fun addFriend(user: User)
/** Deletes a friend from your friends list **/
......
......@@ -6,6 +6,8 @@ import de.akamu.tudarmstadt.interactors.DuelInteractor
import de.akamu.tudarmstadt.interactors.UserInteractor
import de.akamu.tudarmstadt.model.Duel
import de.akamu.tudarmstadt.model.User
import de.akamu.tudarmstadt.util.AppUserUtil
import de.akamu.tudarmstadt.util.Constants
class FriendsListPresenter(
private val friendsInteractor: FriendsListInteractor,
......@@ -15,16 +17,35 @@ class FriendsListPresenter(
private var view: FriendsListContract.View?
) : FriendsListContract.Presenter {
lateinit var allUsers: ArrayList<User>
lateinit var mFriends: ArrayList<User>
lateinit var mDuels: ArrayList<Duel>
private var userFetched: Boolean = false
private var friendsFetched: Boolean = false
private var duelsFetched: Boolean = false
override var ID_ME: Long = 0
init {
view?.presenter = this
}
override fun fetchAllUsers() {
friendsInteractor.getAllUsers(object : FriendsDataSource.GetAllUsersCallback {
override fun onGetAllUsersSuccess(all: ArrayList<User>) {
allUsers = all
userFetched = true
handleFriendsAndDuelCallback()
}
override fun onGetAllUsersFailed(reason: String) {
// silent?
}
})
}
override fun fetchFriends() {
friendsInteractor.fetchFriends(object : FriendsDataSource.FetchFriendsCallback {
override fun onFetchFriendsSuccess(friends: ArrayList<User>) {
......@@ -54,12 +75,46 @@ class FriendsListPresenter(
}
private fun handleFriendsAndDuelCallback() {
if(friendsFetched && duelsFetched) {
if(friendsFetched && duelsFetched && userFetched) {
view?.bindRecommendedUsersAdapter(getRecommendedUsers(allUsers), mFriends, mDuels)
view?.bindFriendListAdapter(mFriends, mDuels)
}
}
override fun sendChallengeRequest(friend: User, callback: FriendsListAdapter.ChallengeSentCallback) {
private fun getRecommendedUsers(allUsers: ArrayList<User>): ArrayList<User> {
val generatedIndices: ArrayList<Int> = arrayListOf()
val recommendedUsers: ArrayList<User> = arrayListOf()
val allUsersCopy: ArrayList<User> = arrayListOf()
allUsersCopy.addAll(allUsers)
val allUsersIterator = allUsersCopy.listIterator()
// Make sure we don't recommend someone who is already a friend
while (allUsersIterator.hasNext()) {
val user = allUsersIterator.next()
// Remove yourself fromm the list
if (user.id == ID_ME) {
allUsersIterator.remove()
}
for (friend in mFriends) {
if (user.username == friend.username) {
allUsersIterator.remove()
}
}
}
for (i in 0 until Constants.MAX_RECOMMENDATIONS) {
var index = (0 until allUsersCopy.size).random()
// Make sure we do not recommend the same user twice or more
while (generatedIndices.contains(index)) {
index = (0 until allUsersCopy.size).random()
}
generatedIndices.add(index)
recommendedUsers.add(allUsersCopy[index])
}
return recommendedUsers
}
override fun sendChallengeRequest(friend: User, callback: ChallengeSentCallback) {
friendsInteractor.sendChallengeRequest(friend, object : FriendsDataSource.ChallengeRequestCallback {
override fun onChallengeRequestSuccess(friendName: String) {
view?.showSendChallengeRequestSuccess(friendName)
......@@ -85,23 +140,14 @@ class FriendsListPresenter(
}
override fun findUsersThatStartWith(startsWith: String) {
friendsInteractor.getAllUsers(object : FriendsDataSource.GetAllUsersCallback {
override fun onGetAllUsersSuccess(all: ArrayList<User>) {
val usersThatStartsWith = ArrayList<User>()
all.forEach {
if (it.username.startsWith(startsWith, true)
&& it.username != me.username) {
usersThatStartsWith.add(it)
}
}
view?.bindSearchListAdapter(usersThatStartsWith)
val usersThatStartsWith = ArrayList<User>()
allUsers.forEach {
if (it.username.startsWith(startsWith, true)
&& it.username != me.username) {
usersThatStartsWith.add(it)
}
override fun onGetAllUsersFailed(reason: String) {
view?.showFetchFriendsFailed(reason)
}
})
}
view?.bindSearchListAdapter(usersThatStartsWith, mDuels)
}
override fun addFriend(user: User) {
......
......@@ -7,22 +7,30 @@ import android.view.View
import android.view.ViewGroup
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import de.akamu.tudarmstadt.R
import de.akamu.tudarmstadt.model.Duel
import de.akamu.tudarmstadt.model.User
import de.akamu.tudarmstadt.util.AkamuResource
import de.akamu.tudarmstadt.util.SortOrder
import kotlin.math.abs
class SearchListAdapter(var users : ArrayList<User>,
var friends: ArrayList<User>,
var presenter: FriendsListContract.Presenter,
var mUserOnClickHandler: UserOnClickHandler,
var context: Context)
: RecyclerView.Adapter<SearchListAdapter.UserViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchListAdapter.UserViewHolder {
class SearchListAdapter(
var users: ArrayList<User>,
var friends: ArrayList<User>,
var duels: ArrayList<Duel>,
var presenter: FriendsListContract.Presenter,
var mUserOnClickHandler: UserOnClickHandler,
var context: Context
) : RecyclerView.Adapter<SearchListAdapter.UserViewHolder>() {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): SearchListAdapter.UserViewHolder {
val view = LayoutInflater
.from(parent.context)
.inflate(R.layout.item_searched_users, parent, false)
......@@ -38,9 +46,9 @@ class SearchListAdapter(var users : ArrayList<User>,
holder.bind(users[position])
}
private fun isAlreadyFriend(friends: ArrayList<User>, user: User) : Boolean {
friends.forEach{
if(it.username == user.username) {
private fun isAlreadyFriend(friends: ArrayList<User>, user: User): Boolean {
friends.forEach {
if (it.username == user.username) {
return true
}
}
......@@ -48,7 +56,7 @@ class SearchListAdapter(var users : ArrayList<User>,
}
fun sort(order: SortOrder): Boolean {
return when(order) {
return when (order) {
SortOrder.ALPHABETIC -> {
users.sortWith(compareBy { it.username })
notifyDataSetChanged()
......@@ -70,39 +78,42 @@ class SearchListAdapter(var users : ArrayList<User>,
interface UserOnClickHandler {
fun onAddFriendClicked(user: User)
fun onChallengeUserClicked(user: User, challengeSentCallback: ChallengeSentCallback)
}
inner class UserViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
inner class UserViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView),
View.OnClickListener, ChallengeSentCallback {
private var mFriendName : TextView = itemView.findViewById(R.id.textView_item_searched_user_username)
private var mFriendTitle : TextView = itemView.findViewById(R.id.textView_item_searched_user_title)
private var mFriendAvatar : ImageView = itemView.findViewById(R.id.imageView_item_searched_user_avatar)
private var mAddFriendButton : ImageButton = itemView.findViewById(R.id.imageButton_item_searched_user_add)
lateinit var mUser: User
// attributes for preventing rapid clicking
private val MINIMUM_INTERVAL: Long = 500
private var lastClicked: Long = 0
private var mFriendName: TextView =
itemView.findViewById(R.id.textView_item_searched_user_username)
private var mFriendTitle: TextView =
itemView.findViewById(R.id.textView_item_searched_user_title)
private var mFriendAvatar: ImageView =
itemView.findViewById(R.id.imageView_item_searched_user_avatar)
private var mAddFriendButton: ImageButton =
itemView.findViewById(R.id.imageButton_item_searched_user_add)
private var mChallengeUserButton: ImageButton =
itemView.findViewById(R.id.imageButton_item_searched_user_challenge)
private var mProgressBarSentChallenge: ProgressBar =
itemView.findViewById(R.id.progressbar_item_searched_user_challenge)
init {
mAddFriendButton.setOnClickListener(this)
mChallengeUserButton.setOnClickListener(this)
}
override fun onClick(v: View?) {
if(v!!.id == mAddFriendButton.id) {
val previousClickTimestamp = lastClicked
val currentTimestamp = SystemClock.uptimeMillis()
lastClicked = currentTimestamp
if (previousClickTimestamp == 0L || abs(currentTimestamp - previousClickTimestamp) > MINIMUM_INTERVAL) {
if (adapterPosition >= 0) {
val clickedUser = users[adapterPosition]
mUserOnClickHandler.onAddFriendClicked(clickedUser)
}
}
if (v?.id == mAddFriendButton.id) {
mUserOnClickHandler.onAddFriendClicked(mUser)
} else if (v?.id == mChallengeUserButton.id) {
mUserOnClickHandler.onChallengeUserClicked(mUser, this)
}
}
fun bind(user: User) {
mUser = user
mFriendName.text = user.username
mFriendTitle.text = user.title.name
AkamuResource.smartLoad(mFriendAvatar, user.avatar.image, context)
......@@ -111,8 +122,42 @@ class SearchListAdapter(var users : ArrayList<User>,
isAlreadyFriend(friends, user) -> View.GONE
else -> View.VISIBLE
}
if (alreadyChallenged(user)) {
mChallengeUserButton.isEnabled = false
mChallengeUserButton.background =
ContextCompat.getDrawable(context, R.drawable.md_transparent)
mChallengeUserButton.setImageResource(R.drawable