Add more interactions to member list. Fixes #1214

This commit is contained in:
Phillip Thelen 2019-08-16 15:14:05 +02:00
parent 863e44398d
commit 4d9a1fdc4e
14 changed files with 196 additions and 94 deletions

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="18dp" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="18dp">
<path android:fillColor="#000000" android:pathData="M6,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM18,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/>
</vector>

View file

@ -21,12 +21,26 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.habitrpg.android.habitica.ui.views.social.UsernameLabel
android:id="@+id/display_name_textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_marginBottom="@dimen/spacing_small" />
<androidx.legacy.widget.Space
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content" />
<ImageButton
android:id="@+id/more_button"
android:src="@drawable/ic_more_horiz_black_18dp"
android:background="@color/transparent"
android:layout_width="30dp"
android:layout_height="20dp"
android:layout_gravity="right"/>
</LinearLayout>
<TableLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
@ -115,5 +129,4 @@
</TableLayout>
</LinearLayout>
</LinearLayout>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/send_message"
android:icon="@drawable/menu_messages"
android:title="@string/send_message" />
<item android:id="@+id/transfer_ownership"
android:icon="@drawable/ic_arrow_drop_down_gray_48dp"
android:title="@string/transfer_ownership" />
<item android:id="@+id/remove"
android:icon="@drawable/ic_delete_black_24dp"
android:title="@string/remove_member" />
</menu>

View file

@ -947,4 +947,13 @@
<string name="sent_card">You sent a %s</string>
<string name="group_activity_summary_notif">New messages in %s</string>
<string name="inbox_messages_title">You received %d messages from %s</string>
<string name="remove">Remove</string>
<string name="transfer_ownership">Transfer Ownership</string>
<string name="remove_member">Remove Member</string>
<string name="send_message">Send Message</string>
<string name="transferred_ownership">Ownership transferred to %s</string>
<string name="transfer_ownership_confirm">Are you sure you want to transfer ownership to %s</string>
<string name="remove_member_confirm">Are you sure you want to remove %s from the group?</string>
<string name="transfer">Transfer</string>
<string name="removed_member">%s was removed from the group</string>
</resources>

View file

@ -175,7 +175,10 @@ interface ApiService {
fun createGroup(@Body item: Group): Flowable<HabitResponse<Group>>
@PUT("groups/{id}")
fun updateGroup(@Path("id") id: String, @Body item: Group): Flowable<HabitResponse<Void>>
fun updateGroup(@Path("id") id: String, @Body item: Group): Flowable<HabitResponse<Group>>
@POST("groups/{groupID}/removeMember/{userID}")
fun removeMemberFromGroup(@Path("groupID") groupID: String, @Path("userID") userID: String): Flowable<HabitResponse<Void>>
@GET("groups/{gid}/chat")
fun listGroupChat(@Path("gid") groupId: String): Flowable<HabitResponse<List<ChatMessage>>>

View file

@ -89,7 +89,6 @@ import com.habitrpg.android.habitica.ui.fragments.social.challenges.ChallengesOv
import com.habitrpg.android.habitica.ui.fragments.social.party.PartyDetailFragment;
import com.habitrpg.android.habitica.ui.fragments.social.party.PartyFragment;
import com.habitrpg.android.habitica.ui.fragments.social.party.PartyInviteFragment;
import com.habitrpg.android.habitica.ui.fragments.social.party.PartyMemberListFragment;
import com.habitrpg.android.habitica.ui.fragments.tasks.TaskRecyclerViewFragment;
import com.habitrpg.android.habitica.ui.fragments.tasks.TasksFragment;
import com.habitrpg.android.habitica.ui.viewmodels.GroupViewModel;
@ -179,8 +178,6 @@ public interface UserComponent {
void inject(PartyInviteFragment partyInviteFragment);
void inject(PartyMemberListFragment partyMemberListFragment);
void inject(ChatListFragment chatListFragment);
void inject(GroupInformationFragment groupInformationFragment);

View file

@ -129,7 +129,8 @@ interface ApiClient {
fun getGroup(groupId: String): Flowable<Group>
fun createGroup(group: Group): Flowable<Group>
fun updateGroup(id: String, item: Group): Flowable<Void>
fun updateGroup(id: String, item: Group): Flowable<Group>
fun removeMemberFromGroup(groupID: String, userID: String): Flowable<Void>
fun listGroupChat(groupId: String): Flowable<List<ChatMessage>>

View file

@ -40,7 +40,7 @@ interface SocialRepository : BaseRepository {
fun joinGroup(id: String?): Flowable<Group>
fun createGroup(name: String?, description: String?, leader: String?, type: String?, privacy: String?, leaderCreateChallenge: Boolean?): Flowable<Group>
fun updateGroup(group: Group?, name: String?, description: String?, leader: String?, leaderCreateChallenge: Boolean?): Flowable<Void>
fun updateGroup(group: Group?, name: String?, description: String?, leader: String?, leaderCreateChallenge: Boolean?): Flowable<Group>
fun retrieveGroups(type: String): Flowable<List<Group>>
fun getGroups(type: String): Flowable<RealmResults<Group>>
@ -65,6 +65,10 @@ interface SocialRepository : BaseRepository {
fun markPrivateMessagesRead(user: User?): Flowable<Void>
fun transferGroupOwnership(groupID: String, userID: String): Flowable<Group>
fun removeMemberFromGroup(groupID: String, userID: String): Flowable<List<Member>>
fun acceptQuest(user: User?, partyId: String = "party"): Flowable<Void>
fun rejectQuest(user: User?, partyId: String = "party"): Flowable<Void>

View file

@ -476,10 +476,14 @@ class ApiClientImpl//private OnHabitsAPIResult mResultListener;
return apiService.createGroup(group).compose(configureApiCallObserver())
}
override fun updateGroup(id: String, item: Group): Flowable<Void> {
override fun updateGroup(id: String, item: Group): Flowable<Group> {
return apiService.updateGroup(id, item).compose(configureApiCallObserver())
}
override fun removeMemberFromGroup(groupID: String, userID: String): Flowable<Void> {
return apiService.removeMemberFromGroup(groupID, userID).compose(configureApiCallObserver())
}
override fun listGroupChat(groupId: String): Flowable<List<ChatMessage>> {
return apiService.listGroupChat(groupId).compose(configureApiCallObserver())
}

View file

@ -20,6 +20,25 @@ import io.reactivex.functions.Consumer
import io.realm.RealmResults
class SocialRepositoryImpl(localRepository: SocialLocalRepository, apiClient: ApiClient, userID: String) : BaseRepositoryImpl<SocialLocalRepository>(localRepository, apiClient, userID), SocialRepository {
override fun transferGroupOwnership(groupID: String, userID: String): Flowable<Group> {
return localRepository.getGroup(groupID)
.map {
val group = localRepository.getUnmanagedCopy(it)
group.leaderID = userID
group
}
.flatMap {
apiClient.updateGroup(it.id, it)
}
}
override fun removeMemberFromGroup(groupID: String, userID: String): Flowable<List<Member>> {
return apiClient.removeMemberFromGroup(groupID, userID)
.flatMap {
retrieveGroupMembers(groupID, true)
}
}
override fun getChatmessage(messageID: String): Flowable<ChatMessage> {
return localRepository.getChatMessage(messageID)
}
@ -152,7 +171,7 @@ class SocialRepositoryImpl(localRepository: SocialLocalRepository, apiClient: Ap
}
}
override fun updateGroup(group: Group?, name: String?, description: String?, leader: String?, leaderCreateChallenge: Boolean?): Flowable<Void> {
override fun updateGroup(group: Group?, name: String?, description: String?, leader: String?, leaderCreateChallenge: Boolean?): Flowable<Group> {
if (group == null) {
return Flowable.empty()
}

View file

@ -23,7 +23,7 @@ class PartyMemberRecyclerViewAdapter(data: OrderedRealmCollection<Member>?, auto
override fun onBindViewHolder(holder: GroupMemberViewHolder, position: Int) {
data?.let {
holder.bind(it[position], leaderID)
holder.bind(it[position], leaderID, null)
holder.onClickEvent = {
userClickedEvents.onNext(it[position].id ?: "")
}

View file

@ -13,6 +13,7 @@ import com.facebook.drawee.view.SimpleDraweeView
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.components.UserComponent
import com.habitrpg.android.habitica.data.InventoryRepository
import com.habitrpg.android.habitica.data.SocialRepository
import com.habitrpg.android.habitica.extensions.inflate
import com.habitrpg.android.habitica.helpers.MainNavigationController
import com.habitrpg.android.habitica.helpers.RxErrorHandler
@ -22,15 +23,18 @@ import com.habitrpg.android.habitica.models.social.Group
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.modules.AppModule
import com.habitrpg.android.habitica.ui.activities.FullProfileActivity
import com.habitrpg.android.habitica.ui.activities.MainActivity
import com.habitrpg.android.habitica.ui.fragments.BaseFragment
import com.habitrpg.android.habitica.ui.fragments.inventory.items.ItemRecyclerFragment
import com.habitrpg.android.habitica.ui.helpers.*
import com.habitrpg.android.habitica.ui.viewHolders.GroupMemberViewHolder.GroupMemberViewHolder
import com.habitrpg.android.habitica.ui.viewmodels.PartyViewModel
import com.habitrpg.android.habitica.ui.views.HabiticaSnackbar
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.social.OldQuestProgressView
import io.reactivex.functions.Consumer
import io.realm.RealmResults
import net.pherth.android.emoji_library.EmojiEditText
import javax.inject.Inject
import javax.inject.Named
@ -39,6 +43,8 @@ class PartyDetailFragment : BaseFragment() {
var viewModel: PartyViewModel? = null
@Inject
lateinit var socialRepository: SocialRepository
@Inject
lateinit var inventoryRepository: InventoryRepository
@field:[Inject Named(AppModule.NAMED_USER_ID)]
@ -189,15 +195,83 @@ class PartyDetailFragment : BaseFragment() {
for (member in members) {
val memberView = membersWrapper?.inflate(R.layout.party_member, false) ?: continue
val viewHolder = GroupMemberViewHolder(memberView)
viewHolder.bind(member, leaderID ?: "")
viewHolder.bind(member, leaderID ?: "", viewModel?.getUserData()?.value?.id)
viewHolder.onClickEvent = {
FullProfileActivity.open(member.id ?: "")
}
viewHolder.sendMessageEvent = {
member.id?.let { showSendMessageToUserDialog(it, member.displayName) }
}
viewHolder.transferOwnershipEvent = {
member.id?.let { showTransferOwnerShipDialog(it, member.displayName) }
}
viewHolder.removeMemberEvent = {
member.id?.let { showRemoveMemberDialog(it, member.displayName) }
}
membersWrapper?.addView(memberView)
}
}
}
private fun showSendMessageToUserDialog(userID: String, username: String) {
val factory = LayoutInflater.from(context)
val newMessageView = factory.inflate(R.layout.profile_new_message_dialog, null)
val emojiEditText = newMessageView.findViewById<EmojiEditText>(R.id.edit_new_message_text)
val newMessageTitle = newMessageView.findViewById<TextView>(R.id.new_message_title)
newMessageTitle.text = String.format(getString(R.string.profile_send_message_to), username)
val addMessageDialog = context?.let { HabiticaAlertDialog(it) }
addMessageDialog?.addButton(android.R.string.ok, true) { _, _ ->
socialRepository.postPrivateMessage(userID, emojiEditText.text.toString())
.subscribe(Consumer {
(activity as? MainActivity)?.snackbarContainer?.let { it1 ->
HabiticaSnackbar.showSnackbar(it1,
String.format(getString(R.string.profile_message_sent_to), username), HabiticaSnackbar.SnackbarDisplayType.NORMAL)
}
}, RxErrorHandler.handleEmptyError())
activity?.dismissKeyboard()
}
addMessageDialog?.addButton(android.R.string.cancel, false) { _, _ -> activity?.dismissKeyboard() }
addMessageDialog?.setAdditionalContentView(newMessageView)
addMessageDialog?.show()
}
private fun showTransferOwnerShipDialog(userID: String, displayName: String) {
val dialog = context?.let { HabiticaAlertDialog(it) }
dialog?.addButton(R.string.transfer, true) { _, _ ->
socialRepository.transferGroupOwnership(viewModel?.groupID ?: "", userID)
.subscribe(Consumer {
(activity as? MainActivity)?.snackbarContainer?.let { it1 ->
HabiticaSnackbar.showSnackbar(it1,
String.format(getString(R.string.transferred_ownership), displayName), HabiticaSnackbar.SnackbarDisplayType.NORMAL)
}
}, RxErrorHandler.handleEmptyError())
activity?.dismissKeyboard()
}
dialog?.addButton(android.R.string.cancel, false) { _, _ -> activity?.dismissKeyboard() }
dialog?.setTitle(context?.getString(R.string.transfer_ownership_confirm, displayName))
dialog?.show()
}
private fun showRemoveMemberDialog(userID: String, displayName: String) {
val dialog = context?.let { HabiticaAlertDialog(it) }
dialog?.addButton(R.string.remove, true) { _, _ ->
socialRepository.removeMemberFromGroup(viewModel?.groupID ?: "", userID)
.subscribe(Consumer {
(activity as? MainActivity)?.snackbarContainer?.let { it1 ->
HabiticaSnackbar.showSnackbar(it1,
String.format(getString(R.string.removed_member), displayName), HabiticaSnackbar.SnackbarDisplayType.NORMAL)
}
}, RxErrorHandler.handleEmptyError())
activity?.dismissKeyboard()
}
dialog?.addButton(android.R.string.cancel, false) { _, _ -> activity?.dismissKeyboard() }
dialog?.setTitle(context?.getString(R.string.remove_member_confirm, displayName))
dialog?.show()
}
private fun inviteNewQuest() {
val fragment = ItemRecyclerFragment()
fragment.itemType = "quests"

View file

@ -1,76 +0,0 @@
package com.habitrpg.android.habitica.ui.fragments.social.party
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.components.UserComponent
import com.habitrpg.android.habitica.data.SocialRepository
import com.habitrpg.android.habitica.extensions.inflate
import com.habitrpg.android.habitica.helpers.RxErrorHandler
import com.habitrpg.android.habitica.ui.activities.FullProfileActivity
import com.habitrpg.android.habitica.ui.adapter.social.PartyMemberRecyclerViewAdapter
import com.habitrpg.android.habitica.ui.fragments.BaseFragment
import com.habitrpg.android.habitica.ui.helpers.SafeDefaultItemAnimator
import com.habitrpg.android.habitica.ui.helpers.bindView
import com.habitrpg.android.habitica.ui.viewmodels.PartyViewModel
import io.reactivex.functions.Consumer
import javax.inject.Inject
class PartyMemberListFragment : BaseFragment() {
var viewModel: PartyViewModel? = null
@Inject
lateinit var socialRepository: SocialRepository
private val recyclerView: androidx.recyclerview.widget.RecyclerView? by bindView(R.id.recyclerView)
private val refreshLayout: androidx.swiperefreshlayout.widget.SwipeRefreshLayout? by bindView(R.id.refreshLayout)
private var adapter: PartyMemberRecyclerViewAdapter? = null
private var partyId: String? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState)
return container?.inflate(R.layout.fragment_refresh_recyclerview)
}
override fun injectFragment(component: UserComponent) {
component.inject(this)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerView?.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(context)
adapter = PartyMemberRecyclerViewAdapter(null, true)
adapter?.getUserClickedEvents()?.subscribe(Consumer { userId -> FullProfileActivity.open(userId) }, RxErrorHandler.handleEmptyError())?.let { compositeSubscription.add(it) }
recyclerView?.adapter = adapter
recyclerView?.itemAnimator = SafeDefaultItemAnimator()
refreshLayout?.setOnRefreshListener { this.refreshMembers() }
getUsers()
}
private fun refreshMembers() {
setRefreshing(true)
compositeSubscription.add(socialRepository.retrieveGroupMembers(partyId ?: "", true).doOnComplete { setRefreshing(false) }.subscribe(Consumer { }, RxErrorHandler.handleEmptyError()))
}
private fun setRefreshing(isRefreshing: Boolean) {
refreshLayout?.isRefreshing = isRefreshing
}
fun setPartyId(id: String) {
this.partyId = id
}
private fun getUsers() {
if (partyId == null) {
return
}
compositeSubscription.add(socialRepository.getGroupMembers(partyId ?: "").firstElement().subscribe(Consumer { users ->
adapter?.updateData(users)
}, RxErrorHandler.handleEmptyError()))
}
}

View file

@ -1,8 +1,11 @@
package com.habitrpg.android.habitica.ui.viewHolders.GroupMemberViewHolder
import android.view.MenuItem
import android.view.View
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.widget.PopupMenu
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.models.members.Member
import com.habitrpg.android.habitica.models.user.Stats
@ -12,8 +15,11 @@ import com.habitrpg.android.habitica.ui.views.HabiticaIconsHelper
import com.habitrpg.android.habitica.ui.views.HabiticaProgressBar
import com.habitrpg.android.habitica.ui.views.social.UsernameLabel
class GroupMemberViewHolder(itemView: View) : androidx.recyclerview.widget.RecyclerView.ViewHolder(itemView) {
class GroupMemberViewHolder(itemView: View) : androidx.recyclerview.widget.RecyclerView.ViewHolder(itemView), PopupMenu.OnMenuItemClickListener {
private var currentUserID: String? = null
private var leaderID: String? = null
private val avatarView: AvatarView by bindView(R.id.avatarView)
private val displayNameTextView: UsernameLabel by bindView(R.id.display_name_textview)
private val sublineTextView: TextView by bindView(R.id.subline_textview)
@ -25,25 +31,57 @@ class GroupMemberViewHolder(itemView: View) : androidx.recyclerview.widget.Recyc
private val healthTextView: TextView by bindView(R.id.health_textview)
private val experienceTextView: TextView by bindView(R.id.experience_textview)
private val manaTextView: TextView by bindView(R.id.mana_textview)
private val moreButton: ImageButton by bindView(R.id.more_button)
//private val leaderTextView: TextView by bindView(R.id.leader_textview)
var onClickEvent: (() -> Unit)? = null
var sendMessageEvent: (() -> Unit)? = null
var removeMemberEvent: (() -> Unit)? = null
var transferOwnershipEvent: (() -> Unit)? = null
init {
buffIconView.setImageBitmap(HabiticaIconsHelper.imageOfBuffIcon())
itemView.setOnClickListener { onClickEvent?.invoke() }
moreButton.setOnClickListener { showOptionsPopup() }
}
fun bind(user: Member, leaderID: String?) {
private fun showOptionsPopup() {
val popup = PopupMenu(itemView.context, moreButton)
popup.setOnMenuItemClickListener(this)
val inflater = popup.menuInflater
inflater.inflate(R.menu.party_member_menu, popup.menu)
popup.menu.findItem(R.id.transfer_ownership).isVisible = currentUserID == leaderID
popup.menu.findItem(R.id.remove).isVisible = currentUserID == leaderID
popup.show()
}
override fun onMenuItemClick(item: MenuItem?): Boolean {
when (item?.itemId) {
R.id.send_message -> { sendMessageEvent?.invoke() }
R.id.transfer_ownership -> { transferOwnershipEvent?.invoke() }
R.id.remove -> { removeMemberEvent?.invoke() }
}
return true
}
fun bind(user: Member, leaderID: String?, userID: String?) {
avatarView.setAvatar(user)
this.leaderID = leaderID
this.currentUserID = userID
if (user.id == userID) {
moreButton.visibility = View.GONE
} else {
moreButton.visibility = View.VISIBLE
}
user.stats?.let {
healthBar.set(it.hp ?: 0.0, it.maxHealth?.toDouble() ?: 50.0)
healthTextView.text = "${it.hp?.toInt()} / ${it.maxHealth?.toInt()}"
healthTextView.text = "${it.hp?.toInt()} / ${it.maxHealth}"
experienceBar.set(it.exp ?: 0.0, it.toNextLevel?.toDouble() ?: 0.0)
experienceTextView.text = "${it.exp?.toInt()} / ${it.toNextLevel?.toInt()}"
experienceTextView.text = "${it.exp?.toInt()} / ${it.toNextLevel}"
manaBar.set(it.mp ?: 0.0, it.maxMP?.toDouble() ?: 0.0)
manaTextView.text = "${it.mp?.toInt()} / ${it.maxMP?.toInt()}"
manaTextView.text = "${it.mp?.toInt()} / ${it.maxMP}"
}
displayNameTextView.username = user.profile?.name
displayNameTextView.tier = user.contributor?.level ?: 0