get rid of BaseFragment by using RxJava instead of Retrofit Calls (#2055)

* get rid of BaseFragment by using RxJava instead of Retrofit Calls

* fix tests
This commit is contained in:
Konrad Pozniak 2021-01-31 19:34:33 +01:00 committed by GitHub
parent 2d2b79aa47
commit 886ff2f06b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 170 additions and 309 deletions

View File

@ -57,7 +57,7 @@ class DraftsViewModel @Inject constructor(
}
fun getToot(tootId: String): Single<Status> {
return api.statusSingle(tootId)
return api.status(tootId)
}
override fun onCleared() {

View File

@ -5,6 +5,7 @@ import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
@ -14,7 +15,6 @@ import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.instancemute.adapter.DomainMutesAdapter
import com.keylesspalace.tusky.components.instancemute.interfaces.InstanceActionListener
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.fragment.BaseFragment
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.HttpHeaderLink
import com.keylesspalace.tusky.util.hide
@ -30,7 +30,7 @@ import retrofit2.Response
import java.io.IOException
import javax.inject.Inject
class InstanceListFragment: BaseFragment(), Injectable, InstanceActionListener {
class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectable, InstanceActionListener {
@Inject
lateinit var api: MastodonApi
@ -39,10 +39,6 @@ class InstanceListFragment: BaseFragment(), Injectable, InstanceActionListener {
private var adapter = DomainMutesAdapter(this)
private lateinit var scrollListener: EndlessOnScrollListener
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return inflater.inflate(R.layout.fragment_instance_list, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

View File

@ -17,9 +17,8 @@ package com.keylesspalace.tusky.fragment
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
@ -45,14 +44,12 @@ import com.uber.autodispose.autoDispose
import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.synthetic.main.fragment_account_list.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.io.IOException
import java.util.*
import javax.inject.Inject
class AccountListFragment : BaseFragment(), AccountActionListener, Injectable {
class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountActionListener, Injectable {
@Inject
lateinit var api: MastodonApi
@ -71,10 +68,6 @@ class AccountListFragment : BaseFragment(), AccountActionListener, Injectable {
id = arguments?.getString(ARG_ID)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return inflater.inflate(R.layout.fragment_account_list, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@ -202,27 +195,23 @@ class AccountListFragment : BaseFragment(), AccountActionListener, Injectable {
override fun onRespondToFollowRequest(accept: Boolean, accountId: String,
position: Int) {
val callback = object : Callback<Relationship> {
override fun onResponse(call: Call<Relationship>, response: Response<Relationship>) {
if (response.isSuccessful) {
onRespondToFollowRequestSuccess(position)
} else {
onRespondToFollowRequestFailure(accept, accountId)
}
}
override fun onFailure(call: Call<Relationship>, t: Throwable) {
onRespondToFollowRequestFailure(accept, accountId)
}
}
val call = if (accept) {
if (accept) {
api.authorizeFollowRequest(accountId)
} else {
api.rejectFollowRequest(accountId)
}
callList.add(call)
call.enqueue(callback)
}.observeOn(AndroidSchedulers.mainThread())
.autoDispose(from(this, Lifecycle.Event.ON_DESTROY))
.subscribe({
onRespondToFollowRequestSuccess(position)
}, { throwable ->
val verb = if (accept) {
"accept"
} else {
"reject"
}
Log.e(TAG, "Failed to $verb account id $accountId.", throwable)
})
}
private fun onRespondToFollowRequestSuccess(position: Int) {
@ -230,15 +219,6 @@ class AccountListFragment : BaseFragment(), AccountActionListener, Injectable {
followRequestsAdapter.removeItem(position)
}
private fun onRespondToFollowRequestFailure(accept: Boolean, accountId: String) {
val verb = if (accept) {
"accept"
} else {
"reject"
}
Log.e(TAG, "Failed to $verb account id $accountId.")
}
private fun getFetchCallByListType(fromId: String?): Single<Response<List<Account>>> {
return when (type) {
Type.FOLLOWS -> {

View File

@ -18,12 +18,13 @@ package com.keylesspalace.tusky.fragment
import android.graphics.Color
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.core.app.ActivityOptionsCompat
import androidx.core.view.ViewCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
@ -40,9 +41,11 @@ import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.view.SquareImageView
import com.keylesspalace.tusky.viewdata.AttachmentViewData
import com.uber.autodispose.android.lifecycle.autoDispose
import io.reactivex.SingleObserver
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import kotlinx.android.synthetic.main.fragment_timeline.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.io.IOException
import java.util.*
@ -54,7 +57,7 @@ import javax.inject.Inject
* Fragment with multiple columns of media previews for the specified account.
*/
class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable {
class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFragment, Injectable {
companion object {
@JvmStatic
fun newInstance(accountId: String, enableSwipeToRefresh:Boolean=true): AccountMediaFragment {
@ -78,14 +81,13 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable {
lateinit var api: MastodonApi
private val adapter = MediaGridAdapter()
private var currentCall: Call<List<Status>>? = null
private val statuses = mutableListOf<Status>()
private var fetchingStatus = FetchingStatus.NOT_FETCHING
private lateinit var accountId: String
private val callback = object : Callback<List<Status>> {
override fun onFailure(call: Call<List<Status>>?, t: Throwable?) {
private val callback = object : SingleObserver<Response<List<Status>>> {
override fun onError(t: Throwable) {
fetchingStatus = FetchingStatus.NOT_FETCHING
if (isAdded) {
@ -107,7 +109,7 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable {
Log.d(TAG, "Failed to fetch account media", t)
}
override fun onResponse(call: Call<List<Status>>, response: Response<List<Status>>) {
override fun onSuccess(response: Response<List<Status>>) {
fetchingStatus = FetchingStatus.NOT_FETCHING
if (isAdded) {
swipeRefreshLayout.isRefreshing = false
@ -128,22 +130,23 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable {
if (statuses.isEmpty()) {
statusView.show()
statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty,
null)
statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty)
}
}
}
}
override fun onSubscribe(d: Disposable) {}
}
private val bottomCallback = object : Callback<List<Status>> {
override fun onFailure(call: Call<List<Status>>?, t: Throwable?) {
private val bottomCallback = object : SingleObserver<Response<List<Status>>> {
override fun onError(t: Throwable) {
fetchingStatus = FetchingStatus.NOT_FETCHING
Log.d(TAG, "Failed to fetch account media", t)
}
override fun onResponse(call: Call<List<Status>>, response: Response<List<Status>>) {
override fun onSuccess(response: Response<List<Status>>) {
fetchingStatus = FetchingStatus.NOT_FETCHING
val body = response.body()
body?.let { fetched ->
@ -160,6 +163,7 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable {
}
}
override fun onSubscribe(d: Disposable) { }
}
override fun onCreate(savedInstanceState: Bundle?) {
@ -167,10 +171,6 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable {
isSwipeToRefreshEnabled = arguments?.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH,true) == true
accountId = arguments?.getString(ACCOUNT_ID_ARG)!!
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_timeline, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@ -202,8 +202,10 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable {
statuses.lastOrNull()?.let { (id) ->
Log.d(TAG, "Requesting statuses with max_id: ${id}, (bottom)")
fetchingStatus = FetchingStatus.FETCHING_BOTTOM
currentCall = api.accountStatuses(accountId, id, null, null, null, true, null)
currentCall?.enqueue(bottomCallback)
api.accountStatuses(accountId, id, null, null, null, true, null)
.observeOn(AndroidSchedulers.mainThread())
.autoDispose(this@AccountMediaFragment, Lifecycle.Event.ON_DESTROY)
.subscribe(bottomCallback)
}
}
}
@ -216,14 +218,15 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable {
private fun refresh() {
statusView.hide()
if (fetchingStatus != FetchingStatus.NOT_FETCHING) return
currentCall = if (statuses.isEmpty()) {
if (statuses.isEmpty()) {
fetchingStatus = FetchingStatus.INITIAL_FETCHING
api.accountStatuses(accountId, null, null, null, null, true, null)
} else {
fetchingStatus = FetchingStatus.REFRESHING
api.accountStatuses(accountId, null, statuses[0].id, null, null, true, null)
}
currentCall?.enqueue(callback)
}.observeOn(AndroidSchedulers.mainThread())
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
.subscribe(callback)
if (!isSwipeToRefreshEnabled)
topProgressBar?.show()
@ -235,8 +238,10 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable {
}
if (fetchingStatus == FetchingStatus.NOT_FETCHING && statuses.isEmpty()) {
fetchingStatus = FetchingStatus.INITIAL_FETCHING
currentCall = api.accountStatuses(accountId, null, null, null, null, true, null)
currentCall?.enqueue(callback)
api.accountStatuses(accountId, null, null, null, null, true, null)
.observeOn(AndroidSchedulers.mainThread())
.autoDispose(this@AccountMediaFragment, Lifecycle.Event.ON_DESTROY)
.subscribe(callback)
}
else if (needToRefresh)
refresh()
@ -339,5 +344,4 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable {
needToRefresh = true
}
}

View File

@ -1,43 +0,0 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.fragment;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import java.util.ArrayList;
import java.util.List;
import retrofit2.Call;
public class BaseFragment extends Fragment {
protected List<Call> callList;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
callList = new ArrayList<>();
}
@Override
public void onDestroy() {
for (Call call : callList) {
call.cancel();
}
super.onDestroy();
}
}

View File

@ -102,13 +102,11 @@ import at.connyduck.sparkbutton.helpers.Utils;
import io.reactivex.Observable;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import kotlin.Unit;
import kotlin.collections.CollectionsKt;
import kotlin.jvm.functions.Function1;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import static com.keylesspalace.tusky.util.StringUtils.isLessThan;
import static com.uber.autodispose.AutoDispose.autoDisposable;
@ -125,8 +123,9 @@ public class NotificationsFragment extends SFragment implements
private static final int LOAD_AT_ONCE = 30;
private int maxPlaceholderId = 0;
private final Set<Notification.Type> notificationFilter = new HashSet<>();
private Set<Notification.Type> notificationFilter = new HashSet<>();
private final CompositeDisposable disposables = new CompositeDisposable();
private enum FetchEnd {
TOP,
@ -685,32 +684,21 @@ public class NotificationsFragment extends SFragment implements
updateAdapter();
//Execute clear notifications request
Call<ResponseBody> call = mastodonApi.clearNotifications();
call.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(@NonNull Call<ResponseBody> call, @NonNull Response<ResponseBody> response) {
if (isAdded()) {
if (!response.isSuccessful()) {
//Reload notifications on failure
fullyRefreshWithProgressBar(true);
}
}
}
@Override
public void onFailure(@NonNull Call<ResponseBody> call, @NonNull Throwable t) {
//Reload notifications on failure
fullyRefreshWithProgressBar(true);
}
});
callList.add(call);
mastodonApi.clearNotifications()
.observeOn(AndroidSchedulers.mainThread())
.as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
.subscribe(
response -> {
// nothing to do
},
throwable -> {
//Reload notifications on failure
fullyRefreshWithProgressBar(true);
});
}
private void resetNotificationsLoad() {
for (Call callItem : callList) {
callItem.cancel();
}
callList.clear();
disposables.clear();
bottomLoading = false;
topLoading = false;
@ -840,8 +828,8 @@ public class NotificationsFragment extends SFragment implements
@Override
public void onRespondToFollowRequest(boolean accept, String id, int position) {
Single<Relationship> request = accept ?
mastodonApi.authorizeFollowRequestObservable(id) :
mastodonApi.rejectFollowRequestObservable(id);
mastodonApi.authorizeFollowRequest(id) :
mastodonApi.rejectFollowRequest(id);
request.observeOn(AndroidSchedulers.mainThread())
.as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
.subscribe(
@ -959,27 +947,20 @@ public class NotificationsFragment extends SFragment implements
bottomLoading = true;
}
Call<List<Notification>> call = mastodonApi.notifications(fromId, uptoId, LOAD_AT_ONCE, showNotificationsFilter ? notificationFilter : null);
call.enqueue(new Callback<List<Notification>>() {
@Override
public void onResponse(@NonNull Call<List<Notification>> call,
@NonNull Response<List<Notification>> response) {
if (response.isSuccessful()) {
String linkHeader = response.headers().get("Link");
onFetchNotificationsSuccess(response.body(), linkHeader, fetchEnd, pos);
} else {
onFetchNotificationsFailure(new Exception(response.message()), fetchEnd, pos);
}
}
@Override
public void onFailure(@NonNull Call<List<Notification>> call, @NonNull Throwable t) {
if (!call.isCanceled())
onFetchNotificationsFailure((Exception) t, fetchEnd, pos);
}
});
callList.add(call);
Disposable notificationCall = mastodonApi.notifications(fromId, uptoId, LOAD_AT_ONCE, showNotificationsFilter ? notificationFilter : null)
.observeOn(AndroidSchedulers.mainThread())
.as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
.subscribe(
response -> {
if (response.isSuccessful()) {
String linkHeader = response.headers().get("Link");
onFetchNotificationsSuccess(response.body(), linkHeader, fetchEnd, pos);
} else {
onFetchNotificationsFailure(new Exception(response.message()), fetchEnd, pos);
}
},
throwable -> onFetchNotificationsFailure(throwable, fetchEnd, pos));
disposables.add(notificationCall);
}
private void onFetchNotificationsSuccess(List<Notification> notifications, String linkHeader,
@ -1038,7 +1019,7 @@ public class NotificationsFragment extends SFragment implements
progressBar.setVisibility(View.GONE);
}
private void onFetchNotificationsFailure(Exception exception, FetchEnd fetchEnd, int position) {
private void onFetchNotificationsFailure(Throwable throwable, FetchEnd fetchEnd, int position) {
swipeRefreshLayout.setRefreshing(false);
if (fetchEnd == FetchEnd.MIDDLE && !notifications.get(position).isRight()) {
Placeholder placeholder = notifications.get(position).asLeft();
@ -1050,7 +1031,7 @@ public class NotificationsFragment extends SFragment implements
this.statusView.setVisibility(View.VISIBLE);
swipeRefreshLayout.setEnabled(false);
this.showingError = true;
if (exception instanceof IOException) {
if (throwable instanceof IOException) {
this.statusView.setup(R.drawable.elephant_offline, R.string.error_network, __ -> {
this.progressBar.setVisibility(View.VISIBLE);
this.onRefresh();
@ -1065,7 +1046,7 @@ public class NotificationsFragment extends SFragment implements
}
updateFilterVisibility();
}
Log.e(TAG, "Fetch failure: " + exception.getMessage());
Log.e(TAG, "Fetch failure: " + throwable.getMessage());
if (fetchEnd == FetchEnd.TOP) {
topLoading = false;

View File

@ -20,7 +20,6 @@ import android.app.DownloadManager;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
@ -30,8 +29,6 @@ import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.CheckBox;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
@ -41,14 +38,14 @@ import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.PopupMenu;
import androidx.core.app.ActivityOptionsCompat;
import androidx.core.view.ViewCompat;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.Lifecycle;
import androidx.preference.PreferenceManager;
import com.keylesspalace.tusky.BaseActivity;
import com.keylesspalace.tusky.BottomSheetActivity;
import com.keylesspalace.tusky.MainActivity;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.PostLookupFallbackBehavior;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.ViewMediaActivity;
import com.keylesspalace.tusky.ViewTagActivity;
import com.keylesspalace.tusky.components.compose.ComposeActivity;
@ -76,9 +73,8 @@ import java.util.regex.Pattern;
import javax.inject.Inject;
import kotlin.Unit;
import io.reactivex.android.schedulers.AndroidSchedulers;
import kotlin.Unit;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
@ -92,7 +88,7 @@ import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvid
* adapters. I feel like the profile pages and thread viewer, which I haven't made yet, will also
* overlap functionality. So, I'm momentarily leaving it and hopefully working on those will clear
* up what needs to be where. */
public abstract class SFragment extends BaseFragment implements Injectable {
public abstract class SFragment extends Fragment implements Injectable {
protected abstract void removeItem(int position);
@ -103,7 +99,7 @@ public abstract class SFragment extends BaseFragment implements Injectable {
private static List<Filter> filters;
private boolean filterRemoveRegex;
private Matcher filterRemoveRegexMatcher;
private static Matcher alphanumeric = Pattern.compile("^\\w+$").matcher("");
private static final Matcher alphanumeric = Pattern.compile("^\\w+$").matcher("");
@Inject
public MastodonApi mastodonApi;

View File

@ -101,12 +101,11 @@ import javax.inject.Inject;
import at.connyduck.sparkbutton.helpers.Utils;
import io.reactivex.Observable;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import kotlin.Unit;
import kotlin.collections.CollectionsKt;
import kotlin.jvm.functions.Function1;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import static com.uber.autodispose.AutoDispose.autoDisposable;
@ -1004,7 +1003,7 @@ public class TimelineFragment extends SFragment implements
}
}
private Call<List<Status>> getFetchCallByTimelineType(String fromId, String uptoId) {
private Single<Response<List<Status>>> getFetchCallByTimelineType(String fromId, String uptoId) {
MastodonApi api = mastodonApi;
switch (kind) {
default:
@ -1051,37 +1050,31 @@ public class TimelineFragment extends SFragment implements
.observeOn(AndroidSchedulers.mainThread())
.as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
.subscribe(
(result) -> onFetchTimelineSuccess(result, fetchEnd, pos),
(err) -> onFetchTimelineFailure(new Exception(err), fetchEnd, pos)
result -> onFetchTimelineSuccess(result, fetchEnd, pos),
err -> onFetchTimelineFailure(err, fetchEnd, pos)
);
} else {
Callback<List<Status>> callback = new Callback<List<Status>>() {
@Override
public void onResponse(@NonNull Call<List<Status>> call, @NonNull Response<List<Status>> response) {
if (response.isSuccessful()) {
@Nullable
String newNextId = extractNextId(response);
if (newNextId != null) {
// when we reach the bottom of the list, we won't have a new link. If
// we blindly write `null` here we will start loading from the top
// again.
nextId = newNextId;
}
onFetchTimelineSuccess(liftStatusList(response.body()), fetchEnd, pos);
} else {
onFetchTimelineFailure(new Exception(response.message()), fetchEnd, pos);
}
}
@Override
public void onFailure(@NonNull Call<List<Status>> call, @NonNull Throwable t) {
onFetchTimelineFailure((Exception) t, fetchEnd, pos);
}
};
Call<List<Status>> listCall = getFetchCallByTimelineType(maxId, sinceId);
callList.add(listCall);
listCall.enqueue(callback);
getFetchCallByTimelineType(maxId, sinceId)
.observeOn(AndroidSchedulers.mainThread())
.as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
.subscribe(
response -> {
if (response.isSuccessful()) {
@Nullable
String newNextId = extractNextId(response);
if (newNextId != null) {
// when we reach the bottom of the list, we won't have a new link. If
// we blindly write `null` here we will start loading from the top
// again.
nextId = newNextId;
}
onFetchTimelineSuccess(liftStatusList(response.body()), fetchEnd, pos);
} else {
onFetchTimelineFailure(new Exception(response.message()), fetchEnd, pos);
}
},
err -> onFetchTimelineFailure(err, fetchEnd, pos)
);
}
}
@ -1158,7 +1151,7 @@ public class TimelineFragment extends SFragment implements
}
}
private void onFetchTimelineFailure(Exception exception, FetchEnd fetchEnd, int position) {
private void onFetchTimelineFailure(Throwable throwable, FetchEnd fetchEnd, int position) {
if (isAdded()) {
swipeRefreshLayout.setRefreshing(false);
topProgressBar.hide();
@ -1177,7 +1170,7 @@ public class TimelineFragment extends SFragment implements
} else if (this.statuses.isEmpty()) {
swipeRefreshLayout.setEnabled(false);
this.statusView.setVisibility(View.VISIBLE);
if (exception instanceof IOException) {
if (throwable instanceof IOException) {
this.statusView.setup(R.drawable.elephant_offline, R.string.error_network, __ -> {
this.progressBar.setVisibility(View.VISIBLE);
this.onRefresh();
@ -1192,7 +1185,7 @@ public class TimelineFragment extends SFragment implements
}
}
Log.e(TAG, "Fetch Failure: " + exception.getMessage());
Log.e(TAG, "Fetch Failure: " + throwable.getMessage());
updateBottomLoadingState(fetchEnd);
progressBar.setVisibility(View.GONE);
}

View File

@ -17,10 +17,11 @@ package com.keylesspalace.tusky.fragment
import android.os.Bundle
import android.text.TextUtils
import androidx.fragment.app.Fragment
import com.keylesspalace.tusky.ViewMediaActivity
import com.keylesspalace.tusky.entity.Attachment
abstract class ViewMediaFragment : BaseFragment() {
abstract class ViewMediaFragment : Fragment() {
private var toolbarVisibiltyDisposable: Function0<Boolean>? = null
abstract fun setupMediaView(

View File

@ -55,7 +55,6 @@ import com.keylesspalace.tusky.di.Injectable;
import com.keylesspalace.tusky.entity.Filter;
import com.keylesspalace.tusky.entity.Poll;
import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.entity.StatusContext;
import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.network.MastodonApi;
import com.keylesspalace.tusky.settings.PrefKeys;
@ -75,9 +74,6 @@ import java.util.Locale;
import javax.inject.Inject;
import io.reactivex.android.schedulers.AndroidSchedulers;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import static com.uber.autodispose.AutoDispose.autoDisposable;
import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from;
@ -463,49 +459,32 @@ public final class ViewThreadFragment extends SFragment implements
}
private void sendStatusRequest(final String id) {
Call<Status> call = mastodonApi.status(id);
call.enqueue(new Callback<Status>() {
@Override
public void onResponse(@NonNull Call<Status> call, @NonNull Response<Status> response) {
if (response.isSuccessful()) {
int position = setStatus(response.body());
recyclerView.scrollToPosition(position);
} else {
onThreadRequestFailure(id);
}
}
@Override
public void onFailure(@NonNull Call<Status> call, @NonNull Throwable t) {
onThreadRequestFailure(id);
}
});
callList.add(call);
mastodonApi.status(id)
.observeOn(AndroidSchedulers.mainThread())
.as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
.subscribe(
status -> {
int position = setStatus(status);
recyclerView.scrollToPosition(position);
},
throwable -> onThreadRequestFailure(id, throwable)
);
}
private void sendThreadRequest(final String id) {
Call<StatusContext> call = mastodonApi.statusContext(id);
call.enqueue(new Callback<StatusContext>() {
@Override
public void onResponse(@NonNull Call<StatusContext> call, @NonNull Response<StatusContext> response) {
StatusContext context = response.body();
if (response.isSuccessful() && context != null) {
swipeRefreshLayout.setRefreshing(false);
setContext(context.getAncestors(), context.getDescendants());
} else {
onThreadRequestFailure(id);
}
}
@Override
public void onFailure(@NonNull Call<StatusContext> call, @NonNull Throwable t) {
onThreadRequestFailure(id);
}
});
callList.add(call);
mastodonApi.statusContext(id)
.observeOn(AndroidSchedulers.mainThread())
.as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
.subscribe(
context -> {
swipeRefreshLayout.setRefreshing(false);
setContext(context.getAncestors(), context.getDescendants());
},
throwable -> onThreadRequestFailure(id, throwable)
);
}
private void onThreadRequestFailure(final String id) {
private void onThreadRequestFailure(final String id, final Throwable throwable) {
View view = getView();
swipeRefreshLayout.setRefreshing(false);
if (view != null) {
@ -516,7 +495,7 @@ public final class ViewThreadFragment extends SFragment implements
})
.show();
} else {
Log.e(TAG, "Couldn't display thread fetch error message");
Log.e(TAG, "Network request failed", throwable);
}
}

View File

@ -56,14 +56,7 @@ interface MastodonApi {
@Query("max_id") maxId: String?,
@Query("since_id") sinceId: String?,
@Query("limit") limit: Int?
): Call<List<Status>>
@GET("api/v1/timelines/home")
fun homeTimelineSingle(
@Query("max_id") maxId: String?,
@Query("since_id") sinceId: String?,
@Query("limit") limit: Int?
): Single<List<Status>>
): Single<Response<List<Status>>>
@GET("api/v1/timelines/public")
fun publicTimeline(
@ -71,7 +64,7 @@ interface MastodonApi {
@Query("max_id") maxId: String?,
@Query("since_id") sinceId: String?,
@Query("limit") limit: Int?
): Call<List<Status>>
): Single<Response<List<Status>>>
@GET("api/v1/timelines/tag/{hashtag}")
fun hashtagTimeline(
@ -81,7 +74,7 @@ interface MastodonApi {
@Query("max_id") maxId: String?,
@Query("since_id") sinceId: String?,
@Query("limit") limit: Int?
): Call<List<Status>>
): Single<Response<List<Status>>>
@GET("api/v1/timelines/list/{listId}")
fun listTimeline(
@ -89,7 +82,7 @@ interface MastodonApi {
@Query("max_id") maxId: String?,
@Query("since_id") sinceId: String?,
@Query("limit") limit: Int?
): Call<List<Status>>
): Single<Response<List<Status>>>
@GET("api/v1/notifications")
fun notifications(
@ -97,7 +90,7 @@ interface MastodonApi {
@Query("since_id") sinceId: String?,
@Query("limit") limit: Int?,
@Query("exclude_types[]") excludes: Set<Notification.Type>?
): Call<List<Notification>>
): Single<Response<List<Notification>>>
@GET("api/v1/markers")
fun markersWithAuth(
@ -114,12 +107,7 @@ interface MastodonApi {
): Single<List<Notification>>
@POST("api/v1/notifications/clear")
fun clearNotifications(): Call<ResponseBody>
@GET("api/v1/notifications/{id}")
fun notification(
@Path("id") notificationId: String
): Call<Notification>
fun clearNotifications(): Single<ResponseBody>
@Multipart
@POST("api/v1/media")
@ -146,17 +134,12 @@ interface MastodonApi {
@GET("api/v1/statuses/{id}")
fun status(
@Path("id") statusId: String
): Call<Status>
@GET("api/v1/statuses/{id}")
fun statusSingle(
@Path("id") statusId: String
): Single<Status>
@GET("api/v1/statuses/{id}/context")
fun statusContext(
@Path("id") statusId: String
): Call<StatusContext>
): Single<StatusContext>
@GET("api/v1/statuses/{id}/reblogged_by")
fun statusRebloggedBy(
@ -295,7 +278,7 @@ interface MastodonApi {
@Query("exclude_replies") excludeReplies: Boolean?,
@Query("only_media") onlyMedia: Boolean?,
@Query("pinned") pinned: Boolean?
): Call<List<Status>>
): Single<Response<List<Status>>>
@GET("api/v1/accounts/{id}/followers")
fun accountFollowers(
@ -398,14 +381,14 @@ interface MastodonApi {
@Query("max_id") maxId: String?,
@Query("since_id") sinceId: String?,
@Query("limit") limit: Int?
): Call<List<Status>>
): Single<Response<List<Status>>>
@GET("api/v1/bookmarks")
fun bookmarks(
@Query("max_id") maxId: String?,
@Query("since_id") sinceId: String?,
@Query("limit") limit: Int?
): Call<List<Status>>
): Single<Response<List<Status>>>
@GET("api/v1/follow_requests")
fun followRequests(
@ -415,20 +398,10 @@ interface MastodonApi {
@POST("api/v1/follow_requests/{id}/authorize")
fun authorizeFollowRequest(
@Path("id") accountId: String
): Call<Relationship>
@POST("api/v1/follow_requests/{id}/reject")
fun rejectFollowRequest(
@Path("id") accountId: String
): Call<Relationship>
@POST("api/v1/follow_requests/{id}/authorize")
fun authorizeFollowRequestObservable(
@Path("id") accountId: String
): Single<Relationship>
@POST("api/v1/follow_requests/{id}/reject")
fun rejectFollowRequestObservable(
fun rejectFollowRequest(
@Path("id") accountId: String
): Single<Relationship>

View File

@ -66,9 +66,9 @@ class TimelineRepositoryImpl(
sinceIdMinusOne: String?, limit: Int,
accountId: Long, requestMode: TimelineRequestMode
): Single<out List<TimelineStatus>> {
return mastodonApi.homeTimelineSingle(maxId, sinceIdMinusOne, limit + 1)
.map { statuses ->
this.saveStatusesToDb(accountId, statuses, maxId, sinceId)
return mastodonApi.homeTimeline(maxId, sinceIdMinusOne, limit + 1)
.map { response ->
this.saveStatusesToDb(accountId, response.body().orEmpty(), maxId, sinceId)
}
.flatMap { statuses ->
this.addFromDbIfNeeded(accountId, statuses, maxId, sinceId, limit, requestMode)
@ -85,7 +85,7 @@ class TimelineRepositoryImpl(
private fun addFromDbIfNeeded(accountId: Long, statuses: List<Either<Placeholder, Status>>,
maxId: String?, sinceId: String?, limit: Int,
requestMode: TimelineRequestMode
): Single<List<TimelineStatus>>? {
): Single<List<TimelineStatus>> {
return if (requestMode != NETWORK && statuses.size < 2) {
val newMaxID = if (statuses.isEmpty()) {
maxId

View File

@ -28,6 +28,7 @@ import org.mockito.ArgumentMatchers.*
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import org.robolectric.annotation.Config
import retrofit2.Response
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.collections.ArrayList
@ -76,8 +77,8 @@ class TimelineRepositoryTest {
makeStatus("3"),
makeStatus("2")
)
whenever(mastodonApi.homeTimelineSingle(isNull(), isNull(), anyInt()))
.thenReturn(Single.just(statuses))
whenever(mastodonApi.homeTimeline(isNull(), isNull(), anyInt()))
.thenReturn(Single.just(Response.success(statuses)))
val result = subject.getStatuses(null, null, null, limit, TimelineRequestMode.NETWORK)
.blockingGet()
@ -107,8 +108,8 @@ class TimelineRepositoryTest {
)
val sinceId = "2"
val sinceIdMinusOne = "1"
whenever(mastodonApi.homeTimelineSingle(null, sinceIdMinusOne, limit + 1))
.thenReturn(Single.just(response))
whenever(mastodonApi.homeTimeline(null, sinceIdMinusOne, limit + 1))
.thenReturn(Single.just(Response.success(response)))
val result = subject.getStatuses(null, sinceId, sinceIdMinusOne, limit,
TimelineRequestMode.NETWORK)
.blockingGet()
@ -141,8 +142,8 @@ class TimelineRepositoryTest {
)
val sinceId = "2"
val sinceIdMinusOne = "1"
whenever(mastodonApi.homeTimelineSingle(null, sinceIdMinusOne, limit + 1))
.thenReturn(Single.just(response))
whenever(mastodonApi.homeTimeline(null, sinceIdMinusOne, limit + 1))
.thenReturn(Single.just(Response.success(response)))
val result = subject.getStatuses(null, sinceId, sinceIdMinusOne, limit,
TimelineRequestMode.NETWORK)
.blockingGet()
@ -181,8 +182,8 @@ class TimelineRepositoryTest {
val sinceId = "2"
val sinceIdMinusOne = "1"
val maxId = "3"
whenever(mastodonApi.homeTimelineSingle(maxId, sinceIdMinusOne, limit + 1))
.thenReturn(Single.just(response))
whenever(mastodonApi.homeTimeline(maxId, sinceIdMinusOne, limit + 1))
.thenReturn(Single.just(Response.success(response)))
val result = subject.getStatuses(maxId, sinceId, sinceIdMinusOne, limit,
TimelineRequestMode.NETWORK)
.blockingGet()
@ -224,8 +225,8 @@ class TimelineRepositoryTest {
val sinceId = "2"
val sinceIdMinusOne = "1"
val maxId = "4"
whenever(mastodonApi.homeTimelineSingle(maxId, sinceIdMinusOne, limit + 1))
.thenReturn(Single.just(response))
whenever(mastodonApi.homeTimeline(maxId, sinceIdMinusOne, limit + 1))
.thenReturn(Single.just(Response.success(response)))
val result = subject.getStatuses(maxId, sinceId, sinceIdMinusOne, limit,
TimelineRequestMode.NETWORK)
.blockingGet()
@ -263,8 +264,8 @@ class TimelineRepositoryTest {
dbResult.status = dbStatus.toEntity(account.id, gson)
dbResult.account = status.account.toEntity(account.id, gson)
whenever(mastodonApi.homeTimelineSingle(any(), any(), any()))
.thenReturn(Single.just(listOf(status)))
whenever(mastodonApi.homeTimeline(any(), any(), any()))
.thenReturn(Single.just(Response.success((listOf(status)))))
whenever(timelineDao.getStatusesForAccount(account.id, status.id, null, 30))
.thenReturn(Single.just(listOf(dbResult)))
val result = subject.getStatuses(null, null, null, limit, TimelineRequestMode.ANY)
@ -281,8 +282,8 @@ class TimelineRepositoryTest {
val dbResult2 = TimelineStatusWithAccount()
dbResult2.status = Placeholder("1").toEntity(account.id)
whenever(mastodonApi.homeTimelineSingle(any(), any(), any()))
.thenReturn(Single.just(listOf(status)))
whenever(mastodonApi.homeTimeline(any(), any(), any()))
.thenReturn(Single.just(Response.success(listOf(status))))
whenever(timelineDao.getStatusesForAccount(account.id, status.id, null, 30))
.thenReturn(Single.just(listOf(dbResult, dbResult2)))
val result = subject.getStatuses(null, null, null, limit, TimelineRequestMode.ANY)