mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 20:33:53 +01:00
Add pull down to refresh in Contributions screen (#6041)
* pull down to refresh Signed-off-by: parneet-guraya <gurayaparneet@gmail.com> * add kdoc Signed-off-by: parneet-guraya <gurayaparneet@gmail.com> * only enabled for self user Signed-off-by: parneet-guraya <gurayaparneet@gmail.com> * fix test Signed-off-by: parneet-guraya <gurayaparneet@gmail.com> --------- Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>
This commit is contained in:
parent
a4b74794cb
commit
4c9637c821
6 changed files with 160 additions and 108 deletions
|
|
@ -2,7 +2,6 @@ package fr.free.nrw.commons.contributions
|
||||||
|
|
||||||
import androidx.paging.PagedList.BoundaryCallback
|
import androidx.paging.PagedList.BoundaryCallback
|
||||||
import fr.free.nrw.commons.auth.SessionManager
|
import fr.free.nrw.commons.auth.SessionManager
|
||||||
import fr.free.nrw.commons.di.CommonsApplicationModule
|
|
||||||
import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.IO_THREAD
|
import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.IO_THREAD
|
||||||
import fr.free.nrw.commons.media.MediaClient
|
import fr.free.nrw.commons.media.MediaClient
|
||||||
import io.reactivex.Scheduler
|
import io.reactivex.Scheduler
|
||||||
|
|
@ -31,10 +30,7 @@ class ContributionBoundaryCallback
|
||||||
* network
|
* network
|
||||||
*/
|
*/
|
||||||
override fun onZeroItemsLoaded() {
|
override fun onZeroItemsLoaded() {
|
||||||
if (sessionManager.userName != null) {
|
refreshList()
|
||||||
mediaClient.resetUserNameContinuation(sessionManager.userName!!)
|
|
||||||
}
|
|
||||||
fetchContributions()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -52,9 +48,25 @@ class ContributionBoundaryCallback
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches contributions using the MediaWiki API
|
* Fetch list from network and save it to local DB.
|
||||||
|
*
|
||||||
|
* @param onRefreshFinish callback to invoke when operations finishes
|
||||||
|
* with either error or success.
|
||||||
*/
|
*/
|
||||||
private fun fetchContributions() {
|
fun refreshList(onRefreshFinish: () -> Unit = {}){
|
||||||
|
if (sessionManager.userName != null) {
|
||||||
|
mediaClient.resetUserNameContinuation(sessionManager.userName!!)
|
||||||
|
}
|
||||||
|
fetchContributions(onRefreshFinish)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches contributions using the MediaWiki API
|
||||||
|
*
|
||||||
|
* @param onRefreshFinish callback to invoke when operations finishes
|
||||||
|
* with either error or success.
|
||||||
|
*/
|
||||||
|
private fun fetchContributions(onRefreshFinish: () -> Unit = {}) {
|
||||||
if (sessionManager.userName != null) {
|
if (sessionManager.userName != null) {
|
||||||
userName
|
userName
|
||||||
?.let { userName ->
|
?.let { userName ->
|
||||||
|
|
@ -65,12 +77,15 @@ class ContributionBoundaryCallback
|
||||||
Contribution(media = media, state = Contribution.STATE_COMPLETED)
|
Contribution(media = media, state = Contribution.STATE_COMPLETED)
|
||||||
}
|
}
|
||||||
}.subscribeOn(ioThreadScheduler)
|
}.subscribeOn(ioThreadScheduler)
|
||||||
.subscribe(::saveContributionsToDB) { error: Throwable ->
|
.subscribe({ list ->
|
||||||
|
saveContributionsToDB(list, onRefreshFinish)
|
||||||
|
},{ error ->
|
||||||
|
onRefreshFinish()
|
||||||
Timber.e(
|
Timber.e(
|
||||||
"Failed to fetch contributions: %s",
|
"Failed to fetch contributions: %s",
|
||||||
error.message,
|
error.message,
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
}?.let {
|
}?.let {
|
||||||
compositeDisposable.add(
|
compositeDisposable.add(
|
||||||
it,
|
it,
|
||||||
|
|
@ -83,13 +98,16 @@ class ContributionBoundaryCallback
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves the contributions the the local DB
|
* Saves the contributions the the local DB
|
||||||
|
*
|
||||||
|
* @param onRefreshFinish callback to invoke when successfully saved to DB.
|
||||||
*/
|
*/
|
||||||
private fun saveContributionsToDB(contributions: List<Contribution>) {
|
private fun saveContributionsToDB(contributions: List<Contribution>, onRefreshFinish: () -> Unit) {
|
||||||
compositeDisposable.add(
|
compositeDisposable.add(
|
||||||
repository
|
repository
|
||||||
.save(contributions)
|
.save(contributions)
|
||||||
.subscribeOn(ioThreadScheduler)
|
.subscribeOn(ioThreadScheduler)
|
||||||
.subscribe { longs: List<Long?>? ->
|
.subscribe { longs: List<Long?>? ->
|
||||||
|
onRefreshFinish()
|
||||||
repository["last_fetch_timestamp"] = System.currentTimeMillis()
|
repository["last_fetch_timestamp"] = System.currentTimeMillis()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package fr.free.nrw.commons.contributions;
|
package fr.free.nrw.commons.contributions;
|
||||||
|
|
||||||
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||||
import fr.free.nrw.commons.BasePresenter;
|
import fr.free.nrw.commons.BasePresenter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -17,5 +18,8 @@ public class ContributionsListContract {
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface UserActionListener extends BasePresenter<View> {
|
public interface UserActionListener extends BasePresenter<View> {
|
||||||
|
|
||||||
|
void refreshList(SwipeRefreshLayout swipeRefreshLayout);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -191,6 +191,15 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
|
||||||
|
|
||||||
initAdapter();
|
initAdapter();
|
||||||
|
|
||||||
|
// pull down to refresh only enabled for self user.
|
||||||
|
if(Objects.equals(sessionManager.getUserName(), userName)){
|
||||||
|
binding.swipeRefreshLayout.setOnRefreshListener(() -> {
|
||||||
|
contributionsListPresenter.refreshList(binding.swipeRefreshLayout);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
binding.swipeRefreshLayout.setEnabled(false);
|
||||||
|
}
|
||||||
|
|
||||||
return binding.getRoot();
|
return binding.getRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,15 @@ import androidx.paging.DataSource;
|
||||||
import androidx.paging.DataSource.Factory;
|
import androidx.paging.DataSource.Factory;
|
||||||
import androidx.paging.LivePagedListBuilder;
|
import androidx.paging.LivePagedListBuilder;
|
||||||
import androidx.paging.PagedList;
|
import androidx.paging.PagedList;
|
||||||
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||||
import fr.free.nrw.commons.contributions.ContributionsListContract.UserActionListener;
|
import fr.free.nrw.commons.contributions.ContributionsListContract.UserActionListener;
|
||||||
import fr.free.nrw.commons.di.CommonsApplicationModule;
|
|
||||||
import io.reactivex.Scheduler;
|
import io.reactivex.Scheduler;
|
||||||
import io.reactivex.disposables.CompositeDisposable;
|
import io.reactivex.disposables.CompositeDisposable;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
|
import kotlin.Unit;
|
||||||
|
import kotlin.jvm.functions.Function0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The presenter class for Contributions
|
* The presenter class for Contributions
|
||||||
|
|
@ -95,4 +96,17 @@ public class ContributionsListPresenter implements UserActionListener {
|
||||||
contributionBoundaryCallback.dispose();
|
contributionBoundaryCallback.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* It is used to refresh list.
|
||||||
|
*
|
||||||
|
* @param swipeRefreshLayout used to stop refresh animation when
|
||||||
|
* refresh finishes.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void refreshList(final SwipeRefreshLayout swipeRefreshLayout) {
|
||||||
|
contributionBoundaryCallback.refreshList(() -> {
|
||||||
|
swipeRefreshLayout.setRefreshing(false);
|
||||||
|
return Unit.INSTANCE;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,125 +1,129 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:orientation="vertical"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/swipe_refresh_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="?attr/contributionsListBackground"
|
android:background="?attr/contributionsListBackground"
|
||||||
>
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/noContributionsYet"
|
android:id="@+id/noContributionsYet"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/no_uploads"
|
android:layout_centerInParent="true"
|
||||||
android:gravity="center"
|
android:layout_marginEnd="@dimen/tiny_gap"
|
||||||
android:layout_centerInParent="true"
|
android:layout_marginRight="@dimen/tiny_gap"
|
||||||
android:visibility="gone"
|
android:gravity="center"
|
||||||
android:layout_marginRight="@dimen/tiny_gap"
|
android:text="@string/no_uploads"
|
||||||
android:layout_marginEnd="@dimen/tiny_gap"
|
android:visibility="gone" />
|
||||||
/>
|
|
||||||
|
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
android:id="@+id/loadingContributionsProgressBar"
|
android:id="@+id/loadingContributionsProgressBar"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_centerInParent="true"
|
android:layout_centerInParent="true"
|
||||||
android:visibility="gone"
|
android:visibility="gone" />
|
||||||
/>
|
|
||||||
|
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatTextView
|
<androidx.appcompat.widget.AppCompatTextView
|
||||||
android:id="@+id/tv_contributions_of_user"
|
android:id="@+id/tv_contributions_of_user"
|
||||||
style="@style/MediaDetailTextLabel"
|
style="@style/MediaDetailTextLabel"
|
||||||
tools:text="Contributions of user : Ashish"
|
android:layout_width="match_parent"
|
||||||
android:layout_width="match_parent"
|
android:layout_height="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:padding="10dp"
|
||||||
android:padding="10dp"
|
android:visibility="gone"
|
||||||
tools:visibility="visible"
|
tools:text="Contributions of user : Ashish"
|
||||||
android:visibility="gone" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/contributionsList"
|
android:id="@+id/contributionsList"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_below="@id/tv_contributions_of_user"
|
android:layout_below="@id/tv_contributions_of_user"
|
||||||
android:scrollbars="vertical"
|
android:fadeScrollbars="true"
|
||||||
android:fadeScrollbars="true"
|
android:scrollbarSize="@dimen/dimen_6"
|
||||||
android:scrollbarThumbVertical="@color/primaryColor"
|
android:scrollbarThumbVertical="@color/primaryColor"
|
||||||
android:scrollbarSize="@dimen/dimen_6"/>
|
android:scrollbars="vertical" />
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/fab_layout"
|
android:id="@+id/fab_layout"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:layout_alignParentRight="true"
|
||||||
|
android:layout_alignParentBottom="true"
|
||||||
|
android:layout_marginRight="@dimen/medium_height"
|
||||||
|
android:layout_marginBottom="@dimen/activity_margin_horizontal"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
|
android:id="@+id/fab_camera"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_alignParentBottom="true"
|
android:contentDescription="@string/add_contribution_from_camera"
|
||||||
android:layout_alignParentEnd="true"
|
android:tint="@color/button_blue"
|
||||||
android:layout_alignParentRight="true"
|
android:visibility="gone"
|
||||||
android:layout_marginRight="@dimen/medium_height"
|
app:backgroundTint="@color/main_background_light"
|
||||||
android:layout_marginBottom="@dimen/activity_margin_horizontal"
|
app:elevation="@dimen/tiny_margin"
|
||||||
android:orientation="vertical"
|
app:fabSize="mini"
|
||||||
android:gravity="center"
|
app:srcCompat="@drawable/ic_photo_camera_white_24dp"
|
||||||
>
|
app:useCompatPadding="true" />
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
android:id="@+id/fab_camera"
|
android:id="@+id/fab_gallery"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:contentDescription="@string/add_contribution_from_camera"
|
android:contentDescription="@string/add_contribution_from_photos"
|
||||||
android:tint="@color/button_blue"
|
android:tint="@color/button_blue"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:backgroundTint="@color/main_background_light"
|
app:backgroundTint="@color/main_background_light"
|
||||||
app:elevation="@dimen/tiny_margin"
|
app:elevation="@dimen/tiny_margin"
|
||||||
app:fabSize="mini"
|
app:fabSize="mini"
|
||||||
app:srcCompat="@drawable/ic_photo_camera_white_24dp"
|
app:srcCompat="@drawable/ic_photo_white_24dp"
|
||||||
app:useCompatPadding="true" />
|
app:useCompatPadding="true" />
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
android:id="@+id/fab_gallery"
|
android:id="@+id/fab_custom_gallery"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:contentDescription="@string/add_contribution_from_photos"
|
android:background="@drawable/commons"
|
||||||
android:tint="@color/button_blue"
|
android:contentDescription="@string/add_contribution_from_contributions_gallery"
|
||||||
android:visibility="gone"
|
android:tint="@color/button_blue"
|
||||||
app:backgroundTint="@color/main_background_light"
|
android:visibility="gone"
|
||||||
app:elevation="@dimen/tiny_margin"
|
app:backgroundTint="@color/main_background_light"
|
||||||
app:fabSize="mini"
|
app:elevation="@dimen/tiny_margin"
|
||||||
app:srcCompat="@drawable/ic_photo_white_24dp"
|
app:fabSize="mini"
|
||||||
app:useCompatPadding="true" />
|
app:srcCompat="@drawable/ic_custom_image_picker"
|
||||||
|
app:useCompatPadding="true" />
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
android:id="@+id/fab_custom_gallery"
|
android:id="@+id/fab_plus"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="@drawable/commons"
|
android:contentDescription="@string/add_new_contribution"
|
||||||
android:contentDescription="@string/add_contribution_from_contributions_gallery"
|
android:gravity="center_vertical"
|
||||||
android:tint="@color/button_blue"
|
android:visibility="visible"
|
||||||
android:visibility="gone"
|
app:backgroundTint="@color/status_bar_blue"
|
||||||
app:backgroundTint="@color/main_background_light"
|
app:elevation="@dimen/tiny_margin"
|
||||||
app:elevation="@dimen/tiny_margin"
|
app:srcCompat="@drawable/ic_add_white_24dp"
|
||||||
app:fabSize="mini"
|
app:useCompatPadding="true" />
|
||||||
app:srcCompat="@drawable/ic_custom_image_picker"
|
|
||||||
app:useCompatPadding="true" />
|
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
android:id="@+id/fab_plus"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:contentDescription="@string/add_new_contribution"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:visibility="visible"
|
|
||||||
app:backgroundTint="@color/status_bar_blue"
|
|
||||||
app:elevation="@dimen/tiny_margin"
|
|
||||||
app:srcCompat="@drawable/ic_add_white_24dp"
|
|
||||||
app:useCompatPadding="true" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
||||||
|
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import org.mockito.Mockito.mock
|
||||||
import org.mockito.Mockito.verifyNoInteractions
|
import org.mockito.Mockito.verifyNoInteractions
|
||||||
import org.mockito.MockitoAnnotations
|
import org.mockito.MockitoAnnotations
|
||||||
import java.lang.reflect.Method
|
import java.lang.reflect.Method
|
||||||
|
import kotlin.reflect.jvm.internal.impl.builtins.functions.FunctionTypeKind
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The unit test class for ContributionBoundaryCallbackTest
|
* The unit test class for ContributionBoundaryCallbackTest
|
||||||
|
|
@ -99,9 +100,10 @@ class ContributionBoundaryCallbackTest {
|
||||||
val method: Method =
|
val method: Method =
|
||||||
ContributionBoundaryCallback::class.java.getDeclaredMethod(
|
ContributionBoundaryCallback::class.java.getDeclaredMethod(
|
||||||
"fetchContributions",
|
"fetchContributions",
|
||||||
|
Function0::class.java
|
||||||
)
|
)
|
||||||
method.isAccessible = true
|
method.isAccessible = true
|
||||||
method.invoke(contributionBoundaryCallback)
|
method.invoke(contributionBoundaryCallback, {})
|
||||||
verify(repository).save(anyList())
|
verify(repository).save(anyList())
|
||||||
verify(mediaClient).getMediaListForUser(anyString())
|
verify(mediaClient).getMediaListForUser(anyString())
|
||||||
}
|
}
|
||||||
|
|
@ -113,9 +115,10 @@ class ContributionBoundaryCallbackTest {
|
||||||
val method: Method =
|
val method: Method =
|
||||||
ContributionBoundaryCallback::class.java.getDeclaredMethod(
|
ContributionBoundaryCallback::class.java.getDeclaredMethod(
|
||||||
"fetchContributions",
|
"fetchContributions",
|
||||||
|
Function0::class.java
|
||||||
)
|
)
|
||||||
method.isAccessible = true
|
method.isAccessible = true
|
||||||
method.invoke(contributionBoundaryCallback)
|
method.invoke(contributionBoundaryCallback, {})
|
||||||
verifyNoInteractions(repository)
|
verifyNoInteractions(repository)
|
||||||
verify(mediaClient).getMediaListForUser(anyString())
|
verify(mediaClient).getMediaListForUser(anyString())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue