Commit 8526671b authored by Konstantin Tskhovrebov's avatar Konstantin Tskhovrebov 🤖

Merge branch 'feature/project_members_view' into 'feature/project_members_presentation'

Implement project members view.

See merge request terrakok/gitlab-client!194
parents 79e2957b 2f71a8eb
......@@ -36,6 +36,7 @@ import ru.terrakok.gitlabclient.ui.project.info.ProjectInfoFragment
import ru.terrakok.gitlabclient.ui.project.issues.ProjectIssuesContainerFragment
import ru.terrakok.gitlabclient.ui.project.issues.ProjectIssuesFragment
import ru.terrakok.gitlabclient.ui.project.labels.ProjectLabelsFragment
import ru.terrakok.gitlabclient.ui.project.members.ProjectMembersFragment
import ru.terrakok.gitlabclient.ui.project.mergerequest.ProjectMergeRequestsContainerFragment
import ru.terrakok.gitlabclient.ui.project.mergerequest.ProjectMergeRequestsFragment
import ru.terrakok.gitlabclient.ui.project.milestones.ProjectMilestonesFragment
......@@ -193,6 +194,10 @@ object Screens {
override fun getFragment() = ProjectMilestonesFragment()
}
object ProjectMembers : SupportAppScreen() {
override fun getFragment() = ProjectMembersFragment()
}
object ProjectFiles : SupportAppScreen() {
override fun getFragment() = ProjectFilesFragment()
}
......
......@@ -3,7 +3,7 @@ package ru.terrakok.gitlabclient.entity
import com.google.gson.annotations.SerializedName
data class Member(
@SerializedName("id") val id: Int,
@SerializedName("id") val id: Long,
@SerializedName("username") val username: String,
@SerializedName("name") val name: String,
@SerializedName("state") val state: String?,
......
......@@ -2,11 +2,11 @@ package ru.terrakok.gitlabclient.model.repository.members
import io.reactivex.Completable
import io.reactivex.Single
import ru.terrakok.gitlabclient.di.DefaultPageSize
import ru.terrakok.gitlabclient.di.PrimitiveWrapper
import ru.terrakok.gitlabclient.entity.Member
import ru.terrakok.gitlabclient.model.data.server.GitlabApi
import ru.terrakok.gitlabclient.model.system.SchedulersProvider
import ru.terrakok.gitlabclient.toothpick.PrimitiveWrapper
import ru.terrakok.gitlabclient.toothpick.qualifier.DefaultPageSize
import javax.inject.Inject
/**
......
package ru.terrakok.gitlabclient.presentation.project.members
import com.arellomobile.mvp.InjectViewState
import ru.terrakok.cicerone.Screen
import ru.terrakok.gitlabclient.Screens
import ru.terrakok.gitlabclient.di.PrimitiveWrapper
import ru.terrakok.gitlabclient.di.ProjectId
import ru.terrakok.gitlabclient.entity.Member
import ru.terrakok.gitlabclient.model.interactor.members.MembersInteractor
import ru.terrakok.gitlabclient.model.system.flow.FlowRouter
import ru.terrakok.gitlabclient.presentation.global.BasePresenter
import ru.terrakok.gitlabclient.presentation.global.ErrorHandler
import ru.terrakok.gitlabclient.presentation.global.Paginator
import ru.terrakok.gitlabclient.toothpick.PrimitiveWrapper
import ru.terrakok.gitlabclient.toothpick.qualifier.ProjectId
import javax.inject.Inject
/**
......@@ -73,8 +72,10 @@ class ProjectMembersPresenter @Inject constructor(
//TODO Member Flow(refactor this logic when Member Flow was be ready).
router.startFlow(Screens.UserFlow(userId))
}
fun refreshMembers() = paginator.refresh()
fun loadNextMembersPage() = paginator.loadNewPage()
fun onBackPressed() = router.exit()
override fun onDestroy() {
super.onDestroy()
......
package ru.terrakok.gitlabclient.ui.global.list
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.hannesdorfmann.adapterdelegates4.AdapterDelegate
import kotlinx.android.synthetic.main.item_member.view.*
import ru.terrakok.gitlabclient.R
import ru.terrakok.gitlabclient.entity.Member
import ru.terrakok.gitlabclient.extension.inflate
import ru.terrakok.gitlabclient.extension.loadRoundedImage
/**
* @author Valentin Logvinovitch (glvvl) on 28.02.19.
*/
class MembersAdapterDelegate(
private val clickListener: (Long) -> Unit
) : AdapterDelegate<MutableList<Any>>() {
override fun isForViewType(items: MutableList<Any>, position: Int) =
items[position] is Member
override fun onCreateViewHolder(parent: ViewGroup): RecyclerView.ViewHolder =
ViewHolder(parent.inflate(R.layout.item_member))
override fun onBindViewHolder(
items: MutableList<Any>,
position: Int,
viewHolder: RecyclerView.ViewHolder,
payloads: MutableList<Any>
) = (viewHolder as ViewHolder).bind(items[position] as Member)
private inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
private val guestTitle = view.context.getString(R.string.members_guest)
private val reporterTitle = view.context.getString(R.string.members_reporter)
private val developerTitle = view.context.getString(R.string.members_developer)
private val maintainerTitle = view.context.getString(R.string.members_maintainer)
private val ownerTitle = view.context.getString(R.string.members_owner)
private lateinit var data: Member
init {
view.setOnClickListener { clickListener(data.id) }
}
fun bind(data: Member) {
this.data = data
with(itemView) {
avatarImageView.loadRoundedImage(data.avatarUrl)
nameTextView.text = data.name
roleTextView.text = data.accessLevel.accessToString()
}
}
private fun Long.accessToString(): String =
when (this) {
10L -> guestTitle
20L -> reporterTitle
30L -> developerTitle
40L -> maintainerTitle
50L -> ownerTitle
else -> throw IllegalArgumentException("You must provide correct value to accessLevel")
}
}
}
\ No newline at end of file
......@@ -38,16 +38,18 @@ class ProjectInfoContainerFragment : BaseFragment() {
0 -> Screens.ProjectInfo.fragment
1 -> Screens.ProjectEvents.fragment
2 -> Screens.ProjectLabels.fragment
else -> Screens.ProjectMilestones.fragment
3 -> Screens.ProjectMilestones.fragment
else -> Screens.ProjectMembers.fragment
}
override fun getCount() = 4
override fun getCount() = 5
override fun getPageTitle(position: Int) = when (position) {
0 -> getString(R.string.project_info)
1 -> getString(R.string.project_events)
2 -> getString(R.string.project_labels)
3 -> getString(R.string.project_milestones)
4 -> getString(R.string.project_members)
else -> null
}
}
......
package ru.terrakok.gitlabclient.ui.project.members
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.hannesdorfmann.adapterdelegates4.ListDelegationAdapter
import ru.terrakok.gitlabclient.entity.Member
import ru.terrakok.gitlabclient.ui.global.list.MembersAdapterDelegate
import ru.terrakok.gitlabclient.ui.global.list.ProgressAdapterDelegate
import ru.terrakok.gitlabclient.ui.global.list.ProgressItem
/*
* @author Valentin Logvinovitch (glvvl) on 28.02.19.
*/
class MembersAdapter(
clickListener: (Long) -> Unit,
private val nextPageListener: () -> Unit
) : ListDelegationAdapter<MutableList<Any>>() {
init {
items = mutableListOf()
delegatesManager.addDelegate(MembersAdapterDelegate(clickListener))
delegatesManager.addDelegate(ProgressAdapterDelegate())
}
fun setData(events: List<Member>) {
val oldData = items.toList()
val progress = isProgress()
items.clear()
items.addAll(events)
if (progress) items.add(ProgressItem())
//yes, on main thread...
DiffUtil
.calculateDiff(DiffCallback(items, oldData), false)
.dispatchUpdatesTo(this)
}
fun showProgress(isVisible: Boolean) {
val oldData = items.toList()
val currentProgress = isProgress()
if (isVisible && !currentProgress) {
items.add(ProgressItem())
notifyItemInserted(items.lastIndex)
} else if (!isVisible && currentProgress) {
items.remove(items.last())
notifyItemRemoved(oldData.lastIndex)
}
}
private fun isProgress() = items.isNotEmpty() && items.last() is ProgressItem
override fun onBindViewHolder(
holder: RecyclerView.ViewHolder,
position: Int,
payloads: MutableList<Any?>
) {
super.onBindViewHolder(holder, position, payloads)
if (position == items.size - 10) nextPageListener()
}
private inner class DiffCallback(
private val newItems: List<Any>,
private val oldItems: List<Any>
) : DiffUtil.Callback() {
override fun getOldListSize() = oldItems.size
override fun getNewListSize() = newItems.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldItems[oldItemPosition]
val newItem = newItems[newItemPosition]
return if (newItem is Member && oldItem is Member) {
newItem.id == oldItem.id && newItem.accessLevel == oldItem.accessLevel && newItem.username == oldItem.username
} else {
newItem is ProgressItem && oldItem is ProgressItem
}
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldItems[oldItemPosition]
val newItem = newItems[newItemPosition]
return if (newItem is Member && oldItem is Member) {
newItem == oldItem
} else {
true
}
}
}
}
\ No newline at end of file
package ru.terrakok.gitlabclient.ui.project.members
import android.os.Bundle
import androidx.recyclerview.widget.LinearLayoutManager
import com.arellomobile.mvp.presenter.InjectPresenter
import com.arellomobile.mvp.presenter.ProvidePresenter
import kotlinx.android.synthetic.main.layout_base_list.*
import ru.terrakok.gitlabclient.R
import ru.terrakok.gitlabclient.entity.Member
import ru.terrakok.gitlabclient.extension.showSnackMessage
import ru.terrakok.gitlabclient.extension.visible
import ru.terrakok.gitlabclient.presentation.project.members.ProjectMembersPresenter
import ru.terrakok.gitlabclient.presentation.project.members.ProjectMembersView
import ru.terrakok.gitlabclient.ui.global.BaseFragment
/**
* @author Valentin Logvinovitch (glvvl) on 28.02.19.
*/
class ProjectMembersFragment : BaseFragment(), ProjectMembersView {
override val layoutRes = R.layout.fragment_project_members
@InjectPresenter
lateinit var presenter: ProjectMembersPresenter
@ProvidePresenter
fun providePresenter(): ProjectMembersPresenter =
scope.getInstance(ProjectMembersPresenter::class.java)
private val adapter by lazy {
MembersAdapter(
{ presenter.onMemberClick(it) },
{ presenter.loadNextMembersPage() }
)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
recyclerView.apply {
layoutManager = LinearLayoutManager(context)
setHasFixedSize(true)
adapter = this@ProjectMembersFragment.adapter
}
swipeToRefresh.setOnRefreshListener { presenter.refreshMembers() }
emptyView.setRefreshListener { presenter.refreshMembers() }
}
override fun showRefreshProgress(show: Boolean) {
postViewAction { swipeToRefresh.isRefreshing = show }
}
override fun showEmptyProgress(show: Boolean) {
fullscreenProgressView.visible(show)
//trick for disable and hide swipeToRefresh on fullscreen progress
swipeToRefresh.visible(!show)
postViewAction { swipeToRefresh.isRefreshing = false }
}
override fun showPageProgress(show: Boolean) {
postViewAction { adapter.showProgress(show) }
}
override fun showEmptyView(show: Boolean) {
emptyView.apply { if (show) showEmptyData() else hide() }
}
override fun showEmptyError(show: Boolean, message: String?) {
emptyView.apply { if (show) showEmptyError(message) else hide() }
}
override fun showMembers(show: Boolean, members: List<Member>) {
recyclerView.visible(show)
postViewAction { adapter.setData(members) }
}
override fun showMessage(message: String) {
showSnackMessage(message)
}
override fun onBackPressed() {
presenter.onBackPressed()
}
}
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#000000"
android:pathData="M16.5,12c1.38,0 2.49,-1.12 2.49,-2.5S17.88,7 16.5,7C15.12,7 14,8.12 14,9.5s1.12,2.5 2.5,2.5zM9,11c1.66,0 2.99,-1.34 2.99,-3S10.66,5 9,5C7.34,5 6,6.34 6,8s1.34,3 3,3zM16.5,14c-1.83,0 -5.5,0.92 -5.5,2.75L11,19h11v-2.25c0,-1.83 -3.67,-2.75 -5.5,-2.75zM9,13c-2.33,0 -7,1.17 -7,3.5L2,19h7v-2.25c0,-0.85 0.33,-2.34 2.37,-3.47C10.5,13.1 9.66,13 9,13z" />
</vector>
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/layout_base_list"/>
</FrameLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground">
<ImageView
android:id="@+id/avatarImageView"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="16dp"
android:layout_marginTop="12dp"
android:layout_marginBottom="12dp"
android:src="@drawable/default_img"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/nameTextView"
style="@style/TextTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:ellipsize="end"
android:lines="1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/roleTextView"
app:layout_constraintStart_toEndOf="@+id/avatarImageView"
app:layout_constraintTop_toTopOf="parent"
tools:text="Konstantin Tskhovrebov" />
<TextView
android:id="@+id/roleTextView"
android:layout_width="70dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Maintainer" />
</androidx.constraintlayout.widget.ConstraintLayout>
......@@ -177,6 +177,7 @@
<string name="project_events">Activity</string>
<string name="project_labels">Labels</string>
<string name="project_milestones">Milestones</string>
<string name="project_members">Members</string>
<string name="project_files">Files</string>
<string name="project_milestone_date">%s ― %s</string>
<string name="milestone_active">Opened</string>
......@@ -186,11 +187,18 @@
<string name="project_files_show_branches">Branches</string>
<string name="project_files_default_path">Project Files</string>
<string name="project_files_no_branches">You have no branches</string>
<!--Privacy policy screen-->
<string name="privacy_policy">Privacy policy</string>
<!--Label screen-->
<string name="label_description_empty">No description</string>
<!--Members screen-->
<string name="members_guest">Guest</string>
<string name="members_reporter">Reporter</string>
<string name="members_developer">Developer</string>
<string name="members_maintainer">Maintainer</string>
<string name="members_owner">Owner</string>
</resources>
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment