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:
Parneet Singh 2024-12-20 06:36:07 +05:30 committed by GitHub
parent a4b74794cb
commit 4c9637c821
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 160 additions and 108 deletions

View file

@ -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()
}, },
) )

View file

@ -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);
} }
} }

View file

@ -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();
} }

View file

@ -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;
});
}
} }

View file

@ -1,32 +1,36 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical" 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:gravity="center"
android:layout_centerInParent="true" android:layout_centerInParent="true"
android:visibility="gone"
android:layout_marginRight="@dimen/tiny_gap"
android:layout_marginEnd="@dimen/tiny_gap" android:layout_marginEnd="@dimen/tiny_gap"
/> android:layout_marginRight="@dimen/tiny_gap"
android:gravity="center"
android:text="@string/no_uploads"
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"
@ -36,36 +40,35 @@
<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"
tools:visibility="visible" android:visibility="gone"
android:visibility="gone" /> tools:text="Contributions of user : Ashish"
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_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true" android:layout_alignParentEnd="true"
android:layout_alignParentRight="true" android:layout_alignParentRight="true"
android:layout_alignParentBottom="true"
android:layout_marginRight="@dimen/medium_height" android:layout_marginRight="@dimen/medium_height"
android:layout_marginBottom="@dimen/activity_margin_horizontal" android:layout_marginBottom="@dimen/activity_margin_horizontal"
android:orientation="vertical"
android:gravity="center" android:gravity="center"
> android:orientation="vertical">
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_camera" android:id="@+id/fab_camera"
@ -123,3 +126,4 @@
</RelativeLayout> </RelativeLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

View file

@ -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())
} }