Add EventHub, add fav, reblog events, improve timelines (#562)
* Add AppStore, add fav, reblog events * Add events, add handling to Timeline * Add event handling to Notifications * Mostly finish events * Fix unsubscribing * Cleanup timeline * Fix newStatusEvent in thread, fix deleteEvent * Insert new toots only in specific timelines * Add missing else * Rename AppStore to EventHub * Fix tests * Use DiffUtils for timeline * Fix empty timeline bug. Improve loading placeholder * Fix AsyncListDiff, loading indicator, "load more" * Timeline fixes & improvements. Fix infinite loading. Remove spinner correctly. Don't refresh timeline without need.
This commit is contained in:
parent
3a8d96346b
commit
3756a1fd20
31 changed files with 1064 additions and 542 deletions
|
|
@ -16,6 +16,7 @@
|
|||
package com.keylesspalace.tusky.fragment;
|
||||
|
||||
import android.arch.core.util.Function;
|
||||
import android.arch.lifecycle.Lifecycle;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
|
@ -24,11 +25,12 @@ import android.preference.PreferenceManager;
|
|||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.design.widget.Snackbar;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.support.v4.util.Pair;
|
||||
import android.support.v4.widget.SwipeRefreshLayout;
|
||||
import android.support.v7.widget.DividerItemDecoration;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.SimpleItemAnimator;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
|
|
@ -39,14 +41,19 @@ import com.keylesspalace.tusky.BuildConfig;
|
|||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.ViewThreadActivity;
|
||||
import com.keylesspalace.tusky.adapter.ThreadAdapter;
|
||||
import com.keylesspalace.tusky.appstore.EventHub;
|
||||
import com.keylesspalace.tusky.appstore.BlockEvent;
|
||||
import com.keylesspalace.tusky.appstore.FavoriteEvent;
|
||||
import com.keylesspalace.tusky.appstore.ReblogEvent;
|
||||
import com.keylesspalace.tusky.appstore.StatusComposedEvent;
|
||||
import com.keylesspalace.tusky.appstore.StatusDeletedEvent;
|
||||
import com.keylesspalace.tusky.di.Injectable;
|
||||
import com.keylesspalace.tusky.entity.Attachment;
|
||||
import com.keylesspalace.tusky.entity.Card;
|
||||
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.network.TimelineCases;
|
||||
import com.keylesspalace.tusky.receiver.TimelineReceiver;
|
||||
import com.keylesspalace.tusky.util.PairedList;
|
||||
import com.keylesspalace.tusky.util.ThemeUtils;
|
||||
import com.keylesspalace.tusky.util.ViewDataUtils;
|
||||
|
|
@ -59,22 +66,29 @@ 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.*;
|
||||
import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.*;
|
||||
|
||||
public final class ViewThreadFragment extends SFragment implements
|
||||
SwipeRefreshLayout.OnRefreshListener, StatusActionListener, Injectable {
|
||||
private static final String TAG = "ViewThreadFragment";
|
||||
|
||||
@Inject
|
||||
public TimelineCases timelineCases;
|
||||
@Inject
|
||||
public MastodonApi mastodonApi;
|
||||
@Inject
|
||||
public EventHub eventHub;
|
||||
|
||||
private SwipeRefreshLayout swipeRefreshLayout;
|
||||
private RecyclerView recyclerView;
|
||||
private ThreadAdapter adapter;
|
||||
private String thisThreadsStatusId;
|
||||
private TimelineReceiver timelineReceiver;
|
||||
private Card card;
|
||||
private boolean alwaysShowSensitiveMedia;
|
||||
|
||||
|
|
@ -101,6 +115,36 @@ public final class ViewThreadFragment extends SFragment implements
|
|||
return timelineCases;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
adapter = new ThreadAdapter(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPostCreate() {
|
||||
super.onPostCreate();
|
||||
|
||||
eventHub.getEvents()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
|
||||
.subscribe(event -> {
|
||||
if (event instanceof FavoriteEvent) {
|
||||
handleFavEvent((FavoriteEvent) event);
|
||||
} else if (event instanceof ReblogEvent) {
|
||||
handleReblogEvent((ReblogEvent) event);
|
||||
} else if (event instanceof BlockEvent) {
|
||||
removeAllByAccountId(((BlockEvent) event).getAccountId());
|
||||
} else if (event instanceof StatusComposedEvent) {
|
||||
handleStatusComposedEvent((StatusComposedEvent) event);
|
||||
} else if (event instanceof StatusDeletedEvent) {
|
||||
handleStatusDeletedEvent((StatusDeletedEvent) event);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
|
|
@ -128,7 +172,6 @@ public final class ViewThreadFragment extends SFragment implements
|
|||
R.drawable.conversation_thread_line_dark);
|
||||
recyclerView.addItemDecoration(new ConversationLineItemDecoration(context,
|
||||
threadLineDrawable));
|
||||
adapter = new ThreadAdapter(this);
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(
|
||||
getActivity());
|
||||
alwaysShowSensitiveMedia = preferences.getBoolean("alwaysShowSensitiveMedia", false);
|
||||
|
|
@ -139,19 +182,11 @@ public final class ViewThreadFragment extends SFragment implements
|
|||
statuses.clear();
|
||||
thisThreadsStatusId = null;
|
||||
|
||||
timelineReceiver = new TimelineReceiver(this, this);
|
||||
LocalBroadcastManager.getInstance(context.getApplicationContext())
|
||||
.registerReceiver(timelineReceiver, TimelineReceiver.getFilter(null));
|
||||
((SimpleItemAnimator) recyclerView.getItemAnimator()).setSupportsChangeAnimations(false);
|
||||
|
||||
return rootView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
LocalBroadcastManager.getInstance(getContext())
|
||||
.unregisterReceiver(timelineReceiver);
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
||||
|
|
@ -202,21 +237,8 @@ public final class ViewThreadFragment extends SFragment implements
|
|||
@Override
|
||||
public void onResponse(@NonNull Call<Status> call, @NonNull Response<Status> response) {
|
||||
if (response.isSuccessful()) {
|
||||
status.setReblogged(reblog);
|
||||
|
||||
if (status.getReblog() != null) {
|
||||
status.getReblog().setReblogged(reblog);
|
||||
}
|
||||
|
||||
StatusViewData.Concrete viewdata = statuses.getPairedItem(position);
|
||||
|
||||
StatusViewData.Builder viewDataBuilder = new StatusViewData.Builder((viewdata));
|
||||
viewDataBuilder.setReblogged(reblog);
|
||||
|
||||
StatusViewData.Concrete newViewData = viewDataBuilder.createStatusViewData();
|
||||
|
||||
statuses.setPairedItem(position, newViewData);
|
||||
adapter.setItem(position, newViewData, false);
|
||||
setReblogForStatus(position, status, reblog);
|
||||
eventHub.dispatch(new ReblogEvent(status.getId(), reblog));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -228,6 +250,24 @@ public final class ViewThreadFragment extends SFragment implements
|
|||
});
|
||||
}
|
||||
|
||||
private void setReblogForStatus(int position, Status status, boolean reblog) {
|
||||
status.setReblogged(reblog);
|
||||
|
||||
if (status.getReblog() != null) {
|
||||
status.getReblog().setReblogged(reblog);
|
||||
}
|
||||
|
||||
StatusViewData.Concrete viewdata = statuses.getPairedItem(position);
|
||||
|
||||
StatusViewData.Builder viewDataBuilder = new StatusViewData.Builder((viewdata));
|
||||
viewDataBuilder.setReblogged(reblog);
|
||||
|
||||
StatusViewData.Concrete newViewData = viewDataBuilder.createStatusViewData();
|
||||
|
||||
statuses.setPairedItem(position, newViewData);
|
||||
adapter.setItem(position, newViewData, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFavourite(final boolean favourite, final int position) {
|
||||
final Status status = statuses.get(position);
|
||||
|
|
@ -235,21 +275,8 @@ public final class ViewThreadFragment extends SFragment implements
|
|||
@Override
|
||||
public void onResponse(@NonNull Call<Status> call, @NonNull Response<Status> response) {
|
||||
if (response.isSuccessful()) {
|
||||
status.setFavourited(favourite);
|
||||
|
||||
if (status.getReblog() != null) {
|
||||
status.getReblog().setFavourited(favourite);
|
||||
}
|
||||
|
||||
StatusViewData.Concrete viewdata = statuses.getPairedItem(position);
|
||||
|
||||
StatusViewData.Builder viewDataBuilder = new StatusViewData.Builder((viewdata));
|
||||
viewDataBuilder.setFavourited(favourite);
|
||||
|
||||
StatusViewData.Concrete newViewData = viewDataBuilder.createStatusViewData();
|
||||
|
||||
statuses.setPairedItem(position, newViewData);
|
||||
adapter.setItem(position, newViewData, false);
|
||||
setFavForStatus(position, status, favourite);
|
||||
eventHub.dispatch(new FavoriteEvent(status.getId(), favourite));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -261,6 +288,24 @@ public final class ViewThreadFragment extends SFragment implements
|
|||
});
|
||||
}
|
||||
|
||||
private void setFavForStatus(int position, Status status, boolean favourite) {
|
||||
status.setFavourited(favourite);
|
||||
|
||||
if (status.getReblog() != null) {
|
||||
status.getReblog().setFavourited(favourite);
|
||||
}
|
||||
|
||||
StatusViewData.Concrete viewdata = statuses.getPairedItem(position);
|
||||
|
||||
StatusViewData.Builder viewDataBuilder = new StatusViewData.Builder((viewdata));
|
||||
viewDataBuilder.setFavourited(favourite);
|
||||
|
||||
StatusViewData.Concrete newViewData = viewDataBuilder.createStatusViewData();
|
||||
|
||||
statuses.setPairedItem(position, newViewData);
|
||||
adapter.setItem(position, newViewData, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMore(View view, int position) {
|
||||
super.more(statuses.get(position), view, position);
|
||||
|
|
@ -334,8 +379,7 @@ public final class ViewThreadFragment extends SFragment implements
|
|||
adapter.setStatuses(statuses.getPairedCopy());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAllByAccountId(String accountId) {
|
||||
private void removeAllByAccountId(String accountId) {
|
||||
Status status = null;
|
||||
if (!statuses.isEmpty()) {
|
||||
status = statuses.get(statusIndex);
|
||||
|
|
@ -532,6 +576,69 @@ public final class ViewThreadFragment extends SFragment implements
|
|||
}
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
statuses.clear();
|
||||
adapter.clear();
|
||||
}
|
||||
|
||||
private void handleFavEvent(FavoriteEvent event) {
|
||||
Pair<Integer, Status> posAndStatus = findStatusAndPos(event.getStatusId());
|
||||
if (posAndStatus == null) return;
|
||||
//noinspection ConstantConditions
|
||||
setFavForStatus(posAndStatus.first, posAndStatus.second, event.getFavourite());
|
||||
}
|
||||
|
||||
private void handleReblogEvent(ReblogEvent event) {
|
||||
Pair<Integer, Status> posAndStatus = findStatusAndPos(event.getStatusId());
|
||||
if (posAndStatus == null) return;
|
||||
//noinspection ConstantConditions
|
||||
setReblogForStatus(posAndStatus.first, posAndStatus.second, event.getReblog());
|
||||
}
|
||||
|
||||
private void handleStatusComposedEvent(StatusComposedEvent event) {
|
||||
Status eventStatus = event.getStatus();
|
||||
if (eventStatus.getInReplyToId() == null) return;
|
||||
|
||||
if (eventStatus.getInReplyToId().equals(statuses.get(statusIndex).getId())) {
|
||||
insertStatus(eventStatus, statuses.size());
|
||||
} else {
|
||||
// If new status is a reply to some status in the thread, insert new status after it
|
||||
// We only check statuses below main status, ones on top don't belong to this thread
|
||||
for (int i = statusIndex; i < statuses.size(); i++) {
|
||||
Status status = statuses.get(i);
|
||||
if (eventStatus.getInReplyToId().equals(status.getId())) {
|
||||
insertStatus(eventStatus, i + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void insertStatus(Status status, int at) {
|
||||
statuses.add(at, status);
|
||||
adapter.addItem(at, statuses.getPairedItem(at));
|
||||
}
|
||||
|
||||
private void handleStatusDeletedEvent(StatusDeletedEvent event) {
|
||||
Pair<Integer, Status> posAndStatus = findStatusAndPos(event.getStatusId());
|
||||
if (posAndStatus == null) return;
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
int pos = posAndStatus.first;
|
||||
statuses.remove(pos);
|
||||
adapter.removeItem(pos);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Pair<Integer, Status> findStatusAndPos(@NonNull String statusId) {
|
||||
for (int i = 0; i < statuses.size(); i++) {
|
||||
if (statusId.equals(statuses.get(i).getId())) {
|
||||
return new Pair<>(i, statuses.get(i));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void updateRevealIcon() {
|
||||
ViewThreadActivity activity = ((ViewThreadActivity) getActivity());
|
||||
if (activity == null) return;
|
||||
|
|
@ -545,8 +652,8 @@ public final class ViewThreadFragment extends SFragment implements
|
|||
}
|
||||
}
|
||||
if (!hasAnyWarnings) {
|
||||
activity.setRevealButtonState(ViewThreadActivity.REVEAL_BUTTON_HIDDEN);
|
||||
return;
|
||||
activity.setRevealButtonState(ViewThreadActivity.REVEAL_BUTTON_HIDDEN);
|
||||
return;
|
||||
}
|
||||
activity.setRevealButtonState(allExpanded() ? ViewThreadActivity.REVEAL_BUTTON_HIDE :
|
||||
ViewThreadActivity.REVEAL_BUTTON_REVEAL);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue