Commit 50f56045 authored by Konstantin Tskhovrebov's avatar Konstantin Tskhovrebov 🤖

Merge branch 'feature/avatar_custom_view' into 'develop'

Create user avatar view with built-in navigation.

See merge request terrakok/gitlab-client!205
parents ed35351f 00740d38
package ru.terrakok.gitlabclient.entity.app
import ru.terrakok.gitlabclient.entity.Commit
import ru.terrakok.gitlabclient.entity.ShortUser
/**
* Created by Eugene Shapovalov (@CraggyHaggy) on 20.10.18.
*/
data class CommitWithAvatarUrl(val commit: Commit, val authorAvatarUrl: String?)
\ No newline at end of file
data class CommitWithShortUser(val commit: Commit, val shortUser: ShortUser?)
\ No newline at end of file
......@@ -24,8 +24,6 @@ import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.DrawableCompat
import androidx.fragment.app.Fragment
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.google.android.material.snackbar.Snackbar
import retrofit2.adapter.rxjava2.Result
import ru.terrakok.cicerone.Navigator
......@@ -140,20 +138,6 @@ fun Fragment.sendEmail(email: String?) {
}
}
fun ImageView.loadRoundedImage(
url: String?,
ctx: Context? = null
) {
Glide.with(ctx ?: context)
.load(url)
.apply(RequestOptions().apply {
placeholder(R.drawable.default_img)
error(R.drawable.default_img)
})
.apply(RequestOptions.circleCropTransform())
.into(this)
}
fun TargetHeader.Public.openInfo(router: FlowRouter) {
when (target) {
AppTarget.PROJECT -> {
......
......@@ -7,7 +7,7 @@ import org.threeten.bp.ZonedDateTime
import ru.terrakok.gitlabclient.di.DefaultPageSize
import ru.terrakok.gitlabclient.di.PrimitiveWrapper
import ru.terrakok.gitlabclient.entity.*
import ru.terrakok.gitlabclient.entity.app.CommitWithAvatarUrl
import ru.terrakok.gitlabclient.entity.app.CommitWithShortUser
import ru.terrakok.gitlabclient.entity.app.target.*
import ru.terrakok.gitlabclient.entity.event.EventAction
import ru.terrakok.gitlabclient.entity.mergerequest.MergeRequest
......@@ -230,11 +230,11 @@ class MergeRequestRepository @Inject constructor(
.zip(
getAllMergeRequestParticipants(projectId, mergeRequestId),
api.getMergeRequestCommits(projectId, mergeRequestId, page, pageSize),
BiFunction<List<ShortUser>, List<Commit>, List<CommitWithAvatarUrl>> { participants, commits ->
BiFunction<List<ShortUser>, List<Commit>, List<CommitWithShortUser>> { participants, commits ->
commits.map { commit ->
CommitWithAvatarUrl(
CommitWithShortUser(
commit,
participants.find { it.name == commit.authorName || it.username == commit.authorName }?.avatarUrl
participants.find { it.name == commit.authorName || it.username == commit.authorName }
)
}
}
......
package ru.terrakok.gitlabclient.presentation.issue.info
import com.arellomobile.mvp.InjectViewState
import ru.terrakok.gitlabclient.Screens
import ru.terrakok.gitlabclient.di.IssueId
import ru.terrakok.gitlabclient.di.PrimitiveWrapper
import ru.terrakok.gitlabclient.di.ProjectId
import ru.terrakok.gitlabclient.entity.ShortUser
import ru.terrakok.gitlabclient.model.interactor.issue.IssueInteractor
import ru.terrakok.gitlabclient.model.system.flow.FlowRouter
import ru.terrakok.gitlabclient.presentation.global.BasePresenter
import ru.terrakok.gitlabclient.presentation.global.ErrorHandler
import javax.inject.Inject
......@@ -20,8 +17,7 @@ class IssueInfoPresenter @Inject constructor(
@ProjectId private val projectIdWrapper: PrimitiveWrapper<Long>,
@IssueId private val issueIdWrapper: PrimitiveWrapper<Long>,
private val issueInteractor: IssueInteractor,
private val errorHandler: ErrorHandler,
private val router: FlowRouter
private val errorHandler: ErrorHandler
) : BasePresenter<IssueInfoView>() {
private val projectId = projectIdWrapper.value
......@@ -40,6 +36,4 @@ class IssueInfoPresenter @Inject constructor(
)
.connect()
}
fun onAssigneeClicked(assignee: ShortUser) = router.startFlow(Screens.UserFlow(assignee.id))
}
\ No newline at end of file
......@@ -4,7 +4,7 @@ import com.arellomobile.mvp.InjectViewState
import ru.terrakok.gitlabclient.di.MergeRequestId
import ru.terrakok.gitlabclient.di.PrimitiveWrapper
import ru.terrakok.gitlabclient.di.ProjectId
import ru.terrakok.gitlabclient.entity.app.CommitWithAvatarUrl
import ru.terrakok.gitlabclient.entity.app.CommitWithShortUser
import ru.terrakok.gitlabclient.model.interactor.mergerequest.MergeRequestInteractor
import ru.terrakok.gitlabclient.presentation.global.BasePresenter
import ru.terrakok.gitlabclient.presentation.global.ErrorHandler
......@@ -35,7 +35,7 @@ class MergeRequestCommitsPresenter @Inject constructor(
private val paginator = Paginator(
{ page -> mrInteractor.getMergeRequestCommits(projectId, mrId, page) },
object : Paginator.ViewController<CommitWithAvatarUrl> {
object : Paginator.ViewController<CommitWithShortUser> {
override fun showEmptyProgress(show: Boolean) {
viewState.showEmptyProgress(show)
}
......@@ -56,7 +56,7 @@ class MergeRequestCommitsPresenter @Inject constructor(
viewState.showEmptyView(show)
}
override fun showData(show: Boolean, data: List<CommitWithAvatarUrl>) {
override fun showData(show: Boolean, data: List<CommitWithShortUser>) {
viewState.showCommits(show, data)
}
......
......@@ -4,7 +4,7 @@ import com.arellomobile.mvp.MvpView
import com.arellomobile.mvp.viewstate.strategy.AddToEndSingleStrategy
import com.arellomobile.mvp.viewstate.strategy.OneExecutionStateStrategy
import com.arellomobile.mvp.viewstate.strategy.StateStrategyType
import ru.terrakok.gitlabclient.entity.app.CommitWithAvatarUrl
import ru.terrakok.gitlabclient.entity.app.CommitWithShortUser
/**
* Created by Eugene Shapovalov (@CraggyHaggy) on 20.10.18.
......@@ -17,7 +17,7 @@ interface MergeRequestCommitsView : MvpView {
fun showPageProgress(show: Boolean)
fun showEmptyView(show: Boolean)
fun showEmptyError(show: Boolean, message: String?)
fun showCommits(show: Boolean, commits: List<CommitWithAvatarUrl>)
fun showCommits(show: Boolean, commits: List<CommitWithShortUser>)
@StateStrategyType(OneExecutionStateStrategy::class)
fun showMessage(message: String)
......
......@@ -40,6 +40,4 @@ class MergeRequestInfoPresenter @Inject constructor(
)
.connect()
}
fun onAssigneeClicked(assignee: ShortUser) = router.startFlow(Screens.UserFlow(assignee.id))
}
\ No newline at end of file
......@@ -9,7 +9,6 @@ import kotlinx.android.synthetic.main.item_user_acount.view.*
import ru.terrakok.gitlabclient.R
import ru.terrakok.gitlabclient.entity.app.session.UserAccount
import ru.terrakok.gitlabclient.extension.inflate
import ru.terrakok.gitlabclient.extension.loadRoundedImage
import ru.terrakok.gitlabclient.extension.visible
import ru.terrakok.gitlabclient.presentation.drawer.NavigationDrawerPresenter
import ru.terrakok.gitlabclient.presentation.drawer.NavigationDrawerView
......@@ -17,6 +16,7 @@ import ru.terrakok.gitlabclient.presentation.drawer.NavigationDrawerView.MenuIte
import ru.terrakok.gitlabclient.presentation.drawer.NavigationDrawerView.MenuItem.*
import ru.terrakok.gitlabclient.ui.global.BaseFragment
import ru.terrakok.gitlabclient.ui.global.MessageDialogFragment
import ru.terrakok.gitlabclient.ui.global.view.custom.bindUserAccount
/**
* @author Konstantin Tskhovrebov (aka terrakok). Date: 04.04.17
......@@ -71,13 +71,13 @@ class NavigationDrawerFragment : BaseFragment(), NavigationDrawerView, MessageDi
override fun setAccounts(accounts: List<UserAccount>, currentAccount: UserAccount) {
nickTV.text = currentAccount.userName
serverNameTV.text = currentAccount.serverPath
avatarImageView.loadRoundedImage(currentAccount.avatarUrl, context)
avatarImageView.bindUserAccount(currentAccount, true)
accountsContainer.removeAllViews()
accounts.forEach { acc ->
accountsContainer.inflate(R.layout.item_user_acount)
.apply {
avatarImageView.loadRoundedImage(acc.avatarUrl, context)
avatarImageView.bindUserAccount(acc, false)
nameTextView.text = acc.userName
serverTextView.text = acc.serverPath
selectorView.visible(acc == currentAccount)
......
......@@ -7,13 +7,11 @@ import ru.terrakok.gitlabclient.entity.ShortUser
/**
* Created by Eugene Shapovalov (@CraggyHaggy) on 26.05.19.
*/
class AssigneesAdapter(
clickListener: (ShortUser) -> Unit
) : ListDelegationAdapter<MutableList<ShortUser>>() {
class AssigneesAdapter : ListDelegationAdapter<MutableList<ShortUser>>() {
init {
items = mutableListOf()
delegatesManager.addDelegate(AssigneesAdapterDelegate(clickListener))
delegatesManager.addDelegate(AssigneesAdapterDelegate())
}
fun setData(assignees: List<ShortUser>) {
......
......@@ -5,19 +5,15 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.hannesdorfmann.adapterdelegates4.AdapterDelegate
import kotlinx.android.synthetic.main.item_assignee.view.*
import kotlinx.android.synthetic.main.item_target_header_public.view.avatarImageView
import kotlinx.android.synthetic.main.item_target_header_public.view.titleTextView
import ru.terrakok.gitlabclient.R
import ru.terrakok.gitlabclient.entity.ShortUser
import ru.terrakok.gitlabclient.extension.inflate
import ru.terrakok.gitlabclient.extension.loadRoundedImage
import ru.terrakok.gitlabclient.ui.global.view.custom.bindShortUser
/**
* Created by Eugene Shapovalov (@CraggyHaggy) on 26.05.19.
*/
class AssigneesAdapterDelegate(
private val clickListener: (ShortUser) -> Unit
) : AdapterDelegate<MutableList<ShortUser>>() {
class AssigneesAdapterDelegate : AdapterDelegate<MutableList<ShortUser>>() {
override fun isForViewType(items: MutableList<ShortUser>, position: Int) = true
......@@ -35,16 +31,12 @@ class AssigneesAdapterDelegate(
private lateinit var item: ShortUser
init {
view.setOnClickListener { clickListener(item) }
}
fun bind(item: ShortUser) {
this.item = item
with(itemView) {
titleTextView.text = item.name
subtitleTextView.text = item.username
avatarImageView.loadRoundedImage(item.avatarUrl)
avatarImageView.bindShortUser(item)
}
}
}
......
......@@ -6,10 +6,10 @@ import androidx.recyclerview.widget.RecyclerView
import com.hannesdorfmann.adapterdelegates4.AdapterDelegate
import kotlinx.android.synthetic.main.item_merge_request_commit.view.*
import ru.terrakok.gitlabclient.R
import ru.terrakok.gitlabclient.entity.app.CommitWithAvatarUrl
import ru.terrakok.gitlabclient.entity.app.CommitWithShortUser
import ru.terrakok.gitlabclient.extension.humanTime
import ru.terrakok.gitlabclient.extension.inflate
import ru.terrakok.gitlabclient.extension.loadRoundedImage
import ru.terrakok.gitlabclient.ui.global.view.custom.bindShortUser
/**
* Created by Eugene Shapovalov (@CraggyHaggy) on 20.10.18.
......@@ -17,7 +17,7 @@ import ru.terrakok.gitlabclient.extension.loadRoundedImage
class CommitAdapterDelegate : AdapterDelegate<MutableList<Any>>() {
override fun isForViewType(items: MutableList<Any>, position: Int) =
items[position] is CommitWithAvatarUrl
items[position] is CommitWithShortUser
override fun onCreateViewHolder(parent: ViewGroup): RecyclerView.ViewHolder =
ViewHolder(parent.inflate(R.layout.item_merge_request_commit))
......@@ -27,20 +27,23 @@ class CommitAdapterDelegate : AdapterDelegate<MutableList<Any>>() {
position: Int,
viewHolder: RecyclerView.ViewHolder,
payloads: MutableList<Any>
) = (viewHolder as ViewHolder).bind(items[position] as CommitWithAvatarUrl)
) = (viewHolder as ViewHolder).bind(items[position] as CommitWithShortUser)
private inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
private lateinit var commitWithAvatarUrl: CommitWithAvatarUrl
private lateinit var commitWithShortUser: CommitWithShortUser
fun bind(commitWithAvatarUrl: CommitWithAvatarUrl) {
this.commitWithAvatarUrl = commitWithAvatarUrl
fun bind(commitWithShortUser: CommitWithShortUser) {
this.commitWithShortUser = commitWithShortUser
with(itemView) {
avatarImageView.loadRoundedImage(commitWithAvatarUrl.authorAvatarUrl)
titleTextView.text = commitWithAvatarUrl.commit.title
val shortUser = commitWithShortUser.shortUser
if (shortUser != null) {
avatarImageView.bindShortUser(shortUser)
}
titleTextView.text = commitWithShortUser.commit.title
descriptionTextView.text = String.format(
context.getString(R.string.merge_request_commits_description),
commitWithAvatarUrl.commit.authorName,
commitWithAvatarUrl.commit.authoredDate.humanTime(resources)
commitWithShortUser.commit.authorName,
commitWithShortUser.commit.authoredDate.humanTime(resources)
)
}
}
......
......@@ -10,8 +10,8 @@ import ru.terrakok.gitlabclient.entity.Project
import ru.terrakok.gitlabclient.entity.Visibility
import ru.terrakok.gitlabclient.extension.getTintDrawable
import ru.terrakok.gitlabclient.extension.inflate
import ru.terrakok.gitlabclient.extension.loadRoundedImage
import ru.terrakok.gitlabclient.extension.setStartDrawable
import ru.terrakok.gitlabclient.ui.global.view.custom.bindProject
/**
* @author Konstantin Tskhovrebov (aka terrakok) on 18.06.17.
......@@ -63,7 +63,7 @@ class ProjectAdapterDelegate(private val clickListener: (Project) -> Unit) : Ada
else -> R.drawable.ic_globe_18dp
}
)
avatarImageView.loadRoundedImage(project.avatarUrl)
avatarImageView.bindProject(project)
}
}
}
......
......@@ -3,7 +3,7 @@ package ru.terrakok.gitlabclient.ui.global.list
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.hannesdorfmann.adapterdelegates4.ListDelegationAdapter
import ru.terrakok.gitlabclient.entity.app.CommitWithAvatarUrl
import ru.terrakok.gitlabclient.entity.app.CommitWithShortUser
/**
* Created by Eugene Shapovalov (@CraggyHaggy) on 20.10.18.
......@@ -18,7 +18,7 @@ class TargetCommitsAdapter(
delegatesManager.addDelegate(ProgressAdapterDelegate())
}
fun setData(data: List<CommitWithAvatarUrl>) {
fun setData(data: List<CommitWithShortUser>) {
val oldItems = items.toList()
items.clear()
......@@ -66,7 +66,7 @@ class TargetCommitsAdapter(
val oldItem = oldItems[oldItemPosition]
val newItem = newItems[newItemPosition]
return if (newItem is CommitWithAvatarUrl && oldItem is CommitWithAvatarUrl) {
return if (newItem is CommitWithShortUser && oldItem is CommitWithShortUser) {
newItem.commit.id == oldItem.commit.id
} else {
newItem is ProgressItem && oldItem is ProgressItem
......@@ -77,7 +77,7 @@ class TargetCommitsAdapter(
val oldItem = oldItems[oldItemPosition]
val newItem = newItems[newItemPosition]
return if (newItem is CommitWithAvatarUrl && oldItem is CommitWithAvatarUrl) {
return if (newItem is CommitWithShortUser && oldItem is CommitWithShortUser) {
newItem == oldItem
} else {
false
......
......@@ -14,6 +14,7 @@ import ru.terrakok.gitlabclient.entity.app.target.TargetBadgeIcon
import ru.terrakok.gitlabclient.entity.app.target.TargetHeader
import ru.terrakok.gitlabclient.entity.app.target.TargetHeaderIcon
import ru.terrakok.gitlabclient.extension.*
import ru.terrakok.gitlabclient.ui.global.view.custom.bindShortUser
/**
* @author Konstantin Tskhovrebov (aka terrakok) on 18.06.17.
......@@ -70,7 +71,7 @@ class TargetHeaderPublicAdapterDelegate(
titleTextView.text = item.title.getHumanName(resources)
Markwon.setText(descriptionTextView, item.body)
descriptionTextView.movementMethod = null //disable internal link click
avatarImageView.loadRoundedImage(item.author.avatarUrl)
avatarImageView.bindShortUser(item.author)
iconImageView.setImageResource(item.icon.getIcon())
dateTextView.text = item.date.humanTime(resources)
......
......@@ -10,8 +10,8 @@ import ru.terrakok.gitlabclient.R
import ru.terrakok.gitlabclient.entity.Note
import ru.terrakok.gitlabclient.extension.humanTime
import ru.terrakok.gitlabclient.extension.inflate
import ru.terrakok.gitlabclient.extension.loadRoundedImage
import ru.terrakok.gitlabclient.presentation.global.NoteWithFormattedBody
import ru.terrakok.gitlabclient.ui.global.view.custom.bindShortUser
/**
* @author Konstantin Tskhovrebov (aka terrakok) on 18.06.17.
......@@ -37,7 +37,7 @@ class UserNoteAdapterDelegate : AdapterDelegate<MutableList<Any>>() {
fun bind(data: NoteWithFormattedBody) {
this.note = data.note
with(itemView) {
avatarImageView.loadRoundedImage(note.author.avatarUrl)
avatarImageView.bindShortUser(note.author)
titleTextView.text = note.author.name
subtitleTextView.text = note.createdAt.humanTime(context.resources)
Markwon.setText(descriptionTextView, data.body)
......
package ru.terrakok.gitlabclient.ui.global.view.custom
import android.content.Context
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.View
import android.widget.FrameLayout
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.Target
import kotlinx.android.synthetic.main.view_avatar.view.*
import ru.terrakok.cicerone.Router
import ru.terrakok.gitlabclient.R
import ru.terrakok.gitlabclient.Screens
import ru.terrakok.gitlabclient.di.DI
import ru.terrakok.gitlabclient.entity.Project
import ru.terrakok.gitlabclient.entity.ShortUser
import ru.terrakok.gitlabclient.entity.User
import ru.terrakok.gitlabclient.entity.app.session.UserAccount
import ru.terrakok.gitlabclient.extension.getTintDrawable
import toothpick.Toothpick
class AvatarView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
private val shortNameBackgroundColors = intArrayOf(
R.color.green,
R.color.silver,
R.color.grey,
R.color.fruit_salad,
R.color.red,
R.color.brandy_punch,
R.color.blue,
R.color.mariner
)
private val whiteCircle = context.getTintDrawable(R.drawable.circle, R.color.white)
init {
View.inflate(context, R.layout.view_avatar, this)
}
fun setData(id: Long, name: String, avatarUrl: String?, clickAction: () -> Unit = {}) {
resetAvatar()
val names = name.split(" ")
val shortName = when (names.size) {
0 -> ""
1 -> getShortNameFromSingleWord(names[0])
else -> getShortNameFromMultipleWords(names)
}
if (shortName.length == SHORT_NAME_LENGTH) {
avatarShortName.text = shortName
val shortNameBackgroundColor = shortNameBackgroundColors[(id % shortNameBackgroundColors.size).toInt()]
avatarShortName.background = context.getTintDrawable(R.drawable.circle, shortNameBackgroundColor)
} else {
avatarImage.setImageResource(R.drawable.default_img)
}
Glide.with(avatarImage)
.load(avatarUrl)
.listener(
object : RequestListener<Drawable> {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable>?,
isFirstResource: Boolean
): Boolean {
return true
}
override fun onResourceReady(
resource: Drawable?,
model: Any?,
target: Target<Drawable>?,
dataSource: DataSource?,
isFirstResource: Boolean
): Boolean {
avatarImage.background = whiteCircle
return false
}
}
)
.apply(RequestOptions.circleCropTransform())
.into(avatarImage)
setOnClickListener { clickAction() }
}
private fun resetAvatar() {
avatarImage.background = null
Glide.with(avatarImage)
.clear(avatarImage)
}
private fun getShortNameFromSingleWord(name: String): String {
val result = StringBuilder()
name.forEach {
if (it.isLetter()) {
result.append(it.toUpperCase())
}
if (result.length == SHORT_NAME_LENGTH) {
return result.toString()
}
}
return result.toString()
}
private fun getShortNameFromMultipleWords(names: List<String>): String {
val result = StringBuilder()
names.forEach {
val letter = it.find { char -> char.isLetter() }
if (letter != null) {
result.append(letter.toUpperCase())
}
if (result.length == SHORT_NAME_LENGTH) {
return result.toString()
}
}
return result.toString()
}
companion object {
private const val SHORT_NAME_LENGTH = 2
}
}
fun AvatarView.bindShortUser(shortUser: ShortUser) {
with(shortUser) {
val router = Toothpick.openScope(DI.APP_SCOPE).getInstance(Router::class.java)
setData(id, name, avatarUrl) { router.navigateTo(Screens.UserFlow(id)) }
}
}
fun AvatarView.bindUser(user: User) {
with(user) {
setData(id, name, avatarUrl)
}
}
fun AvatarView.bindUserAccount(userAccount: UserAccount, withNavigation: Boolean) {
with(userAccount) {
if (withNavigation) {
val router = Toothpick.openScope(DI.APP_SCOPE).getInstance(Router::class.java)
setData(userId, userName, avatarUrl) { router.navigateTo(Screens.UserFlow(userId)) }
} else {
setData(userId, userName, avatarUrl)
}
}
}
fun AvatarView.bindProject(project: Project, withNavigation: Boolean = true) {
with(project) {
if (withNavigation) {
val router = Toothpick.openScope(DI.APP_SCOPE).getInstance(Router::class.java)
setData(id, name, avatarUrl) { router.navigateTo(Screens.ProjectFlow(id)) }