Commit 155b1eae authored by Konstantin Tskhovrebov's avatar Konstantin Tskhovrebov 🤖

Merge branch 'develop'

parents b9885a11 03faf777
......@@ -15,8 +15,8 @@ android {
minSdkVersion 19
targetSdkVersion 28
versionName "1.2.1"
versionCode 8
versionName "1.3.0"
versionCode 9
buildConfigField "String", "VERSION_UID", '"' + getBuildUid() + '"'
buildConfigField "String", "APP_DESCRIPTION", '"Gitfox is an Android client for Gitlab."'
......@@ -74,6 +74,7 @@ ext {
toothpickVersion = "1.0.6"
retrofitVersion = "2.2.0"
markwonVersion = "1.1.1"
glideVersion = "4.8.0"
}
dependencies {
......@@ -85,7 +86,7 @@ dependencies {
//Kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
//Log
implementation "com.jakewharton.timber:timber:4.5.1"
implementation "com.jakewharton.timber:timber:4.7.0"
//MVP Moxy
kapt "com.arello-mobile:moxy-compiler:$moxyVersion"
implementation "com.arello-mobile:moxy-app-compat:$moxyVersion"
......@@ -102,13 +103,17 @@ dependencies {
implementation "com.squareup.okhttp3:logging-interceptor:3.11.0"
implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofitVersion"
//RxJava
implementation "io.reactivex.rxjava2:rxandroid:2.0.1"
implementation "io.reactivex.rxjava2:rxjava:2.2.0"
implementation "io.reactivex.rxjava2:rxandroid:2.1.0"
implementation "io.reactivex.rxjava2:rxjava:2.2.2"
implementation 'com.jakewharton.rxrelay2:rxrelay:2.0.0'
//Adapter simplify
implementation "com.hannesdorfmann:adapterdelegates3:3.0.1"
//Image load and cache
implementation "com.github.bumptech.glide:glide:4.7.1"
implementation "com.github.bumptech.glide:glide:$glideVersion"
kapt "com.github.bumptech.glide:compiler:$glideVersion"
implementation ('com.github.bumptech.glide:okhttp3-integration:4.6.1') {
exclude group: 'glide-parent'
}
//Markdown to HTML converter
implementation "ru.noties:markwon:$markwonVersion"
implementation "ru.noties:markwon-image-loader:$markwonVersion"
......@@ -121,7 +126,7 @@ dependencies {
//FlexBox Layout
implementation 'com.google.android:flexbox:1.0.0'
//Firebase
implementation 'com.google.firebase:firebase-core:16.0.3'
implementation 'com.google.firebase:firebase-core:16.0.4'
//Crashlytics
implementation 'com.crashlytics.sdk.android:crashlytics:2.9.5'
......
......@@ -26,6 +26,7 @@
# We use ProGuard only for minify
-keep class ru.terrakok.** { *; }
-keep class com.bumptech.glide.GeneratedAppGlideModuleImpl
-dontwarn com.roughike.bottombar.VerticalScrollingBehavior$ScrollDirection
-dontwarn org.codehaus.mojo.animal_sniffer.*
......
......@@ -202,6 +202,10 @@ object Screens {
override fun getFragment() = MergeRequestNotesFragment()
}
object MergeRequestChanges : SupportAppScreen() {
override fun getFragment() = MergeRequestChangesFragment()
}
data class IssueFlow(
val projectId: Long,
val issueId: Long
......
......@@ -41,5 +41,6 @@ data class MergeRequest(
@SerializedName("closed_by") val closedBy: Author?,
@SerializedName("closed_at") val closedAt: LocalDateTime?,
@SerializedName("merged_by") val mergedBy: Author?,
@SerializedName("merged_at") val mergedAt: LocalDateTime?
@SerializedName("merged_at") val mergedAt: LocalDateTime?,
@SerializedName("changes") val changes: List<MergeRequestChange>?
)
package ru.terrakok.gitlabclient.entity.mergerequest
import com.google.gson.annotations.SerializedName
/**
* Created by Eugene Shapovalov (@CraggyHaggy) on 26.10.18.
*/
data class MergeRequestChange(
@SerializedName("old_path") val oldPath: String,
@SerializedName("new_path") val newPath: String,
@SerializedName("a_mode") val aMode: String,
@SerializedName("b_mode") val bMode: String,
@SerializedName("new_file") val newFile: Boolean,
@SerializedName("renamed_file") val renamedFile: Boolean,
@SerializedName("deleted_file") val deletedFile: Boolean,
@SerializedName("diff") val diff: String
)
\ No newline at end of file
package ru.terrakok.gitlabclient.glide
import android.content.Context
import com.bumptech.glide.Glide
import com.bumptech.glide.Registry
import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.module.AppGlideModule
import okhttp3.OkHttpClient
import ru.terrakok.gitlabclient.toothpick.DI
import toothpick.Toothpick
import java.io.InputStream
/**
* Created by Alexei Korshun on 25/10/2018.
*/
@GlideModule
class GlideModule : AppGlideModule() {
private val okHttpClient: OkHttpClient =
Toothpick
.openScope(DI.SERVER_SCOPE)
.getInstance(OkHttpClient::class.java)
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
val factory = OkHttpUrlLoader.Factory(okHttpClient)
glide.registry.replace(GlideUrl::class.java, InputStream::class.java, factory)
}
override fun isManifestParsingEnabled() = false
}
\ No newline at end of file
......@@ -255,4 +255,10 @@ interface GitlabApi {
@Query("page") page: Int,
@Query("per_page") pageSize: Int
): Single<List<Author>>
@GET("$API_PATH/projects/{project_id}/merge_requests/{merge_request_id}/changes")
fun getMergeRequestChanges(
@Path("project_id") projectId: Long,
@Path("merge_request_id") mergeRequestId: Long
): Single<MergeRequest>
}
\ No newline at end of file
......@@ -61,4 +61,9 @@ class MergeRequestInteractor @Inject constructor(
mergeRequestId: Long,
page: Int
) = mergeRequestRepository.getMergeRequestCommits(projectId, mergeRequestId, page)
fun getMergeRequestChanges(
projectId: Long,
mergeRequestId: Long
) = mergeRequestRepository.getMergeRequestChanges(projectId, mergeRequestId)
}
\ No newline at end of file
......@@ -226,7 +226,6 @@ class MergeRequestRepository @Inject constructor(
}
}
)
.subscribeOn(schedulers.io())
.observeOn(schedulers.ui())
......@@ -240,6 +239,12 @@ class MergeRequestRepository @Inject constructor(
.flatMapIterable { it }
.toList()
fun getMergeRequestChanges(projectId: Long, mergeRequestId: Long) =
api.getMergeRequestChanges(projectId, mergeRequestId)
.map { it.changes ?: arrayListOf() }
.subscribeOn(schedulers.io())
.observeOn(schedulers.ui())
companion object {
// See GitLab documentation: https://docs.gitlab.com/ee/api/#pagination.
private const val MAX_PAGE_SIZE = 100
......
package ru.terrakok.gitlabclient.presentation.mergerequest.changes
import com.arellomobile.mvp.InjectViewState
import ru.terrakok.gitlabclient.entity.mergerequest.MergeRequestChange
import ru.terrakok.gitlabclient.model.interactor.mergerequest.MergeRequestInteractor
import ru.terrakok.gitlabclient.presentation.global.BasePresenter
import ru.terrakok.gitlabclient.presentation.global.ErrorHandler
import ru.terrakok.gitlabclient.toothpick.PrimitiveWrapper
import ru.terrakok.gitlabclient.toothpick.qualifier.MergeRequestId
import ru.terrakok.gitlabclient.toothpick.qualifier.ProjectId
import javax.inject.Inject
/**
* Created by Konstantin Tskhovrebov (aka @terrakok) on 12.02.18.
*/
@InjectViewState
class MergeRequestChangesPresenter @Inject constructor(
@ProjectId projectIdWrapper: PrimitiveWrapper<Long>,
@MergeRequestId mrIdWrapper: PrimitiveWrapper<Long>,
private val mrInteractor: MergeRequestInteractor,
private val errorHandler: ErrorHandler
) : BasePresenter<MergeRequestChangesView>() {
private val projectId = projectIdWrapper.value
private val mrId = mrIdWrapper.value
private val changes = arrayListOf<MergeRequestChange>()
private var isEmptyError: Boolean = false
override fun onFirstViewAttach() {
super.onFirstViewAttach()
mrInteractor
.getMergeRequestChanges(projectId, mrId)
.doOnSubscribe { viewState.showEmptyProgress(true) }
.doAfterTerminate { viewState.showEmptyProgress(false) }
.subscribe(
{
if (it.isNotEmpty()) {
changes.addAll(it)
viewState.showChanges(true, it)
} else {
viewState.showChanges(false, it)
viewState.showEmptyView(true)
}
},
{
isEmptyError = true
errorHandler.proceed(it, { viewState.showEmptyError(true, it) })
}
)
.connect()
}
fun refreshChanges() {
mrInteractor
.getMergeRequestChanges(projectId, mrId)
.doOnSubscribe {
if (isEmptyError) {
viewState.showEmptyError(false, null)
isEmptyError = false
}
if (changes.isEmpty()) {
viewState.showEmptyView(false)
}
if (changes.isNotEmpty()) {
viewState.showRefreshProgress(true)
} else {
viewState.showEmptyProgress(true)
}
}
.subscribe(
{
if (changes.isNotEmpty()) {
viewState.showRefreshProgress(false)
} else {
viewState.showEmptyProgress(false)
}
changes.clear()
if (it.isNotEmpty()) {
changes.addAll(it)
viewState.showChanges(true, it)
} else {
viewState.showChanges(false, it)
viewState.showEmptyView(true)
}
},
{
if (changes.isNotEmpty()) {
viewState.showRefreshProgress(false)
} else {
viewState.showEmptyProgress(false)
}
errorHandler.proceed(
it,
{
if (changes.isNotEmpty()) {
viewState.showMessage(it)
} else {
isEmptyError = true
viewState.showEmptyError(true, it)
}
}
)
}
)
.connect()
}
}
\ No newline at end of file
package ru.terrakok.gitlabclient.presentation.mergerequest.changes
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.mergerequest.MergeRequestChange
/**
* Created by Eugene Shapovalov (@CraggyHaggy) on 26.10.18.
*/
@StateStrategyType(AddToEndSingleStrategy::class)
interface MergeRequestChangesView : MvpView {
fun showRefreshProgress(show: Boolean)
fun showEmptyProgress(show: Boolean)
fun showEmptyView(show: Boolean)
fun showEmptyError(show: Boolean, message: String?)
fun showChanges(show: Boolean, changes: List<MergeRequestChange>)
@StateStrategyType(OneExecutionStateStrategy::class)
fun showMessage(message: String)
}
\ No newline at end of file
/*
* Modifications copyright (C) 2018 https://github.com/alorma/GitDiffTextView
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ru.terrakok.gitlabclient.ui.global
import android.graphics.*
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.text.TextUtils
import android.text.style.ForegroundColorSpan
import android.text.style.LineBackgroundSpan
import android.widget.TextView
class GitDiffViewController(
private val view: TextView
) {
private val additionColor: Int = Color.parseColor("#CCFFCC")
private val deletionColor: Int = Color.parseColor("#FFDDDD")
private val additionTextColor: Int = Color.TRANSPARENT
private val deletionTextColor: Int = Color.TRANSPARENT
private var addedLineCount = 0
private var deletedLineCount = 0
fun setText(text: CharSequence) {
if (!TextUtils.isEmpty(text)) {
val diff = text.toString()
val split = diff.split("\\r?\\n|\\r".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
if (split.isNotEmpty()) {
val builder = SpannableStringBuilder()
val lines = split.size
for (i in 0 until lines) {
var token = split[i]
if (!token.startsWith("@@")) {
if (i < lines - 1) {
token += "\n"
}
val firstChar = token[0]
var color = Color.TRANSPARENT
var textColor = Color.TRANSPARENT
when (firstChar) {
'+' -> {
color = additionColor
textColor = additionTextColor
addedLineCount++
}
'-' -> {
color = deletionColor
textColor = deletionTextColor
deletedLineCount++
}
}
val spannableDiff = SpannableString(token)
// Span for line color (where transparent is considered as default)
if (color != Color.TRANSPARENT) {
val span = DiffLineSpan(color, view.paddingLeft)
spannableDiff.setSpan(span, 0, token.length, SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE)
}
// Span for text color (where transparent is considered as default)
if (textColor != Color.TRANSPARENT) {
val span = ForegroundColorSpan(textColor)
spannableDiff.setSpan(span, 0, token.length, SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE)
}
builder.append(spannableDiff)
}
}
view.text = builder
}
} else {
view.text = text
}
}
fun getAddedLineCount() = addedLineCount
fun getDeletedLineCount() = deletedLineCount
fun release() {
addedLineCount = 0
deletedLineCount = 0
}
private inner class DiffLineSpan(
private val color: Int,
private val padding: Int
) : LineBackgroundSpan {
private val mTmpRect = Rect()
override fun drawBackground(
c: Canvas,
p: Paint,
left: Int,
right: Int,
top: Int,
baseline: Int,
bottom: Int,
text: CharSequence,
start: Int,
end: Int,
lnum: Int
) {
// expand canvas bounds by padding
val clipBounds = c.clipBounds
clipBounds.inset(-padding, 0)
c.clipRect(clipBounds, Region.Op.INTERSECT)
val paintColor = p.color
p.color = color
mTmpRect.set(left - padding, top, right + padding, bottom)
c.drawRect(mTmpRect, p)
p.color = paintColor
}
}
}
\ No newline at end of file
package ru.terrakok.gitlabclient.ui.global.list
import android.support.v7.widget.RecyclerView
import android.view.View
import android.view.ViewGroup
import com.hannesdorfmann.adapterdelegates3.AdapterDelegate
import kotlinx.android.synthetic.main.item_merge_request_change.view.*
import ru.terrakok.gitlabclient.R
import ru.terrakok.gitlabclient.entity.mergerequest.MergeRequestChange
import ru.terrakok.gitlabclient.extension.inflate
import ru.terrakok.gitlabclient.ui.global.GitDiffViewController
/**
* Created by Eugene Shapovalov (@CraggyHaggy) on 26.10.18.
*/
class MergeRequestChangeAdapterDelegate : AdapterDelegate<MutableList<MergeRequestChange>>() {
override fun isForViewType(items: MutableList<MergeRequestChange>, position: Int) = true
override fun onCreateViewHolder(parent: ViewGroup): RecyclerView.ViewHolder =
ViewHolder(parent.inflate(R.layout.item_merge_request_change))
override fun onBindViewHolder(
items: MutableList<MergeRequestChange>,
position: Int,
viewHolder: RecyclerView.ViewHolder,
payloads: MutableList<Any>
) = (viewHolder as ViewHolder).bind(items[position])
override fun onViewRecycled(viewHolder: RecyclerView.ViewHolder) {
super.onViewRecycled(viewHolder)
(viewHolder as ViewHolder).recycle()
}
private inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
private lateinit var mergeRequestChange: MergeRequestChange
private val gitDiffViewController: GitDiffViewController = GitDiffViewController(itemView.changeFile)
fun bind(mergeRequestChange: MergeRequestChange) {
this.mergeRequestChange = mergeRequestChange
with(itemView) {
changePath.text = mergeRequestChange.newPath
changeIcon.setImageResource(
when {
mergeRequestChange.newFile -> R.drawable.ic_file_added
mergeRequestChange.deletedFile -> R.drawable.ic_file_deleted
else -> R.drawable.ic_file_changed
}
)
changeFileName.text = mergeRequestChange.newPath.let {
val index = it.lastIndexOf("/")
it.substring(if (index != -1) index + 1 else 0)
}
gitDiffViewController.setText(mergeRequestChange.diff)
changeAddedCount.text = context.getString(
R.string.merge_request_changes_added_count,
gitDiffViewController.getAddedLineCount()
)
changeDeletedCount.text = context.getString(
R.string.merge_request_changes_deleted_count,
gitDiffViewController.getDeletedLineCount()
)
}
}
fun recycle() {
gitDiffViewController.release()
}
}
}
\ No newline at end of file
......@@ -44,9 +44,9 @@ class MainFlowFragment : BaseFragment() {
selectTab(
when (currentTabFragment?.tag) {
eventsTab.screenKey -> eventsTab
issuesTab.screenKey -> eventsTab
mrsTab.screenKey -> eventsTab
todosTab.screenKey -> eventsTab
issuesTab.screenKey -> issuesTab
mrsTab.screenKey -> mrsTab
todosTab.screenKey -> todosTab
else -> eventsTab
}
)
......
package ru.terrakok.gitlabclient.ui.mergerequest
import android.support.v7.util.DiffUtil
import com.hannesdorfmann.adapterdelegates3.ListDelegationAdapter
import ru.terrakok.gitlabclient.entity.mergerequest.MergeRequestChange
import ru.terrakok.gitlabclient.ui.global.list.MergeRequestChangeAdapterDelegate
/**
* Created by Eugene Shapovalov (@CraggyHaggy) on 26.10.18.
*/
class MergeRequestChangeAdapter : ListDelegationAdapter<MutableList<MergeRequestChange>>() {
init {
items = mutableListOf()
delegatesManager.addDelegate(MergeRequestChangeAdapterDelegate())
}
fun setData(data: List<MergeRequestChange>) {
val oldItems = items.toList()
items.clear()
items.addAll(data)
DiffUtil
.calculateDiff(DiffCallback(items, oldItems), false)
.dispatchUpdatesTo(this)
}
private inner class DiffCallback(
private val newItems: List<MergeRequestChange>,
private val oldItems: List<MergeRequestChange>
) : 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 newItem.diff == oldItem.diff
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldItems[oldItemPosition]
val newItem = newItems[newItemPosition]
return newItem == oldItem
}
}
}
\ No newline at end of file
package ru.terrakok.gitlabclient.ui.mergerequest
import android.os.Bundle
import android.support.v7.widget.LinearLayoutManager
import com.arellomobile.mvp.presenter.InjectPresenter
import com.arellomobile.mvp.presenter.ProvidePresenter
import kotlinx.android.synthetic.main.layout_base_list.*
import kotlinx.android.synthetic.main.layout_zero.*
import ru.terrakok.gitlabclient.R
import ru.terrakok.gitlabclient.entity.mergerequest.MergeRequestChange
import ru.terrakok.gitlabclient.extension.showSnackMessage
import ru.terrakok.gitlabclient.extension.visible
import ru.terrakok.gitlabclient.presentation.mergerequest.changes.MergeRequestChangesPresenter
import ru.terrakok.gitlabclient.presentation.mergerequest.changes.MergeRequestChangesView
import ru.terrakok.gitlabclient.toothpick.DI
import ru.terrakok.gitlabclient.ui.global.BaseFragment
import ru.terrakok.gitlabclient.ui.global.ZeroViewHolder
import ru.terrakok.gitlabclient.ui.global.list.SimpleDividerDecorator
import toothpick.Toothpick
/**
* Created by Eugene Shapovalov (@CraggyHaggy) on 25.10.18.
*/
class MergeRequestChangesFragment : BaseFragment(), MergeRequestChangesView {
override val layoutRes = R.layout.fragment_mr_changes
private val adapter by lazy { MergeRequestChangeAdapter() }
private var zeroViewHolder: ZeroViewHolder? = null
@InjectPresenter
lateinit var presenter: MergeRequestChangesPresenter
@ProvidePresenter
fun providePresenter() =
Toothpick.openScope(DI.MERGE_REQUEST_FLOW_SCOPE)
.getInstance(MergeRequestChangesPresenter::class.java)
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
with(recyclerView) {
setHasFixedSize(true)
layoutManager = LinearLayoutManager(context)
addItemDecoration(SimpleDividerDecorator(context))
adapter = this@MergeRequestChangesFragment.adapter
}
swipeToRefresh.setOnRefreshListener { presenter.refreshChanges() }
zeroViewHolder = ZeroViewHolder(zeroLayout, { presenter.refreshChanges() })
}
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 showEmptyView(show: Boolean) {
if (show) zeroViewHolder?.showEmptyData()
else zeroViewHolder?.hide()
}
override fun showEmptyError(show: Boolean, message: String?) {
if (show) zeroViewHolder?.showEmptyError(message)
else zeroViewHolder?.hide()
}
override fun showChanges(show: Boolean, changes: List<MergeRequestChange>) {