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
|
@ -96,4 +96,9 @@ dependencies {
|
||||||
})
|
})
|
||||||
|
|
||||||
debugImplementation 'im.dino:dbinspector:3.4.1@aar'
|
debugImplementation 'im.dino:dbinspector:3.4.1@aar'
|
||||||
|
|
||||||
|
implementation 'io.reactivex.rxjava2:rxjava:2.1.12'
|
||||||
|
implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
|
||||||
|
implementation 'com.uber.autodispose:autodispose-android-archcomponents:0.7.0'
|
||||||
|
implementation 'com.uber.autodispose:autodispose-kotlin:0.7.0'
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,13 +47,16 @@ import android.widget.Button;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.keylesspalace.tusky.appstore.EventHub;
|
||||||
|
import com.keylesspalace.tusky.appstore.BlockEvent;
|
||||||
|
import com.keylesspalace.tusky.appstore.MuteEvent;
|
||||||
|
import com.keylesspalace.tusky.appstore.UnfollowEvent;
|
||||||
import com.keylesspalace.tusky.db.AccountEntity;
|
import com.keylesspalace.tusky.db.AccountEntity;
|
||||||
import com.keylesspalace.tusky.entity.Account;
|
import com.keylesspalace.tusky.entity.Account;
|
||||||
import com.keylesspalace.tusky.entity.Relationship;
|
import com.keylesspalace.tusky.entity.Relationship;
|
||||||
import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
|
import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
|
||||||
import com.keylesspalace.tusky.interfaces.LinkListener;
|
import com.keylesspalace.tusky.interfaces.LinkListener;
|
||||||
import com.keylesspalace.tusky.pager.AccountPagerAdapter;
|
import com.keylesspalace.tusky.pager.AccountPagerAdapter;
|
||||||
import com.keylesspalace.tusky.receiver.TimelineReceiver;
|
|
||||||
import com.keylesspalace.tusky.util.Assert;
|
import com.keylesspalace.tusky.util.Assert;
|
||||||
import com.keylesspalace.tusky.util.LinkHelper;
|
import com.keylesspalace.tusky.util.LinkHelper;
|
||||||
import com.keylesspalace.tusky.util.ThemeUtils;
|
import com.keylesspalace.tusky.util.ThemeUtils;
|
||||||
|
@ -86,6 +89,8 @@ public final class AccountActivity extends BottomSheetActivity implements Action
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public DispatchingAndroidInjector<Fragment> dispatchingAndroidInjector;
|
public DispatchingAndroidInjector<Fragment> dispatchingAndroidInjector;
|
||||||
|
@Inject
|
||||||
|
public EventHub appstore;
|
||||||
|
|
||||||
private String accountId;
|
private String accountId;
|
||||||
private FollowState followState;
|
private FollowState followState;
|
||||||
|
@ -524,7 +529,7 @@ public final class AccountActivity extends BottomSheetActivity implements Action
|
||||||
Snackbar.LENGTH_LONG).show();
|
Snackbar.LENGTH_LONG).show();
|
||||||
} else {
|
} else {
|
||||||
followState = FollowState.NOT_FOLLOWING;
|
followState = FollowState.NOT_FOLLOWING;
|
||||||
broadcast(TimelineReceiver.Types.UNFOLLOW_ACCOUNT, id);
|
appstore.dispatch(new UnfollowEvent(id));
|
||||||
}
|
}
|
||||||
updateButtons();
|
updateButtons();
|
||||||
} else {
|
} else {
|
||||||
|
@ -581,7 +586,7 @@ public final class AccountActivity extends BottomSheetActivity implements Action
|
||||||
@NonNull Response<Relationship> response) {
|
@NonNull Response<Relationship> response) {
|
||||||
Relationship relationship = response.body();
|
Relationship relationship = response.body();
|
||||||
if (response.isSuccessful() && relationship != null) {
|
if (response.isSuccessful() && relationship != null) {
|
||||||
broadcast(TimelineReceiver.Types.BLOCK_ACCOUNT, id);
|
appstore.dispatch(new BlockEvent(id));
|
||||||
blocking = relationship.getBlocking();
|
blocking = relationship.getBlocking();
|
||||||
updateButtons();
|
updateButtons();
|
||||||
} else {
|
} else {
|
||||||
|
@ -615,7 +620,7 @@ public final class AccountActivity extends BottomSheetActivity implements Action
|
||||||
@NonNull Response<Relationship> response) {
|
@NonNull Response<Relationship> response) {
|
||||||
Relationship relationship = response.body();
|
Relationship relationship = response.body();
|
||||||
if (response.isSuccessful() && relationship != null) {
|
if (response.isSuccessful() && relationship != null) {
|
||||||
broadcast(TimelineReceiver.Types.MUTE_ACCOUNT, id);
|
appstore.dispatch(new MuteEvent(id));
|
||||||
muting = relationship.getMuting();
|
muting = relationship.getMuting();
|
||||||
updateButtons();
|
updateButtons();
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -85,6 +85,8 @@ import com.keylesspalace.tusky.adapter.EmojiAdapter;
|
||||||
import com.keylesspalace.tusky.adapter.MentionAutoCompleteAdapter;
|
import com.keylesspalace.tusky.adapter.MentionAutoCompleteAdapter;
|
||||||
import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener;
|
import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener;
|
||||||
import com.keylesspalace.tusky.db.AccountEntity;
|
import com.keylesspalace.tusky.db.AccountEntity;
|
||||||
|
import com.keylesspalace.tusky.db.AccountManager;
|
||||||
|
import com.keylesspalace.tusky.db.AppDatabase;
|
||||||
import com.keylesspalace.tusky.db.InstanceEntity;
|
import com.keylesspalace.tusky.db.InstanceEntity;
|
||||||
import com.keylesspalace.tusky.di.Injectable;
|
import com.keylesspalace.tusky.di.Injectable;
|
||||||
import com.keylesspalace.tusky.entity.Account;
|
import com.keylesspalace.tusky.entity.Account;
|
||||||
|
@ -169,6 +171,8 @@ public final class ComposeActivity
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public MastodonApi mastodonApi;
|
public MastodonApi mastodonApi;
|
||||||
|
@Inject
|
||||||
|
public AppDatabase database;
|
||||||
|
|
||||||
private TextView replyTextView;
|
private TextView replyTextView;
|
||||||
private TextView replyContentTextView;
|
private TextView replyContentTextView;
|
||||||
|
@ -230,7 +234,7 @@ public final class ComposeActivity
|
||||||
emojiView = findViewById(R.id.emojiView);
|
emojiView = findViewById(R.id.emojiView);
|
||||||
emojiList = Collections.emptyList();
|
emojiList = Collections.emptyList();
|
||||||
|
|
||||||
saveTootHelper = new SaveTootHelper(TuskyApplication.getDB().tootDao(), this);
|
saveTootHelper = new SaveTootHelper(database.tootDao(), this);
|
||||||
|
|
||||||
// Setup the toolbar.
|
// Setup the toolbar.
|
||||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||||
|
@ -1454,7 +1458,8 @@ public final class ComposeActivity
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadCachedInstanceMetadata(@NotNull AccountEntity activeAccount) {
|
private void loadCachedInstanceMetadata(@NotNull AccountEntity activeAccount) {
|
||||||
InstanceEntity instanceEntity = TuskyApplication.getDB().instanceDao().loadMetadataForInstance(activeAccount.getDomain());
|
InstanceEntity instanceEntity = database.instanceDao()
|
||||||
|
.loadMetadataForInstance(activeAccount.getDomain());
|
||||||
|
|
||||||
if(instanceEntity != null) {
|
if(instanceEntity != null) {
|
||||||
Integer max = instanceEntity.getMaximumTootCharacters();
|
Integer max = instanceEntity.getMaximumTootCharacters();
|
||||||
|
@ -1474,7 +1479,7 @@ public final class ComposeActivity
|
||||||
|
|
||||||
private void cacheInstanceMetadata(@NotNull AccountEntity activeAccount) {
|
private void cacheInstanceMetadata(@NotNull AccountEntity activeAccount) {
|
||||||
InstanceEntity instanceEntity = new InstanceEntity(activeAccount.getDomain(), emojiList, maximumTootCharacters);
|
InstanceEntity instanceEntity = new InstanceEntity(activeAccount.getDomain(), emojiList, maximumTootCharacters);
|
||||||
TuskyApplication.getDB().instanceDao().insertOrReplace(instanceEntity);
|
database.instanceDao().insertOrReplace(instanceEntity);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Accessors for testing, hence package scope
|
// Accessors for testing, hence package scope
|
||||||
|
|
|
@ -70,7 +70,7 @@ import retrofit2.Call;
|
||||||
import retrofit2.Callback;
|
import retrofit2.Callback;
|
||||||
import retrofit2.Response;
|
import retrofit2.Response;
|
||||||
|
|
||||||
public class MainActivity extends BottomSheetActivity implements ActionButtonActivity,
|
public final class MainActivity extends BottomSheetActivity implements ActionButtonActivity,
|
||||||
HasSupportFragmentInjector {
|
HasSupportFragmentInjector {
|
||||||
private static final String TAG = "MainActivity"; // logging tag
|
private static final String TAG = "MainActivity"; // logging tag
|
||||||
private static final long DRAWER_ITEM_ADD_ACCOUNT = -13;
|
private static final long DRAWER_ITEM_ADD_ACCOUNT = -13;
|
||||||
|
|
|
@ -15,15 +15,12 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky;
|
package com.keylesspalace.tusky;
|
||||||
|
|
||||||
import android.content.BroadcastReceiver;
|
import android.arch.lifecycle.Lifecycle;
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.IntentFilter;
|
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.v4.content.LocalBroadcastManager;
|
|
||||||
import android.support.v7.app.ActionBar;
|
import android.support.v7.app.ActionBar;
|
||||||
import android.support.v7.widget.DividerItemDecoration;
|
import android.support.v7.widget.DividerItemDecoration;
|
||||||
import android.support.v7.widget.LinearLayoutManager;
|
import android.support.v7.widget.LinearLayoutManager;
|
||||||
|
@ -34,9 +31,12 @@ import android.view.View;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import com.keylesspalace.tusky.adapter.SavedTootAdapter;
|
import com.keylesspalace.tusky.adapter.SavedTootAdapter;
|
||||||
|
import com.keylesspalace.tusky.appstore.EventHub;
|
||||||
|
import com.keylesspalace.tusky.appstore.StatusComposedEvent;
|
||||||
|
import com.keylesspalace.tusky.db.AppDatabase;
|
||||||
import com.keylesspalace.tusky.db.TootDao;
|
import com.keylesspalace.tusky.db.TootDao;
|
||||||
import com.keylesspalace.tusky.db.TootEntity;
|
import com.keylesspalace.tusky.db.TootEntity;
|
||||||
import com.keylesspalace.tusky.receiver.TimelineReceiver;
|
import com.keylesspalace.tusky.di.Injectable;
|
||||||
import com.keylesspalace.tusky.util.SaveTootHelper;
|
import com.keylesspalace.tusky.util.SaveTootHelper;
|
||||||
import com.keylesspalace.tusky.util.ThemeUtils;
|
import com.keylesspalace.tusky.util.ThemeUtils;
|
||||||
|
|
||||||
|
@ -44,10 +44,15 @@ import java.lang.ref.WeakReference;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class SavedTootActivity extends BaseActivity implements SavedTootAdapter.SavedTootAction {
|
import javax.inject.Inject;
|
||||||
|
|
||||||
// dao
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
private static TootDao tootDao = TuskyApplication.getDB().tootDao();
|
|
||||||
|
import static com.uber.autodispose.AutoDispose.autoDisposable;
|
||||||
|
import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from;
|
||||||
|
|
||||||
|
public final class SavedTootActivity extends BaseActivity implements SavedTootAdapter.SavedTootAction,
|
||||||
|
Injectable {
|
||||||
|
|
||||||
private SaveTootHelper saveTootHelper;
|
private SaveTootHelper saveTootHelper;
|
||||||
|
|
||||||
|
@ -56,28 +61,25 @@ public class SavedTootActivity extends BaseActivity implements SavedTootAdapter.
|
||||||
private TextView noContent;
|
private TextView noContent;
|
||||||
|
|
||||||
private List<TootEntity> toots = new ArrayList<>();
|
private List<TootEntity> toots = new ArrayList<>();
|
||||||
@Nullable private AsyncTask<?, ?, ?> asyncTask;
|
@Nullable
|
||||||
|
private AsyncTask<?, ?, ?> asyncTask;
|
||||||
|
|
||||||
private BroadcastReceiver broadcastReceiver;
|
@Inject
|
||||||
|
EventHub eventHub;
|
||||||
|
@Inject
|
||||||
|
AppDatabase database;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
saveTootHelper = new SaveTootHelper(tootDao, this);
|
saveTootHelper = new SaveTootHelper(database.tootDao(), this);
|
||||||
|
|
||||||
broadcastReceiver = new BroadcastReceiver() {
|
eventHub.getEvents()
|
||||||
@Override
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
public void onReceive(Context context, Intent intent) {
|
.ofType(StatusComposedEvent.class)
|
||||||
fetchToots();
|
.as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
|
||||||
}
|
.subscribe((__) -> this.fetchToots());
|
||||||
};
|
|
||||||
|
|
||||||
IntentFilter intentFilter = new IntentFilter();
|
|
||||||
intentFilter.addAction(TimelineReceiver.Types.STATUS_COMPOSED);
|
|
||||||
|
|
||||||
LocalBroadcastManager.getInstance(this)
|
|
||||||
.registerReceiver(broadcastReceiver, intentFilter);
|
|
||||||
|
|
||||||
setContentView(R.layout.activity_saved_toot);
|
setContentView(R.layout.activity_saved_toot);
|
||||||
|
|
||||||
|
@ -117,12 +119,6 @@ public class SavedTootActivity extends BaseActivity implements SavedTootAdapter.
|
||||||
if (asyncTask != null) asyncTask.cancel(true);
|
if (asyncTask != null) asyncTask.cancel(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(broadcastReceiver);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
switch (item.getItemId()) {
|
switch (item.getItemId()) {
|
||||||
|
@ -135,7 +131,7 @@ public class SavedTootActivity extends BaseActivity implements SavedTootAdapter.
|
||||||
}
|
}
|
||||||
|
|
||||||
private void fetchToots() {
|
private void fetchToots() {
|
||||||
asyncTask = new FetchPojosTask(this)
|
asyncTask = new FetchPojosTask(this, database.tootDao())
|
||||||
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,9 +174,11 @@ public class SavedTootActivity extends BaseActivity implements SavedTootAdapter.
|
||||||
static final class FetchPojosTask extends AsyncTask<Void, Void, List<TootEntity>> {
|
static final class FetchPojosTask extends AsyncTask<Void, Void, List<TootEntity>> {
|
||||||
|
|
||||||
private final WeakReference<SavedTootActivity> activityRef;
|
private final WeakReference<SavedTootActivity> activityRef;
|
||||||
|
private final TootDao tootDao;
|
||||||
|
|
||||||
FetchPojosTask(SavedTootActivity activity) {
|
FetchPojosTask(SavedTootActivity activity, TootDao tootDao) {
|
||||||
this.activityRef = new WeakReference<>(activity);
|
this.activityRef = new WeakReference<>(activity);
|
||||||
|
this.tootDao = tootDao;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -194,13 +192,12 @@ public class SavedTootActivity extends BaseActivity implements SavedTootAdapter.
|
||||||
SavedTootActivity activity = activityRef.get();
|
SavedTootActivity activity = activityRef.get();
|
||||||
if (activity == null) return;
|
if (activity == null) return;
|
||||||
|
|
||||||
|
activity.toots.clear();
|
||||||
activity.toots.addAll(pojos);
|
activity.toots.addAll(pojos);
|
||||||
|
|
||||||
// set ui
|
// set ui
|
||||||
activity.setNoContent(pojos.size());
|
activity.setNoContent(pojos.size());
|
||||||
List<TootEntity> toots = new ArrayList<>(pojos.size());
|
activity.adapter.setItems(activity.toots);
|
||||||
toots.addAll(pojos);
|
|
||||||
activity.adapter.setItems(toots);
|
|
||||||
activity.adapter.notifyDataSetChanged();
|
activity.adapter.notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,30 +18,35 @@ package com.keylesspalace.tusky.adapter;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
|
import android.widget.ProgressBar;
|
||||||
|
|
||||||
import com.keylesspalace.tusky.R;
|
import com.keylesspalace.tusky.R;
|
||||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||||
|
|
||||||
public class PlaceholderViewHolder extends RecyclerView.ViewHolder {
|
public final class PlaceholderViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
|
||||||
private Button loadMoreButton;
|
private Button loadMoreButton;
|
||||||
|
private ProgressBar progressBar;
|
||||||
|
|
||||||
PlaceholderViewHolder(View itemView) {
|
PlaceholderViewHolder(View itemView) {
|
||||||
super(itemView);
|
super(itemView);
|
||||||
loadMoreButton = itemView.findViewById(R.id.button_load_more);
|
loadMoreButton = itemView.findViewById(R.id.button_load_more);
|
||||||
|
progressBar = itemView.findViewById(R.id.progress_bar);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setup(boolean enabled, final StatusActionListener listener){
|
public void setup(boolean enabled, final StatusActionListener listener) {
|
||||||
|
this.setup(enabled, listener, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setup(boolean enabled, final StatusActionListener listener, boolean progress) {
|
||||||
|
loadMoreButton.setVisibility(progress ? View.GONE : View.VISIBLE);
|
||||||
|
progressBar.setVisibility(progress ? View.VISIBLE : View.GONE);
|
||||||
|
|
||||||
loadMoreButton.setEnabled(enabled);
|
loadMoreButton.setEnabled(enabled);
|
||||||
if(enabled) {
|
if (enabled) {
|
||||||
loadMoreButton.setOnClickListener(new View.OnClickListener() {
|
loadMoreButton.setOnClickListener(v -> {
|
||||||
@Override
|
loadMoreButton.setEnabled(false);
|
||||||
public void onClick(View v) {
|
listener.onLoadMore(getAdapterPosition());
|
||||||
loadMoreButton.setEnabled(false);
|
|
||||||
listener.onLoadMore(getAdapterPosition());
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -118,6 +118,11 @@ public class ThreadAdapter extends RecyclerView.Adapter {
|
||||||
notifyItemRangeInserted(end, statuses.size());
|
notifyItemRangeInserted(end, statuses.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void removeItem(int position) {
|
||||||
|
statuses.remove(position);
|
||||||
|
notifyItemRemoved(position);
|
||||||
|
}
|
||||||
|
|
||||||
public void clear() {
|
public void clear() {
|
||||||
statuses.clear();
|
statuses.clear();
|
||||||
detailedStatusPosition = RecyclerView.NO_POSITION;
|
detailedStatusPosition = RecyclerView.NO_POSITION;
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.adapter;
|
package com.keylesspalace.tusky.adapter;
|
||||||
|
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
@ -25,29 +25,32 @@ import com.keylesspalace.tusky.R;
|
||||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||||
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
public final class TimelineAdapter extends RecyclerView.Adapter {
|
||||||
import java.util.List;
|
|
||||||
|
public interface AdapterDataSource<T> {
|
||||||
|
int getItemCount();
|
||||||
|
|
||||||
|
T getItemAt(int pos);
|
||||||
|
}
|
||||||
|
|
||||||
public class TimelineAdapter extends RecyclerView.Adapter {
|
|
||||||
private static final int VIEW_TYPE_STATUS = 0;
|
private static final int VIEW_TYPE_STATUS = 0;
|
||||||
private static final int VIEW_TYPE_FOOTER = 1;
|
|
||||||
private static final int VIEW_TYPE_PLACEHOLDER = 2;
|
private static final int VIEW_TYPE_PLACEHOLDER = 2;
|
||||||
|
|
||||||
private List<StatusViewData> statuses;
|
private final AdapterDataSource<StatusViewData> dataSource;
|
||||||
private StatusActionListener statusListener;
|
private final StatusActionListener statusListener;
|
||||||
private FooterViewHolder.State footerState;
|
|
||||||
private boolean mediaPreviewEnabled;
|
private boolean mediaPreviewEnabled;
|
||||||
|
|
||||||
public TimelineAdapter(StatusActionListener statusListener) {
|
public TimelineAdapter(AdapterDataSource<StatusViewData> dataSource,
|
||||||
|
StatusActionListener statusListener) {
|
||||||
super();
|
super();
|
||||||
statuses = new ArrayList<>();
|
this.dataSource = dataSource;
|
||||||
this.statusListener = statusListener;
|
this.statusListener = statusListener;
|
||||||
footerState = FooterViewHolder.State.END;
|
|
||||||
mediaPreviewEnabled = true;
|
mediaPreviewEnabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
|
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
|
||||||
switch (viewType) {
|
switch (viewType) {
|
||||||
default:
|
default:
|
||||||
case VIEW_TYPE_STATUS: {
|
case VIEW_TYPE_STATUS: {
|
||||||
|
@ -55,11 +58,6 @@ public class TimelineAdapter extends RecyclerView.Adapter {
|
||||||
.inflate(R.layout.item_status, viewGroup, false);
|
.inflate(R.layout.item_status, viewGroup, false);
|
||||||
return new StatusViewHolder(view);
|
return new StatusViewHolder(view);
|
||||||
}
|
}
|
||||||
case VIEW_TYPE_FOOTER: {
|
|
||||||
View view = LayoutInflater.from(viewGroup.getContext())
|
|
||||||
.inflate(R.layout.item_footer, viewGroup, false);
|
|
||||||
return new FooterViewHolder(view);
|
|
||||||
}
|
|
||||||
case VIEW_TYPE_PLACEHOLDER: {
|
case VIEW_TYPE_PLACEHOLDER: {
|
||||||
View view = LayoutInflater.from(viewGroup.getContext())
|
View view = LayoutInflater.from(viewGroup.getContext())
|
||||||
.inflate(R.layout.item_status_placeholder, viewGroup, false);
|
.inflate(R.layout.item_status_placeholder, viewGroup, false);
|
||||||
|
@ -69,76 +67,39 @@ public class TimelineAdapter extends RecyclerView.Adapter {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
|
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
|
||||||
if (position < statuses.size()) {
|
StatusViewData status = dataSource.getItemAt(position);
|
||||||
StatusViewData status = statuses.get(position);
|
if (status instanceof StatusViewData.Placeholder) {
|
||||||
if (status instanceof StatusViewData.Placeholder) {
|
PlaceholderViewHolder holder = (PlaceholderViewHolder) viewHolder;
|
||||||
PlaceholderViewHolder holder = (PlaceholderViewHolder) viewHolder;
|
holder.setup(!((StatusViewData.Placeholder) status).isLoading(),
|
||||||
holder.setup(!((StatusViewData.Placeholder) status).isLoading(), statusListener);
|
statusListener, ((StatusViewData.Placeholder) status).isLoading());
|
||||||
} else {
|
|
||||||
|
|
||||||
StatusViewHolder holder = (StatusViewHolder) viewHolder;
|
|
||||||
holder.setupWithStatus((StatusViewData.Concrete) status,
|
|
||||||
statusListener, mediaPreviewEnabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
FooterViewHolder holder = (FooterViewHolder) viewHolder;
|
StatusViewHolder holder = (StatusViewHolder) viewHolder;
|
||||||
holder.setState(footerState);
|
holder.setupWithStatus((StatusViewData.Concrete) status,
|
||||||
|
statusListener, mediaPreviewEnabled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getItemCount() {
|
public int getItemCount() {
|
||||||
return statuses.size() + 1;
|
return dataSource.getItemCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getItemViewType(int position) {
|
public int getItemViewType(int position) {
|
||||||
if (position == statuses.size()) {
|
if (dataSource.getItemAt(position) instanceof StatusViewData.Placeholder) {
|
||||||
return VIEW_TYPE_FOOTER;
|
return VIEW_TYPE_PLACEHOLDER;
|
||||||
} else {
|
} else {
|
||||||
if (statuses.get(position) instanceof StatusViewData.Placeholder) {
|
return VIEW_TYPE_STATUS;
|
||||||
return VIEW_TYPE_PLACEHOLDER;
|
|
||||||
} else {
|
|
||||||
return VIEW_TYPE_STATUS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void update(@Nullable List<StatusViewData> newStatuses) {
|
|
||||||
if (newStatuses == null || newStatuses.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
statuses.clear();
|
|
||||||
statuses.addAll(newStatuses);
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addItems(List<StatusViewData> newStatuses) {
|
|
||||||
statuses.addAll(newStatuses);
|
|
||||||
notifyItemRangeInserted(statuses.size(), newStatuses.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void changeItem(int position, StatusViewData newData, boolean notifyAdapter) {
|
|
||||||
statuses.set(position, newData);
|
|
||||||
if (notifyAdapter) notifyItemChanged(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void clear() {
|
|
||||||
statuses.clear();
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setFooterState(FooterViewHolder.State newFooterState) {
|
|
||||||
FooterViewHolder.State oldValue = footerState;
|
|
||||||
footerState = newFooterState;
|
|
||||||
if (footerState != oldValue) {
|
|
||||||
notifyItemChanged(statuses.size());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setMediaPreviewEnabled(boolean enabled) {
|
public void setMediaPreviewEnabled(boolean enabled) {
|
||||||
mediaPreviewEnabled = enabled;
|
mediaPreviewEnabled = enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getItemId(int position) {
|
||||||
|
return dataSource.getItemAt(position).getViewDataId();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
package com.keylesspalace.tusky.appstore
|
||||||
|
|
||||||
|
import io.reactivex.Observable
|
||||||
|
import io.reactivex.subjects.PublishSubject
|
||||||
|
|
||||||
|
interface Event
|
||||||
|
interface Dispatchable : Event
|
||||||
|
|
||||||
|
interface EventHub {
|
||||||
|
val events: Observable<Event>
|
||||||
|
fun dispatch(event: Dispatchable)
|
||||||
|
}
|
||||||
|
|
||||||
|
object EventHubImpl : EventHub {
|
||||||
|
|
||||||
|
private val eventsSubject = PublishSubject.create<Event>()
|
||||||
|
override val events: Observable<Event> = eventsSubject
|
||||||
|
|
||||||
|
override fun dispatch(event: Dispatchable) {
|
||||||
|
eventsSubject.onNext(event)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package com.keylesspalace.tusky.appstore
|
||||||
|
|
||||||
|
import com.keylesspalace.tusky.entity.Status
|
||||||
|
|
||||||
|
data class FavoriteEvent(val statusId: String, val favourite: Boolean) : Dispatchable
|
||||||
|
data class ReblogEvent(val statusId: String, val reblog: Boolean) : Dispatchable
|
||||||
|
data class UnfollowEvent(val accountId: String) : Dispatchable
|
||||||
|
data class BlockEvent(val accountId: String) : Dispatchable
|
||||||
|
data class MuteEvent(val accountId: String) : Dispatchable
|
||||||
|
data class StatusDeletedEvent(val statusId: String) : Dispatchable
|
||||||
|
data class StatusComposedEvent(val status: Status) : Dispatchable
|
|
@ -22,7 +22,10 @@ import android.content.SharedPreferences
|
||||||
import android.preference.PreferenceManager
|
import android.preference.PreferenceManager
|
||||||
import android.support.v4.content.LocalBroadcastManager
|
import android.support.v4.content.LocalBroadcastManager
|
||||||
import com.keylesspalace.tusky.TuskyApplication
|
import com.keylesspalace.tusky.TuskyApplication
|
||||||
|
import com.keylesspalace.tusky.appstore.EventHub
|
||||||
|
import com.keylesspalace.tusky.appstore.EventHubImpl
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
|
import com.keylesspalace.tusky.db.AppDatabase
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.network.TimelineCases
|
import com.keylesspalace.tusky.network.TimelineCases
|
||||||
import com.keylesspalace.tusky.network.TimelineCasesImpl
|
import com.keylesspalace.tusky.network.TimelineCasesImpl
|
||||||
|
@ -55,8 +58,8 @@ class AppModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
fun providesTimelineUseCases(api: MastodonApi,
|
fun providesTimelineUseCases(api: MastodonApi,
|
||||||
broadcastManager: LocalBroadcastManager): TimelineCases {
|
eventHub: EventHub): TimelineCases {
|
||||||
return TimelineCasesImpl(api, broadcastManager)
|
return TimelineCasesImpl(api, eventHub)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
@ -64,4 +67,12 @@ class AppModule {
|
||||||
fun providesAccountManager(app: TuskyApplication): AccountManager {
|
fun providesAccountManager(app: TuskyApplication): AccountManager {
|
||||||
return app.serviceLocator.get(AccountManager::class.java)
|
return app.serviceLocator.get(AccountManager::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun providesEventHub(): EventHub = EventHubImpl
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun providesDatabase(app: TuskyApplication): AppDatabase = TuskyApplication.getDB()
|
||||||
}
|
}
|
|
@ -124,6 +124,29 @@ data class Status(
|
||||||
|
|
||||||
@SerializedName("username")
|
@SerializedName("username")
|
||||||
var localUsername: String? = null
|
var localUsername: String? = null
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as Mention
|
||||||
|
|
||||||
|
if (id != other.id) return false
|
||||||
|
if (url != other.url) return false
|
||||||
|
if (username != other.username) return false
|
||||||
|
if (localUsername != other.localUsername) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = id?.hashCode() ?: 0
|
||||||
|
result = 31 * result + (url?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (username?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (localUsername?.hashCode() ?: 0)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Application {
|
class Application {
|
||||||
|
|
|
@ -154,7 +154,7 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
|
||||||
// Just use the basic scroll listener to load more accounts.
|
// Just use the basic scroll listener to load more accounts.
|
||||||
scrollListener = new EndlessOnScrollListener(layoutManager) {
|
scrollListener = new EndlessOnScrollListener(layoutManager) {
|
||||||
@Override
|
@Override
|
||||||
public void onLoadMore(int page, int totalItemsCount, RecyclerView view) {
|
public void onLoadMore(int totalItemsCount, RecyclerView view) {
|
||||||
AccountListFragment.this.onLoadMore(view);
|
AccountListFragment.this.onLoadMore(view);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,6 +16,8 @@
|
||||||
package com.keylesspalace.tusky.fragment;
|
package com.keylesspalace.tusky.fragment;
|
||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.v4.app.Fragment;
|
import android.support.v4.app.Fragment;
|
||||||
|
|
||||||
|
@ -26,11 +28,21 @@ import retrofit2.Call;
|
||||||
|
|
||||||
public class BaseFragment extends Fragment {
|
public class BaseFragment extends Fragment {
|
||||||
protected List<Call> callList;
|
protected List<Call> callList;
|
||||||
|
private final Handler handler = new Handler(Looper.getMainLooper());
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
callList = new ArrayList<>();
|
callList = new ArrayList<>();
|
||||||
|
handler.post(this::onPostCreate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For actions which should happen only once per lifecycle but after onCreate.
|
||||||
|
* Example: subscribe for events in {@code onCreate()} but need dependencies to be injected
|
||||||
|
*/
|
||||||
|
public void onPostCreate() {
|
||||||
|
// No-op
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -17,6 +17,7 @@ package com.keylesspalace.tusky.fragment;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.arch.core.util.Function;
|
import android.arch.core.util.Function;
|
||||||
|
import android.arch.lifecycle.Lifecycle;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
|
@ -26,30 +27,36 @@ import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.design.widget.FloatingActionButton;
|
import android.support.design.widget.FloatingActionButton;
|
||||||
import android.support.design.widget.TabLayout;
|
import android.support.design.widget.TabLayout;
|
||||||
import android.support.v4.content.LocalBroadcastManager;
|
import android.support.v4.util.Pair;
|
||||||
import android.support.v4.widget.SwipeRefreshLayout;
|
import android.support.v4.widget.SwipeRefreshLayout;
|
||||||
|
import android.support.v7.content.res.AppCompatResources;
|
||||||
import android.support.v7.widget.DividerItemDecoration;
|
import android.support.v7.widget.DividerItemDecoration;
|
||||||
import android.support.v7.widget.LinearLayoutManager;
|
import android.support.v7.widget.LinearLayoutManager;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
import android.support.v7.widget.SimpleItemAnimator;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ProgressBar;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
import com.keylesspalace.tusky.MainActivity;
|
import com.keylesspalace.tusky.MainActivity;
|
||||||
import com.keylesspalace.tusky.R;
|
import com.keylesspalace.tusky.R;
|
||||||
import com.keylesspalace.tusky.adapter.FooterViewHolder;
|
import com.keylesspalace.tusky.adapter.FooterViewHolder;
|
||||||
import com.keylesspalace.tusky.adapter.NotificationsAdapter;
|
import com.keylesspalace.tusky.adapter.NotificationsAdapter;
|
||||||
|
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.db.AccountEntity;
|
import com.keylesspalace.tusky.db.AccountEntity;
|
||||||
import com.keylesspalace.tusky.db.AccountManager;
|
import com.keylesspalace.tusky.db.AccountManager;
|
||||||
import com.keylesspalace.tusky.di.Injectable;
|
import com.keylesspalace.tusky.di.Injectable;
|
||||||
import com.keylesspalace.tusky.entity.Attachment;
|
|
||||||
import com.keylesspalace.tusky.entity.Notification;
|
import com.keylesspalace.tusky.entity.Notification;
|
||||||
import com.keylesspalace.tusky.entity.Status;
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
|
import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
|
||||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||||
import com.keylesspalace.tusky.network.TimelineCases;
|
import com.keylesspalace.tusky.network.TimelineCases;
|
||||||
import com.keylesspalace.tusky.receiver.TimelineReceiver;
|
|
||||||
import com.keylesspalace.tusky.util.CollectionUtil;
|
import com.keylesspalace.tusky.util.CollectionUtil;
|
||||||
import com.keylesspalace.tusky.util.Either;
|
import com.keylesspalace.tusky.util.Either;
|
||||||
import com.keylesspalace.tusky.util.HttpHeaderLink;
|
import com.keylesspalace.tusky.util.HttpHeaderLink;
|
||||||
|
@ -64,13 +71,18 @@ import com.keylesspalace.tusky.viewdata.StatusViewData;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
import retrofit2.Call;
|
import retrofit2.Call;
|
||||||
import retrofit2.Callback;
|
import retrofit2.Callback;
|
||||||
import retrofit2.Response;
|
import retrofit2.Response;
|
||||||
|
|
||||||
|
import static com.uber.autodispose.AutoDispose.autoDisposable;
|
||||||
|
import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from;
|
||||||
|
|
||||||
public class NotificationsFragment extends SFragment implements
|
public class NotificationsFragment extends SFragment implements
|
||||||
SwipeRefreshLayout.OnRefreshListener,
|
SwipeRefreshLayout.OnRefreshListener,
|
||||||
StatusActionListener,
|
StatusActionListener,
|
||||||
|
@ -106,15 +118,19 @@ public class NotificationsFragment extends SFragment implements
|
||||||
public TimelineCases timelineCases;
|
public TimelineCases timelineCases;
|
||||||
@Inject
|
@Inject
|
||||||
AccountManager accountManager;
|
AccountManager accountManager;
|
||||||
|
@Inject
|
||||||
|
EventHub eventHub;
|
||||||
|
|
||||||
private SwipeRefreshLayout swipeRefreshLayout;
|
private SwipeRefreshLayout swipeRefreshLayout;
|
||||||
private LinearLayoutManager layoutManager;
|
|
||||||
private RecyclerView recyclerView;
|
private RecyclerView recyclerView;
|
||||||
|
private ProgressBar progressBar;
|
||||||
|
private TextView nothingMessageView;
|
||||||
|
|
||||||
|
private LinearLayoutManager layoutManager;
|
||||||
private EndlessOnScrollListener scrollListener;
|
private EndlessOnScrollListener scrollListener;
|
||||||
private NotificationsAdapter adapter;
|
private NotificationsAdapter adapter;
|
||||||
private TabLayout.OnTabSelectedListener onTabSelectedListener;
|
private TabLayout.OnTabSelectedListener onTabSelectedListener;
|
||||||
private boolean hideFab;
|
private boolean hideFab;
|
||||||
private TimelineReceiver timelineReceiver;
|
|
||||||
private boolean topLoading;
|
private boolean topLoading;
|
||||||
private int topFetches;
|
private int topFetches;
|
||||||
private boolean bottomLoading;
|
private boolean bottomLoading;
|
||||||
|
@ -158,11 +174,14 @@ public class NotificationsFragment extends SFragment implements
|
||||||
@NonNull Context context = inflater.getContext(); // from inflater to silence warning
|
@NonNull Context context = inflater.getContext(); // from inflater to silence warning
|
||||||
// Setup the SwipeRefreshLayout.
|
// Setup the SwipeRefreshLayout.
|
||||||
swipeRefreshLayout = rootView.findViewById(R.id.swipe_refresh_layout);
|
swipeRefreshLayout = rootView.findViewById(R.id.swipe_refresh_layout);
|
||||||
|
recyclerView = rootView.findViewById(R.id.recycler_view);
|
||||||
|
progressBar = rootView.findViewById(R.id.progress_bar);
|
||||||
|
nothingMessageView = rootView.findViewById(R.id.nothing_message);
|
||||||
|
|
||||||
swipeRefreshLayout.setOnRefreshListener(this);
|
swipeRefreshLayout.setOnRefreshListener(this);
|
||||||
swipeRefreshLayout.setColorSchemeResources(R.color.primary);
|
swipeRefreshLayout.setColorSchemeResources(R.color.primary);
|
||||||
swipeRefreshLayout.setProgressBackgroundColorSchemeColor(ThemeUtils.getColor(context, android.R.attr.colorBackground));
|
swipeRefreshLayout.setProgressBackgroundColorSchemeColor(ThemeUtils.getColor(context, android.R.attr.colorBackground));
|
||||||
// Setup the RecyclerView.
|
// Setup the RecyclerView.
|
||||||
recyclerView = rootView.findViewById(R.id.recycler_view);
|
|
||||||
recyclerView.setHasFixedSize(true);
|
recyclerView.setHasFixedSize(true);
|
||||||
layoutManager = new LinearLayoutManager(context);
|
layoutManager = new LinearLayoutManager(context);
|
||||||
recyclerView.setLayoutManager(layoutManager);
|
recyclerView.setLayoutManager(layoutManager);
|
||||||
|
@ -181,10 +200,6 @@ public class NotificationsFragment extends SFragment implements
|
||||||
adapter.setMediaPreviewEnabled(mediaPreviewEnabled);
|
adapter.setMediaPreviewEnabled(mediaPreviewEnabled);
|
||||||
recyclerView.setAdapter(adapter);
|
recyclerView.setAdapter(adapter);
|
||||||
|
|
||||||
timelineReceiver = new TimelineReceiver(this);
|
|
||||||
LocalBroadcastManager.getInstance(context.getApplicationContext())
|
|
||||||
.registerReceiver(timelineReceiver, TimelineReceiver.getFilter(null));
|
|
||||||
|
|
||||||
notifications.clear();
|
notifications.clear();
|
||||||
topLoading = false;
|
topLoading = false;
|
||||||
topFetches = 0;
|
topFetches = 0;
|
||||||
|
@ -193,9 +208,58 @@ public class NotificationsFragment extends SFragment implements
|
||||||
bottomId = null;
|
bottomId = null;
|
||||||
topId = null;
|
topId = null;
|
||||||
|
|
||||||
|
((SimpleItemAnimator) recyclerView.getItemAnimator()).setSupportsChangeAnimations(false);
|
||||||
|
setupNothingView();
|
||||||
|
|
||||||
return rootView;
|
return rootView;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupNothingView() {
|
||||||
|
Drawable top = AppCompatResources.getDrawable(Objects.requireNonNull(getContext()),
|
||||||
|
R.drawable.elephant_friend);
|
||||||
|
if (top != null) {
|
||||||
|
top.setBounds(0, 0, top.getIntrinsicWidth() / 2, top.getIntrinsicHeight() / 2);
|
||||||
|
}
|
||||||
|
nothingMessageView.setCompoundDrawables(null, top, null, null);
|
||||||
|
nothingMessageView.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleFavEvent(FavoriteEvent event) {
|
||||||
|
Pair<Integer, Notification> posAndNotification =
|
||||||
|
findReplyPosition(event.getStatusId());
|
||||||
|
if (posAndNotification == null) return;
|
||||||
|
//noinspection ConstantConditions
|
||||||
|
setFavovouriteForStatus(posAndNotification.first,
|
||||||
|
posAndNotification.second.getStatus(),
|
||||||
|
event.getFavourite());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleReblogEvent(ReblogEvent event) {
|
||||||
|
Pair<Integer, Notification> posAndNotification = findReplyPosition(event.getStatusId());
|
||||||
|
if (posAndNotification == null) return;
|
||||||
|
//noinspection ConstantConditions
|
||||||
|
setReblogForStatus(posAndNotification.first,
|
||||||
|
posAndNotification.second.getStatus(),
|
||||||
|
event.getReblog());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
||||||
super.onActivityCreated(savedInstanceState);
|
super.onActivityCreated(savedInstanceState);
|
||||||
|
@ -250,7 +314,7 @@ public class NotificationsFragment extends SFragment implements
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoadMore(int page, int totalItemsCount, RecyclerView view) {
|
public void onLoadMore(int totalItemsCount, RecyclerView view) {
|
||||||
NotificationsFragment.this.onLoadMore();
|
NotificationsFragment.this.onLoadMore();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -266,9 +330,6 @@ public class NotificationsFragment extends SFragment implements
|
||||||
} else {
|
} else {
|
||||||
TabLayout tabLayout = activity.findViewById(R.id.tab_layout);
|
TabLayout tabLayout = activity.findViewById(R.id.tab_layout);
|
||||||
tabLayout.removeOnTabSelectedListener(onTabSelectedListener);
|
tabLayout.removeOnTabSelectedListener(onTabSelectedListener);
|
||||||
|
|
||||||
LocalBroadcastManager.getInstance(activity)
|
|
||||||
.unregisterReceiver(timelineReceiver);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
super.onDestroyView();
|
super.onDestroyView();
|
||||||
|
@ -292,24 +353,7 @@ public class NotificationsFragment extends SFragment implements
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(@NonNull Call<Status> call, @NonNull retrofit2.Response<Status> response) {
|
public void onResponse(@NonNull Call<Status> call, @NonNull retrofit2.Response<Status> response) {
|
||||||
if (response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
status.setReblogged(reblog);
|
setReblogForStatus(position, status, reblog);
|
||||||
|
|
||||||
if (status.getReblog() != null) {
|
|
||||||
status.getReblog().setReblogged(reblog);
|
|
||||||
}
|
|
||||||
|
|
||||||
NotificationViewData.Concrete viewdata = (NotificationViewData.Concrete) notifications.getPairedItem(position);
|
|
||||||
|
|
||||||
StatusViewData.Builder viewDataBuilder = new StatusViewData.Builder(viewdata.getStatusViewData());
|
|
||||||
viewDataBuilder.setReblogged(reblog);
|
|
||||||
|
|
||||||
NotificationViewData.Concrete newViewData = new NotificationViewData.Concrete(
|
|
||||||
viewdata.getType(), viewdata.getId(), viewdata.getAccount(),
|
|
||||||
viewDataBuilder.createStatusViewData(), viewdata.isExpanded());
|
|
||||||
|
|
||||||
notifications.setPairedItem(position, newViewData);
|
|
||||||
|
|
||||||
adapter.updateItemWithNotify(position, newViewData, false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -320,6 +364,27 @@ public class NotificationsFragment extends SFragment implements
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setReblogForStatus(int position, Status status, boolean reblog) {
|
||||||
|
status.setReblogged(reblog);
|
||||||
|
|
||||||
|
if (status.getReblog() != null) {
|
||||||
|
status.getReblog().setReblogged(reblog);
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationViewData.Concrete viewdata = (NotificationViewData.Concrete) notifications.getPairedItem(position);
|
||||||
|
|
||||||
|
StatusViewData.Builder viewDataBuilder = new StatusViewData.Builder(viewdata.getStatusViewData());
|
||||||
|
viewDataBuilder.setReblogged(reblog);
|
||||||
|
|
||||||
|
NotificationViewData.Concrete newViewData = new NotificationViewData.Concrete(
|
||||||
|
viewdata.getType(), viewdata.getId(), viewdata.getAccount(),
|
||||||
|
viewDataBuilder.createStatusViewData(), viewdata.isExpanded());
|
||||||
|
|
||||||
|
notifications.setPairedItem(position, newViewData);
|
||||||
|
|
||||||
|
adapter.updateItemWithNotify(position, newViewData, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFavourite(final boolean favourite, final int position) {
|
public void onFavourite(final boolean favourite, final int position) {
|
||||||
|
@ -329,24 +394,7 @@ public class NotificationsFragment extends SFragment implements
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(@NonNull Call<Status> call, @NonNull retrofit2.Response<Status> response) {
|
public void onResponse(@NonNull Call<Status> call, @NonNull retrofit2.Response<Status> response) {
|
||||||
if (response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
status.setFavourited(favourite);
|
setFavovouriteForStatus(position, status, favourite);
|
||||||
|
|
||||||
if (status.getReblog() != null) {
|
|
||||||
status.getReblog().setFavourited(favourite);
|
|
||||||
}
|
|
||||||
|
|
||||||
NotificationViewData.Concrete viewdata = (NotificationViewData.Concrete) notifications.getPairedItem(position);
|
|
||||||
|
|
||||||
StatusViewData.Builder viewDataBuilder = new StatusViewData.Builder(viewdata.getStatusViewData());
|
|
||||||
viewDataBuilder.setFavourited(favourite);
|
|
||||||
|
|
||||||
NotificationViewData.Concrete newViewData = new NotificationViewData.Concrete(
|
|
||||||
viewdata.getType(), viewdata.getId(), viewdata.getAccount(),
|
|
||||||
viewDataBuilder.createStatusViewData(), viewdata.isExpanded());
|
|
||||||
|
|
||||||
notifications.setPairedItem(position, newViewData);
|
|
||||||
|
|
||||||
adapter.updateItemWithNotify(position, newViewData, false);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -358,6 +406,27 @@ public class NotificationsFragment extends SFragment implements
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setFavovouriteForStatus(int position, Status status, boolean favourite) {
|
||||||
|
status.setFavourited(favourite);
|
||||||
|
|
||||||
|
if (status.getReblog() != null) {
|
||||||
|
status.getReblog().setFavourited(favourite);
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationViewData.Concrete viewdata = (NotificationViewData.Concrete) notifications.getPairedItem(position);
|
||||||
|
|
||||||
|
StatusViewData.Builder viewDataBuilder = new StatusViewData.Builder(viewdata.getStatusViewData());
|
||||||
|
viewDataBuilder.setFavourited(favourite);
|
||||||
|
|
||||||
|
NotificationViewData.Concrete newViewData = new NotificationViewData.Concrete(
|
||||||
|
viewdata.getType(), viewdata.getId(), viewdata.getAccount(),
|
||||||
|
viewDataBuilder.createStatusViewData(), viewdata.isExpanded());
|
||||||
|
|
||||||
|
notifications.setPairedItem(position, newViewData);
|
||||||
|
|
||||||
|
adapter.updateItemWithNotify(position, newViewData, true);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onMore(View view, int position) {
|
public void onMore(View view, int position) {
|
||||||
Notification notification = notifications.get(position).getAsRight();
|
Notification notification = notifications.get(position).getAsRight();
|
||||||
|
@ -475,8 +544,7 @@ public class NotificationsFragment extends SFragment implements
|
||||||
adapter.update(notifications.getPairedCopy());
|
adapter.update(notifications.getPairedCopy());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private void removeAllByAccountId(String accountId) {
|
||||||
public void removeAllByAccountId(String accountId) {
|
|
||||||
// using iterator to safely remove items while iterating
|
// using iterator to safely remove items while iterating
|
||||||
Iterator<Either<Placeholder, Notification>> iterator = notifications.iterator();
|
Iterator<Either<Placeholder, Notification>> iterator = notifications.iterator();
|
||||||
while (iterator.hasNext()) {
|
while (iterator.hasNext()) {
|
||||||
|
@ -590,6 +658,7 @@ public class NotificationsFragment extends SFragment implements
|
||||||
adapter.setFooterState(FooterViewHolder.State.END);
|
adapter.setFooterState(FooterViewHolder.State.END);
|
||||||
}
|
}
|
||||||
swipeRefreshLayout.setRefreshing(false);
|
swipeRefreshLayout.setRefreshing(false);
|
||||||
|
progressBar.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onFetchNotificationsFailure(Exception exception, FetchEnd fetchEnd, int position) {
|
private void onFetchNotificationsFailure(Exception exception, FetchEnd fetchEnd, int position) {
|
||||||
|
@ -602,6 +671,7 @@ public class NotificationsFragment extends SFragment implements
|
||||||
}
|
}
|
||||||
Log.e(TAG, "Fetch failure: " + exception.getMessage());
|
Log.e(TAG, "Fetch failure: " + exception.getMessage());
|
||||||
fulfillAnyQueuedFetches(fetchEnd);
|
fulfillAnyQueuedFetches(fetchEnd);
|
||||||
|
progressBar.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void saveNewestNotificationId(List<Notification> notifications) {
|
private void saveNewestNotificationId(List<Notification> notifications) {
|
||||||
|
@ -623,7 +693,6 @@ public class NotificationsFragment extends SFragment implements
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isBiggerThan(BigInteger newId, BigInteger lastShownNotificationId) {
|
private boolean isBiggerThan(BigInteger newId, BigInteger lastShownNotificationId) {
|
||||||
|
|
||||||
return lastShownNotificationId.compareTo(newId) < 0;
|
return lastShownNotificationId.compareTo(newId) < 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -736,4 +805,20 @@ public class NotificationsFragment extends SFragment implements
|
||||||
notifications.clear();
|
notifications.clear();
|
||||||
sendFetchNotificationsRequest(null, null, FetchEnd.TOP, -1);
|
sendFetchNotificationsRequest(null, null, FetchEnd.TOP, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private Pair<Integer, Notification> findReplyPosition(@NonNull String statusId) {
|
||||||
|
for (int i = 0; i < notifications.size(); i++) {
|
||||||
|
Notification notification = notifications.get(i).getAsRightOrNull();
|
||||||
|
if (notification != null
|
||||||
|
&& notification.getStatus() != null
|
||||||
|
&& notification.getType() == Notification.Type.MENTION
|
||||||
|
&& (statusId.equals(notification.getStatus().getId())
|
||||||
|
|| (notification.getStatus().getReblog() != null
|
||||||
|
&& statusId.equals(notification.getStatus().getReblog().getId())))) {
|
||||||
|
return new Pair<>(i, notification);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.design.widget.BottomSheetBehavior;
|
||||||
import android.support.v4.app.ActivityOptionsCompat;
|
import android.support.v4.app.ActivityOptionsCompat;
|
||||||
import android.support.v4.view.ViewCompat;
|
import android.support.v4.view.ViewCompat;
|
||||||
import android.support.v7.widget.PopupMenu;
|
import android.support.v7.widget.PopupMenu;
|
||||||
|
@ -39,7 +40,6 @@ import com.keylesspalace.tusky.db.AccountEntity;
|
||||||
import com.keylesspalace.tusky.db.AccountManager;
|
import com.keylesspalace.tusky.db.AccountManager;
|
||||||
import com.keylesspalace.tusky.entity.Attachment;
|
import com.keylesspalace.tusky.entity.Attachment;
|
||||||
import com.keylesspalace.tusky.entity.Status;
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
import com.keylesspalace.tusky.interfaces.AdapterItemRemover;
|
|
||||||
import com.keylesspalace.tusky.network.MastodonApi;
|
import com.keylesspalace.tusky.network.MastodonApi;
|
||||||
import com.keylesspalace.tusky.network.TimelineCases;
|
import com.keylesspalace.tusky.network.TimelineCases;
|
||||||
import com.keylesspalace.tusky.util.HtmlUtils;
|
import com.keylesspalace.tusky.util.HtmlUtils;
|
||||||
|
@ -57,18 +57,19 @@ import javax.inject.Inject;
|
||||||
* adapters. I feel like the profile pages and thread viewer, which I haven't made yet, will also
|
* 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
|
* overlap functionality. So, I'm momentarily leaving it and hopefully working on those will clear
|
||||||
* up what needs to be where. */
|
* up what needs to be where. */
|
||||||
public abstract class SFragment extends BaseFragment implements AdapterItemRemover {
|
public abstract class SFragment extends BaseFragment {
|
||||||
protected static final int COMPOSE_RESULT = 1;
|
protected static final int COMPOSE_RESULT = 1;
|
||||||
|
|
||||||
protected String loggedInAccountId;
|
protected String loggedInAccountId;
|
||||||
protected String loggedInUsername;
|
protected String loggedInUsername;
|
||||||
|
|
||||||
protected abstract TimelineCases timelineCases();
|
protected abstract TimelineCases timelineCases();
|
||||||
|
protected abstract void removeItem(int position);
|
||||||
|
|
||||||
private BottomSheetActivity bottomSheetActivity;
|
private BottomSheetActivity bottomSheetActivity;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
protected MastodonApi mastodonApi;
|
public MastodonApi mastodonApi;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
||||||
|
|
|
@ -126,10 +126,6 @@ class SearchFragment : SFragment(), StatusActionListener, Injectable {
|
||||||
searchAdapter.removeStatusAtPosition(position)
|
searchAdapter.removeStatusAtPosition(position)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun removeAllByAccountId(accountId: String?) {
|
|
||||||
// not supported
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onReply(position: Int) {
|
override fun onReply(position: Int) {
|
||||||
val status = searchAdapter.getStatusAtPosition(position)
|
val status = searchAdapter.getStatusAtPosition(position)
|
||||||
if(status != null) {
|
if(status != null) {
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
package com.keylesspalace.tusky.fragment;
|
package com.keylesspalace.tusky.fragment;
|
||||||
|
|
||||||
import android.arch.core.util.Function;
|
import android.arch.core.util.Function;
|
||||||
|
import android.arch.lifecycle.Lifecycle;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
|
@ -25,29 +26,40 @@ import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.design.widget.FloatingActionButton;
|
import android.support.design.widget.FloatingActionButton;
|
||||||
import android.support.design.widget.TabLayout;
|
import android.support.design.widget.TabLayout;
|
||||||
import android.support.v4.content.LocalBroadcastManager;
|
|
||||||
import android.support.v4.util.Pair;
|
import android.support.v4.util.Pair;
|
||||||
import android.support.v4.widget.SwipeRefreshLayout;
|
import android.support.v4.widget.SwipeRefreshLayout;
|
||||||
|
import android.support.v7.content.res.AppCompatResources;
|
||||||
|
import android.support.v7.recyclerview.extensions.AsyncDifferConfig;
|
||||||
|
import android.support.v7.recyclerview.extensions.AsyncListDiffer;
|
||||||
|
import android.support.v7.util.DiffUtil;
|
||||||
|
import android.support.v7.util.ListUpdateCallback;
|
||||||
import android.support.v7.widget.DividerItemDecoration;
|
import android.support.v7.widget.DividerItemDecoration;
|
||||||
import android.support.v7.widget.LinearLayoutManager;
|
import android.support.v7.widget.LinearLayoutManager;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
import android.support.v7.widget.SimpleItemAnimator;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ProgressBar;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
import com.keylesspalace.tusky.BuildConfig;
|
|
||||||
import com.keylesspalace.tusky.R;
|
import com.keylesspalace.tusky.R;
|
||||||
import com.keylesspalace.tusky.adapter.FooterViewHolder;
|
|
||||||
import com.keylesspalace.tusky.adapter.TimelineAdapter;
|
import com.keylesspalace.tusky.adapter.TimelineAdapter;
|
||||||
|
import com.keylesspalace.tusky.appstore.BlockEvent;
|
||||||
|
import com.keylesspalace.tusky.appstore.EventHub;
|
||||||
|
import com.keylesspalace.tusky.appstore.FavoriteEvent;
|
||||||
|
import com.keylesspalace.tusky.appstore.MuteEvent;
|
||||||
|
import com.keylesspalace.tusky.appstore.ReblogEvent;
|
||||||
|
import com.keylesspalace.tusky.appstore.StatusComposedEvent;
|
||||||
|
import com.keylesspalace.tusky.appstore.StatusDeletedEvent;
|
||||||
|
import com.keylesspalace.tusky.appstore.UnfollowEvent;
|
||||||
import com.keylesspalace.tusky.di.Injectable;
|
import com.keylesspalace.tusky.di.Injectable;
|
||||||
import com.keylesspalace.tusky.entity.Attachment;
|
|
||||||
import com.keylesspalace.tusky.entity.Status;
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
|
import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
|
||||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||||
import com.keylesspalace.tusky.network.MastodonApi;
|
import com.keylesspalace.tusky.network.MastodonApi;
|
||||||
import com.keylesspalace.tusky.network.TimelineCases;
|
import com.keylesspalace.tusky.network.TimelineCases;
|
||||||
import com.keylesspalace.tusky.receiver.TimelineReceiver;
|
|
||||||
import com.keylesspalace.tusky.util.CollectionUtil;
|
import com.keylesspalace.tusky.util.CollectionUtil;
|
||||||
import com.keylesspalace.tusky.util.Either;
|
import com.keylesspalace.tusky.util.Either;
|
||||||
import com.keylesspalace.tusky.util.HttpHeaderLink;
|
import com.keylesspalace.tusky.util.HttpHeaderLink;
|
||||||
|
@ -60,16 +72,20 @@ import com.keylesspalace.tusky.viewdata.StatusViewData;
|
||||||
|
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Objects;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
import retrofit2.Call;
|
import retrofit2.Call;
|
||||||
import retrofit2.Callback;
|
import retrofit2.Callback;
|
||||||
import retrofit2.Response;
|
import retrofit2.Response;
|
||||||
|
|
||||||
|
import static com.uber.autodispose.AutoDispose.autoDisposable;
|
||||||
|
import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from;
|
||||||
|
|
||||||
public class TimelineFragment extends SFragment implements
|
public class TimelineFragment extends SFragment implements
|
||||||
SwipeRefreshLayout.OnRefreshListener,
|
SwipeRefreshLayout.OnRefreshListener,
|
||||||
StatusActionListener,
|
StatusActionListener,
|
||||||
|
@ -98,13 +114,18 @@ public class TimelineFragment extends SFragment implements
|
||||||
}
|
}
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
TimelineCases timelineCases;
|
public TimelineCases timelineCases;
|
||||||
|
@Inject
|
||||||
|
public EventHub eventHub;
|
||||||
|
|
||||||
private SwipeRefreshLayout swipeRefreshLayout;
|
private SwipeRefreshLayout swipeRefreshLayout;
|
||||||
|
private RecyclerView recyclerView;
|
||||||
|
private ProgressBar progressBar;
|
||||||
|
private TextView nothingMessageView;
|
||||||
|
|
||||||
private TimelineAdapter adapter;
|
private TimelineAdapter adapter;
|
||||||
private Kind kind;
|
private Kind kind;
|
||||||
private String hashtagOrId;
|
private String hashtagOrId;
|
||||||
private RecyclerView recyclerView;
|
|
||||||
private LinearLayoutManager layoutManager;
|
private LinearLayoutManager layoutManager;
|
||||||
private EndlessOnScrollListener scrollListener;
|
private EndlessOnScrollListener scrollListener;
|
||||||
private TabLayout.OnTabSelectedListener onTabSelectedListener;
|
private TabLayout.OnTabSelectedListener onTabSelectedListener;
|
||||||
|
@ -113,15 +134,16 @@ public class TimelineFragment extends SFragment implements
|
||||||
private boolean filterRemoveRegex;
|
private boolean filterRemoveRegex;
|
||||||
private Matcher filterRemoveRegexMatcher;
|
private Matcher filterRemoveRegexMatcher;
|
||||||
private boolean hideFab;
|
private boolean hideFab;
|
||||||
private TimelineReceiver timelineReceiver;
|
|
||||||
private boolean topLoading;
|
private boolean topLoading;
|
||||||
private int topFetches;
|
private int topFetches;
|
||||||
private boolean bottomLoading;
|
private boolean bottomLoading;
|
||||||
private int bottomFetches;
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private String bottomId;
|
private String bottomId;
|
||||||
@Nullable
|
@Nullable
|
||||||
private String topId;
|
private String topId;
|
||||||
|
private long maxPlaceholderId = -1;
|
||||||
|
private boolean didLoadEverythingBottom;
|
||||||
|
|
||||||
private boolean alwaysShowSensitiveMedia;
|
private boolean alwaysShowSensitiveMedia;
|
||||||
|
|
||||||
|
@ -138,7 +160,8 @@ public class TimelineFragment extends SFragment implements
|
||||||
if (status != null) {
|
if (status != null) {
|
||||||
return ViewDataUtils.statusToViewData(status, alwaysShowSensitiveMedia);
|
return ViewDataUtils.statusToViewData(status, alwaysShowSensitiveMedia);
|
||||||
} else {
|
} else {
|
||||||
return new StatusViewData.Placeholder(false);
|
Placeholder placeholder = input.getAsLeft();
|
||||||
|
return new StatusViewData.Placeholder(placeholder.id, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -161,20 +184,21 @@ public class TimelineFragment extends SFragment implements
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class Placeholder {
|
private static final class Placeholder {
|
||||||
private final static Placeholder INSTANCE = new Placeholder();
|
final long id;
|
||||||
|
|
||||||
public static Placeholder getInstance() {
|
public static Placeholder getInstance(long id) {
|
||||||
return INSTANCE;
|
return new Placeholder(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Placeholder() {
|
private Placeholder(long id) {
|
||||||
|
this.id = id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||||
Bundle savedInstanceState) {
|
Bundle savedInstanceState) {
|
||||||
Bundle arguments = getArguments();
|
Bundle arguments = Objects.requireNonNull(getArguments());
|
||||||
kind = Kind.valueOf(arguments.getString(KIND_ARG));
|
kind = Kind.valueOf(arguments.getString(KIND_ARG));
|
||||||
if (kind == Kind.TAG || kind == Kind.USER || kind == Kind.LIST) {
|
if (kind == Kind.TAG || kind == Kind.USER || kind == Kind.LIST) {
|
||||||
hashtagOrId = arguments.getString(HASHTAG_OR_ID_ARG);
|
hashtagOrId = arguments.getString(HASHTAG_OR_ID_ARG);
|
||||||
|
@ -182,14 +206,73 @@ public class TimelineFragment extends SFragment implements
|
||||||
|
|
||||||
final View rootView = inflater.inflate(R.layout.fragment_timeline, container, false);
|
final View rootView = inflater.inflate(R.layout.fragment_timeline, container, false);
|
||||||
|
|
||||||
// Setup the SwipeRefreshLayout.
|
recyclerView = rootView.findViewById(R.id.recycler_view);
|
||||||
Context context = getContext();
|
|
||||||
swipeRefreshLayout = rootView.findViewById(R.id.swipe_refresh_layout);
|
swipeRefreshLayout = rootView.findViewById(R.id.swipe_refresh_layout);
|
||||||
|
progressBar = rootView.findViewById(R.id.progress_bar);
|
||||||
|
nothingMessageView = rootView.findViewById(R.id.nothing_message);
|
||||||
|
|
||||||
|
adapter = new TimelineAdapter(dataSource, this);
|
||||||
|
|
||||||
|
|
||||||
|
setupSwipeRefreshLayout();
|
||||||
|
setupRecyclerView();
|
||||||
|
updateAdapter();
|
||||||
|
setupTimelinePreferences();
|
||||||
|
setupNothingView();
|
||||||
|
|
||||||
|
topLoading = false;
|
||||||
|
topFetches = 0;
|
||||||
|
bottomId = null;
|
||||||
|
topId = null;
|
||||||
|
|
||||||
|
|
||||||
|
if (statuses.isEmpty()) {
|
||||||
|
progressBar.setVisibility(View.VISIBLE);
|
||||||
|
bottomLoading = true;
|
||||||
|
sendFetchTimelineRequest(null, null, FetchEnd.BOTTOM, -1);
|
||||||
|
} else {
|
||||||
|
progressBar.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rootView;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupTimelinePreferences() {
|
||||||
|
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(
|
||||||
|
getActivity());
|
||||||
|
preferences.registerOnSharedPreferenceChangeListener(this);
|
||||||
|
alwaysShowSensitiveMedia = preferences.getBoolean("alwaysShowSensitiveMedia", false);
|
||||||
|
boolean mediaPreviewEnabled = preferences.getBoolean("mediaPreviewEnabled", true);
|
||||||
|
adapter.setMediaPreviewEnabled(mediaPreviewEnabled);
|
||||||
|
|
||||||
|
boolean filter = preferences.getBoolean("tabFilterHomeReplies", true);
|
||||||
|
filterRemoveReplies = kind == Kind.HOME && !filter;
|
||||||
|
|
||||||
|
filter = preferences.getBoolean("tabFilterHomeBoosts", true);
|
||||||
|
filterRemoveReblogs = kind == Kind.HOME && !filter;
|
||||||
|
|
||||||
|
String regexFilter = preferences.getString("tabFilterRegex", "");
|
||||||
|
filterRemoveRegex = (kind == Kind.HOME
|
||||||
|
|| kind == Kind.PUBLIC_LOCAL
|
||||||
|
|| kind == Kind.PUBLIC_FEDERATED)
|
||||||
|
&& !regexFilter.isEmpty();
|
||||||
|
|
||||||
|
if (filterRemoveRegex) {
|
||||||
|
filterRemoveRegexMatcher = Pattern.compile(regexFilter, Pattern.CASE_INSENSITIVE)
|
||||||
|
.matcher("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupSwipeRefreshLayout() {
|
||||||
|
Context context = Objects.requireNonNull(getContext());
|
||||||
swipeRefreshLayout.setOnRefreshListener(this);
|
swipeRefreshLayout.setOnRefreshListener(this);
|
||||||
swipeRefreshLayout.setColorSchemeResources(R.color.primary);
|
swipeRefreshLayout.setColorSchemeResources(R.color.primary);
|
||||||
swipeRefreshLayout.setProgressBackgroundColorSchemeColor(ThemeUtils.getColor(context, android.R.attr.colorBackground));
|
swipeRefreshLayout.setProgressBackgroundColorSchemeColor(ThemeUtils.getColor(context,
|
||||||
// Setup the RecyclerView.
|
android.R.attr.colorBackground));
|
||||||
recyclerView = rootView.findViewById(R.id.recycler_view);
|
}
|
||||||
|
|
||||||
|
private void setupRecyclerView() {
|
||||||
|
Context context = Objects.requireNonNull(getContext());
|
||||||
recyclerView.setHasFixedSize(true);
|
recyclerView.setHasFixedSize(true);
|
||||||
layoutManager = new LinearLayoutManager(context);
|
layoutManager = new LinearLayoutManager(context);
|
||||||
recyclerView.setLayoutManager(layoutManager);
|
recyclerView.setLayoutManager(layoutManager);
|
||||||
|
@ -199,38 +282,58 @@ public class TimelineFragment extends SFragment implements
|
||||||
R.drawable.status_divider_dark);
|
R.drawable.status_divider_dark);
|
||||||
divider.setDrawable(drawable);
|
divider.setDrawable(drawable);
|
||||||
recyclerView.addItemDecoration(divider);
|
recyclerView.addItemDecoration(divider);
|
||||||
adapter = new TimelineAdapter(this);
|
|
||||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(
|
// CWs are expanded without animation, buttons animate itself, we don't need it basically
|
||||||
getActivity());
|
((SimpleItemAnimator) recyclerView.getItemAnimator()).setSupportsChangeAnimations(false);
|
||||||
preferences.registerOnSharedPreferenceChangeListener(this);
|
|
||||||
alwaysShowSensitiveMedia = preferences.getBoolean("alwaysShowSensitiveMedia", false);
|
|
||||||
boolean mediaPreviewEnabled = preferences.getBoolean("mediaPreviewEnabled", true);
|
|
||||||
adapter.setMediaPreviewEnabled(mediaPreviewEnabled);
|
|
||||||
recyclerView.setAdapter(adapter);
|
recyclerView.setAdapter(adapter);
|
||||||
|
}
|
||||||
|
|
||||||
boolean filter = preferences.getBoolean("tabFilterHomeReplies", true);
|
@Override
|
||||||
filterRemoveReplies = kind == Kind.HOME && !filter;
|
public void onPostCreate() {
|
||||||
|
super.onPostCreate();
|
||||||
|
|
||||||
filter = preferences.getBoolean("tabFilterHomeBoosts", true);
|
eventHub.getEvents()
|
||||||
filterRemoveReblogs = kind == Kind.HOME && !filter;
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
|
||||||
|
.subscribe(event -> {
|
||||||
|
if (event instanceof FavoriteEvent) {
|
||||||
|
FavoriteEvent favEvent = ((FavoriteEvent) event);
|
||||||
|
handleFavEvent(favEvent);
|
||||||
|
} else if (event instanceof ReblogEvent) {
|
||||||
|
ReblogEvent reblogEvent = (ReblogEvent) event;
|
||||||
|
handleReblogEvent(reblogEvent);
|
||||||
|
} else if (event instanceof UnfollowEvent) {
|
||||||
|
if (kind == Kind.HOME) {
|
||||||
|
String id = ((UnfollowEvent) event).getAccountId();
|
||||||
|
removeAllByAccountId(id);
|
||||||
|
}
|
||||||
|
} else if (event instanceof BlockEvent) {
|
||||||
|
String id = ((BlockEvent) event).getAccountId();
|
||||||
|
removeAllByAccountId(id);
|
||||||
|
} else if (event instanceof MuteEvent) {
|
||||||
|
String id = ((MuteEvent) event).getAccountId();
|
||||||
|
removeAllByAccountId(id);
|
||||||
|
} else if (event instanceof StatusDeletedEvent) {
|
||||||
|
String id = ((StatusDeletedEvent) event).getStatusId();
|
||||||
|
deleteStatusById(id);
|
||||||
|
} else if (event instanceof StatusComposedEvent) {
|
||||||
|
Status status = ((StatusComposedEvent) event).getStatus();
|
||||||
|
handleStatusComposeEvent(status);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
String regexFilter = preferences.getString("tabFilterRegex", "");
|
private void deleteStatusById(String id) {
|
||||||
filterRemoveRegex = (kind == Kind.HOME || kind == Kind.PUBLIC_LOCAL || kind == Kind.PUBLIC_FEDERATED) && !regexFilter.isEmpty();
|
for (int i = 0; i < statuses.size(); i++) {
|
||||||
if (filterRemoveRegex) filterRemoveRegexMatcher = Pattern.compile(regexFilter, Pattern.CASE_INSENSITIVE).matcher("");
|
Either<Placeholder, Status> either = statuses.get(i);
|
||||||
|
if (either.isRight()
|
||||||
timelineReceiver = new TimelineReceiver(this, this);
|
&& id.equals(either.getAsRight().getId())) {
|
||||||
LocalBroadcastManager.getInstance(context.getApplicationContext())
|
statuses.remove(either);
|
||||||
.registerReceiver(timelineReceiver, TimelineReceiver.getFilter(kind));
|
updateAdapter();
|
||||||
|
break;
|
||||||
statuses.clear();
|
}
|
||||||
topLoading = false;
|
}
|
||||||
topFetches = 0;
|
|
||||||
bottomLoading = false;
|
|
||||||
bottomFetches = 0;
|
|
||||||
bottomId = null;
|
|
||||||
topId = null;
|
|
||||||
|
|
||||||
return rootView;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -238,7 +341,7 @@ public class TimelineFragment extends SFragment implements
|
||||||
super.onActivityCreated(savedInstanceState);
|
super.onActivityCreated(savedInstanceState);
|
||||||
|
|
||||||
if (jumpToTopAllowed()) {
|
if (jumpToTopAllowed()) {
|
||||||
TabLayout layout = getActivity().findViewById(R.id.tab_layout);
|
TabLayout layout = Objects.requireNonNull(getActivity()).findViewById(R.id.tab_layout);
|
||||||
if (layout != null) {
|
if (layout != null) {
|
||||||
onTabSelectedListener = new TabLayout.OnTabSelectedListener() {
|
onTabSelectedListener = new TabLayout.OnTabSelectedListener() {
|
||||||
@Override
|
@Override
|
||||||
|
@ -287,7 +390,7 @@ public class TimelineFragment extends SFragment implements
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoadMore(int page, int totalItemsCount, RecyclerView view) {
|
public void onLoadMore(int totalItemsCount, RecyclerView view) {
|
||||||
TimelineFragment.this.onLoadMore();
|
TimelineFragment.this.onLoadMore();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -295,7 +398,7 @@ public class TimelineFragment extends SFragment implements
|
||||||
// Just use the basic scroll listener to load more statuses.
|
// Just use the basic scroll listener to load more statuses.
|
||||||
scrollListener = new EndlessOnScrollListener(layoutManager) {
|
scrollListener = new EndlessOnScrollListener(layoutManager) {
|
||||||
@Override
|
@Override
|
||||||
public void onLoadMore(int page, int totalItemsCount, RecyclerView view) {
|
public void onLoadMore(int totalItemsCount, RecyclerView view) {
|
||||||
TimelineFragment.this.onLoadMore();
|
TimelineFragment.this.onLoadMore();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -306,15 +409,25 @@ public class TimelineFragment extends SFragment implements
|
||||||
@Override
|
@Override
|
||||||
public void onDestroyView() {
|
public void onDestroyView() {
|
||||||
if (jumpToTopAllowed()) {
|
if (jumpToTopAllowed()) {
|
||||||
TabLayout tabLayout = getActivity().findViewById(R.id.tab_layout);
|
TabLayout tabLayout = Objects.requireNonNull(getActivity())
|
||||||
|
.findViewById(R.id.tab_layout);
|
||||||
if (tabLayout != null) {
|
if (tabLayout != null) {
|
||||||
tabLayout.removeOnTabSelectedListener(onTabSelectedListener);
|
tabLayout.removeOnTabSelectedListener(onTabSelectedListener);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(timelineReceiver);
|
|
||||||
super.onDestroyView();
|
super.onDestroyView();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setupNothingView() {
|
||||||
|
Drawable top = AppCompatResources.getDrawable(Objects.requireNonNull(getContext()),
|
||||||
|
R.drawable.elephant_friend);
|
||||||
|
if (top != null) {
|
||||||
|
top.setBounds(0, 0, top.getIntrinsicWidth() / 2, top.getIntrinsicHeight() / 2);
|
||||||
|
}
|
||||||
|
nothingMessageView.setCompoundDrawables(null, top, null, null);
|
||||||
|
nothingMessageView.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRefresh() {
|
public void onRefresh() {
|
||||||
sendFetchTimelineRequest(null, topId, FetchEnd.TOP, -1);
|
sendFetchTimelineRequest(null, topId, FetchEnd.TOP, -1);
|
||||||
|
@ -333,22 +446,7 @@ public class TimelineFragment extends SFragment implements
|
||||||
public void onResponse(@NonNull Call<Status> call, @NonNull Response<Status> response) {
|
public void onResponse(@NonNull Call<Status> call, @NonNull Response<Status> response) {
|
||||||
|
|
||||||
if (response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
status.setReblogged(reblog);
|
setRebloggedForStatus(position, status, reblog);
|
||||||
|
|
||||||
if (status.getReblog() != null) {
|
|
||||||
status.getReblog().setReblogged(reblog);
|
|
||||||
}
|
|
||||||
|
|
||||||
Pair<StatusViewData.Concrete, Integer> actual =
|
|
||||||
findStatusAndPosition(position, status);
|
|
||||||
if (actual == null) return;
|
|
||||||
|
|
||||||
StatusViewData newViewData =
|
|
||||||
new StatusViewData.Builder(actual.first)
|
|
||||||
.setReblogged(reblog)
|
|
||||||
.createStatusViewData();
|
|
||||||
statuses.setPairedItem(actual.second, newViewData);
|
|
||||||
adapter.changeItem(actual.second, newViewData, false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -359,6 +457,25 @@ public class TimelineFragment extends SFragment implements
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setRebloggedForStatus(int position, Status status, boolean reblog) {
|
||||||
|
status.setReblogged(reblog);
|
||||||
|
|
||||||
|
if (status.getReblog() != null) {
|
||||||
|
status.getReblog().setReblogged(reblog);
|
||||||
|
}
|
||||||
|
|
||||||
|
Pair<StatusViewData.Concrete, Integer> actual =
|
||||||
|
findStatusAndPosition(position, status);
|
||||||
|
if (actual == null) return;
|
||||||
|
|
||||||
|
StatusViewData newViewData =
|
||||||
|
new StatusViewData.Builder(actual.first)
|
||||||
|
.setReblogged(reblog)
|
||||||
|
.createStatusViewData();
|
||||||
|
statuses.setPairedItem(actual.second, newViewData);
|
||||||
|
updateAdapter();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFavourite(final boolean favourite, final int position) {
|
public void onFavourite(final boolean favourite, final int position) {
|
||||||
final Status status = statuses.get(position).getAsRight();
|
final Status status = statuses.get(position).getAsRight();
|
||||||
|
@ -368,22 +485,7 @@ public class TimelineFragment extends SFragment implements
|
||||||
public void onResponse(@NonNull Call<Status> call, @NonNull Response<Status> response) {
|
public void onResponse(@NonNull Call<Status> call, @NonNull Response<Status> response) {
|
||||||
|
|
||||||
if (response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
status.setFavourited(favourite);
|
setFavouriteForStatus(position, status, favourite);
|
||||||
|
|
||||||
if (status.getReblog() != null) {
|
|
||||||
status.getReblog().setFavourited(favourite);
|
|
||||||
}
|
|
||||||
|
|
||||||
Pair<StatusViewData.Concrete, Integer> actual =
|
|
||||||
findStatusAndPosition(position, status);
|
|
||||||
if (actual == null) return;
|
|
||||||
|
|
||||||
StatusViewData newViewData = new StatusViewData
|
|
||||||
.Builder(actual.first)
|
|
||||||
.setFavourited(favourite)
|
|
||||||
.createStatusViewData();
|
|
||||||
statuses.setPairedItem(actual.second, newViewData);
|
|
||||||
adapter.changeItem(actual.second, newViewData, false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -394,6 +496,25 @@ public class TimelineFragment extends SFragment implements
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setFavouriteForStatus(int position, Status status, boolean favourite) {
|
||||||
|
status.setFavourited(favourite);
|
||||||
|
|
||||||
|
if (status.getReblog() != null) {
|
||||||
|
status.getReblog().setFavourited(favourite);
|
||||||
|
}
|
||||||
|
|
||||||
|
Pair<StatusViewData.Concrete, Integer> actual =
|
||||||
|
findStatusAndPosition(position, status);
|
||||||
|
if (actual == null) return;
|
||||||
|
|
||||||
|
StatusViewData newViewData = new StatusViewData
|
||||||
|
.Builder(actual.first)
|
||||||
|
.setFavourited(favourite)
|
||||||
|
.createStatusViewData();
|
||||||
|
statuses.setPairedItem(actual.second, newViewData);
|
||||||
|
updateAdapter();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onMore(View view, final int position) {
|
public void onMore(View view, final int position) {
|
||||||
super.more(statuses.get(position).getAsRight(), view, position);
|
super.more(statuses.get(position).getAsRight(), view, position);
|
||||||
|
@ -410,7 +531,7 @@ public class TimelineFragment extends SFragment implements
|
||||||
((StatusViewData.Concrete) statuses.getPairedItem(position)))
|
((StatusViewData.Concrete) statuses.getPairedItem(position)))
|
||||||
.setIsExpanded(expanded).createStatusViewData();
|
.setIsExpanded(expanded).createStatusViewData();
|
||||||
statuses.setPairedItem(position, newViewData);
|
statuses.setPairedItem(position, newViewData);
|
||||||
adapter.changeItem(position, newViewData, false);
|
updateAdapter();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -419,7 +540,7 @@ public class TimelineFragment extends SFragment implements
|
||||||
((StatusViewData.Concrete) statuses.getPairedItem(position)))
|
((StatusViewData.Concrete) statuses.getPairedItem(position)))
|
||||||
.setIsShowingSensitiveContent(isShowing).createStatusViewData();
|
.setIsShowingSensitiveContent(isShowing).createStatusViewData();
|
||||||
statuses.setPairedItem(position, newViewData);
|
statuses.setPairedItem(position, newViewData);
|
||||||
adapter.changeItem(position, newViewData, false);
|
updateAdapter();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -434,9 +555,10 @@ public class TimelineFragment extends SFragment implements
|
||||||
}
|
}
|
||||||
sendFetchTimelineRequest(fromStatus.getId(), toStatus.getId(), FetchEnd.MIDDLE, position);
|
sendFetchTimelineRequest(fromStatus.getId(), toStatus.getId(), FetchEnd.MIDDLE, position);
|
||||||
|
|
||||||
StatusViewData newViewData = new StatusViewData.Placeholder(true);
|
Placeholder placeholder = statuses.get(position).getAsLeft();
|
||||||
|
StatusViewData newViewData = new StatusViewData.Placeholder(placeholder.id, true);
|
||||||
statuses.setPairedItem(position, newViewData);
|
statuses.setPairedItem(position, newViewData);
|
||||||
adapter.changeItem(position, newViewData, false);
|
updateAdapter();
|
||||||
} else {
|
} else {
|
||||||
Log.e(TAG, "error loading more");
|
Log.e(TAG, "error loading more");
|
||||||
}
|
}
|
||||||
|
@ -530,10 +652,9 @@ public class TimelineFragment extends SFragment implements
|
||||||
@Override
|
@Override
|
||||||
public void removeItem(int position) {
|
public void removeItem(int position) {
|
||||||
statuses.remove(position);
|
statuses.remove(position);
|
||||||
adapter.update(statuses.getPairedCopy());
|
updateAdapter();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void removeAllByAccountId(String accountId) {
|
public void removeAllByAccountId(String accountId) {
|
||||||
// using iterator to safely remove items while iterating
|
// using iterator to safely remove items while iterating
|
||||||
Iterator<Either<Placeholder, Status>> iterator = statuses.iterator();
|
Iterator<Either<Placeholder, Status>> iterator = statuses.iterator();
|
||||||
|
@ -543,15 +664,34 @@ public class TimelineFragment extends SFragment implements
|
||||||
iterator.remove();
|
iterator.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
adapter.update(statuses.getPairedCopy());
|
updateAdapter();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onLoadMore() {
|
private void onLoadMore() {
|
||||||
|
if (didLoadEverythingBottom || bottomLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bottomLoading = true;
|
||||||
|
|
||||||
|
Either<Placeholder, Status> last = statuses.get(statuses.size() - 1);
|
||||||
|
Placeholder placeholder;
|
||||||
|
if (last.isRight()) {
|
||||||
|
placeholder = newPlaceholder();
|
||||||
|
statuses.add(Either.left(placeholder));
|
||||||
|
} else {
|
||||||
|
placeholder = last.getAsLeft();
|
||||||
|
}
|
||||||
|
statuses.setPairedItem(statuses.size() - 1,
|
||||||
|
new StatusViewData.Placeholder(placeholder.id, true));
|
||||||
|
|
||||||
|
updateAdapter();
|
||||||
|
|
||||||
sendFetchTimelineRequest(bottomId, null, FetchEnd.BOTTOM, -1);
|
sendFetchTimelineRequest(bottomId, null, FetchEnd.BOTTOM, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void fullyRefresh() {
|
private void fullyRefresh() {
|
||||||
adapter.clear();
|
statuses.clear();
|
||||||
|
updateAdapter();
|
||||||
sendFetchTimelineRequest(null, null, FetchEnd.TOP, -1);
|
sendFetchTimelineRequest(null, null, FetchEnd.TOP, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -560,7 +700,8 @@ public class TimelineFragment extends SFragment implements
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean actionButtonPresent() {
|
private boolean actionButtonPresent() {
|
||||||
return kind != Kind.TAG && kind != Kind.FAVOURITES;
|
return kind != Kind.TAG && kind != Kind.FAVOURITES &&
|
||||||
|
getActivity() instanceof ActionButtonActivity;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void jumpToTop() {
|
private void jumpToTop() {
|
||||||
|
@ -599,17 +740,6 @@ public class TimelineFragment extends SFragment implements
|
||||||
topFetches++;
|
topFetches++;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (fetchEnd == FetchEnd.BOTTOM && bottomLoading) {
|
|
||||||
bottomFetches++;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fromId != null || adapter.getItemCount() <= 1) {
|
|
||||||
/* When this is called by the EndlessScrollListener it cannot refresh the footer state
|
|
||||||
* using adapter.notifyItemChanged. So its necessary to postpone doing so until a
|
|
||||||
* convenient time for the UI thread using a Runnable. */
|
|
||||||
recyclerView.post(() -> adapter.setFooterState(FooterViewHolder.State.LOADING));
|
|
||||||
}
|
|
||||||
|
|
||||||
Callback<List<Status>> callback = new Callback<List<Status>>() {
|
Callback<List<Status>> callback = new Callback<List<Status>>() {
|
||||||
@Override
|
@Override
|
||||||
|
@ -635,6 +765,7 @@ public class TimelineFragment extends SFragment implements
|
||||||
|
|
||||||
private void onFetchTimelineSuccess(List<Status> statuses, String linkHeader,
|
private void onFetchTimelineSuccess(List<Status> statuses, String linkHeader,
|
||||||
FetchEnd fetchEnd, int pos) {
|
FetchEnd fetchEnd, int pos) {
|
||||||
|
|
||||||
// We filled the hole (or reached the end) if the server returned less statuses than we
|
// We filled the hole (or reached the end) if the server returned less statuses than we
|
||||||
// we asked for.
|
// we asked for.
|
||||||
boolean fullFetch = statuses.size() >= LOAD_AT_ONCE;
|
boolean fullFetch = statuses.size() >= LOAD_AT_ONCE;
|
||||||
|
@ -660,7 +791,13 @@ public class TimelineFragment extends SFragment implements
|
||||||
if (next != null) {
|
if (next != null) {
|
||||||
fromId = next.uri.getQueryParameter("max_id");
|
fromId = next.uri.getQueryParameter("max_id");
|
||||||
}
|
}
|
||||||
if (adapter.getItemCount() > 1) {
|
if (!this.statuses.isEmpty()
|
||||||
|
&& !this.statuses.get(this.statuses.size() - 1).isRight()) {
|
||||||
|
this.statuses.remove(this.statuses.size() - 1);
|
||||||
|
updateAdapter();
|
||||||
|
}
|
||||||
|
int oldSize = this.statuses.size();
|
||||||
|
if (this.statuses.size() > 1) {
|
||||||
addItems(statuses, fromId);
|
addItems(statuses, fromId);
|
||||||
} else {
|
} else {
|
||||||
/* If this is the first fetch, also save the id from the "previous" link and
|
/* If this is the first fetch, also save the id from the "previous" link and
|
||||||
|
@ -673,39 +810,45 @@ public class TimelineFragment extends SFragment implements
|
||||||
}
|
}
|
||||||
updateStatuses(statuses, fromId, uptoId, fullFetch);
|
updateStatuses(statuses, fromId, uptoId, fullFetch);
|
||||||
}
|
}
|
||||||
|
if (this.statuses.size() == oldSize) {
|
||||||
|
// This may be a brittle check but seems like it works
|
||||||
|
// Can we check it using headers somehow? Do all server support them?
|
||||||
|
didLoadEverythingBottom = true;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fulfillAnyQueuedFetches(fetchEnd);
|
fulfillAnyQueuedFetches(fetchEnd);
|
||||||
if (statuses.size() == 0 && adapter.getItemCount() == 1) {
|
progressBar.setVisibility(View.GONE);
|
||||||
adapter.setFooterState(FooterViewHolder.State.EMPTY);
|
|
||||||
} else {
|
|
||||||
adapter.setFooterState(FooterViewHolder.State.END);
|
|
||||||
}
|
|
||||||
swipeRefreshLayout.setRefreshing(false);
|
swipeRefreshLayout.setRefreshing(false);
|
||||||
|
if (this.statuses.size() == 0) {
|
||||||
|
nothingMessageView.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onFetchTimelineFailure(Exception exception, FetchEnd fetchEnd, int position) {
|
private void onFetchTimelineFailure(Exception exception, FetchEnd fetchEnd, int position) {
|
||||||
swipeRefreshLayout.setRefreshing(false);
|
swipeRefreshLayout.setRefreshing(false);
|
||||||
|
|
||||||
if (fetchEnd == FetchEnd.MIDDLE && !statuses.get(position).isRight()) {
|
if (fetchEnd == FetchEnd.MIDDLE && !statuses.get(position).isRight()) {
|
||||||
StatusViewData newViewData = new StatusViewData.Placeholder(false);
|
Placeholder placeholder = statuses.get(position).getAsLeftOrNull();
|
||||||
|
StatusViewData newViewData;
|
||||||
|
if (placeholder == null) {
|
||||||
|
placeholder = newPlaceholder();
|
||||||
|
}
|
||||||
|
newViewData = new StatusViewData.Placeholder(placeholder.id, false);
|
||||||
statuses.setPairedItem(position, newViewData);
|
statuses.setPairedItem(position, newViewData);
|
||||||
adapter.changeItem(position, newViewData, true);
|
updateAdapter();
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.e(TAG, "Fetch Failure: " + exception.getMessage());
|
Log.e(TAG, "Fetch Failure: " + exception.getMessage());
|
||||||
fulfillAnyQueuedFetches(fetchEnd);
|
fulfillAnyQueuedFetches(fetchEnd);
|
||||||
|
progressBar.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void fulfillAnyQueuedFetches(FetchEnd fetchEnd) {
|
private void fulfillAnyQueuedFetches(FetchEnd fetchEnd) {
|
||||||
switch (fetchEnd) {
|
switch (fetchEnd) {
|
||||||
case BOTTOM: {
|
case BOTTOM: {
|
||||||
bottomLoading = false;
|
bottomLoading = false;
|
||||||
if (bottomFetches > 0) {
|
|
||||||
bottomFetches--;
|
|
||||||
onLoadMore();
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case TOP: {
|
case TOP: {
|
||||||
|
@ -744,7 +887,7 @@ public class TimelineFragment extends SFragment implements
|
||||||
topId = toId;
|
topId = toId;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Either<Placeholder, Status>> liftedNew = listStatusList(newStatuses);
|
List<Either<Placeholder, Status>> liftedNew = liftStatusList(newStatuses);
|
||||||
|
|
||||||
if (statuses.isEmpty()) {
|
if (statuses.isEmpty()) {
|
||||||
statuses.addAll(liftedNew);
|
statuses.addAll(liftedNew);
|
||||||
|
@ -758,39 +901,35 @@ public class TimelineFragment extends SFragment implements
|
||||||
int newIndex = liftedNew.indexOf(statuses.get(0));
|
int newIndex = liftedNew.indexOf(statuses.get(0));
|
||||||
if (newIndex == -1) {
|
if (newIndex == -1) {
|
||||||
if (index == -1 && fullFetch) {
|
if (index == -1 && fullFetch) {
|
||||||
liftedNew.add(Either.left(Placeholder.getInstance()));
|
liftedNew.add(Either.left(newPlaceholder()));
|
||||||
}
|
}
|
||||||
statuses.addAll(0, liftedNew);
|
statuses.addAll(0, liftedNew);
|
||||||
} else {
|
} else {
|
||||||
statuses.addAll(0, liftedNew.subList(0, newIndex));
|
statuses.addAll(0, liftedNew.subList(0, newIndex));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
adapter.update(statuses.getPairedCopy());
|
updateAdapter();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addItems(List<Status> newStatuses, @Nullable String fromId) {
|
private void addItems(List<Status> newStatuses, @Nullable String fromId) {
|
||||||
if (ListUtils.isEmpty(newStatuses)) {
|
if (ListUtils.isEmpty(newStatuses)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int end = statuses.size();
|
Status last = null;
|
||||||
Status last = statuses.get(end - 1).getAsRightOrNull();
|
for (int i = statuses.size() - 1; i >= 0; i--) {
|
||||||
|
if (statuses.get(i).isRight()) {
|
||||||
|
last = statuses.get(i).getAsRight();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
// I was about to replace findStatus with indexOf but it is incorrect to compare value
|
// I was about to replace findStatus with indexOf but it is incorrect to compare value
|
||||||
// types by ID anyway and we should change equals() for Status, I think, so this makes sense
|
// types by ID anyway and we should change equals() for Status, I think, so this makes sense
|
||||||
if (last != null && !findStatus(newStatuses, last.getId())) {
|
if (last != null && !findStatus(newStatuses, last.getId())) {
|
||||||
statuses.addAll(listStatusList(newStatuses));
|
statuses.addAll(liftStatusList(newStatuses));
|
||||||
List<StatusViewData> newViewDatas = statuses.getPairedCopy()
|
|
||||||
.subList(statuses.size() - newStatuses.size(), statuses.size());
|
|
||||||
if (BuildConfig.DEBUG && newStatuses.size() != newViewDatas.size()) {
|
|
||||||
String error = String.format(Locale.getDefault(),
|
|
||||||
"Incorrectly got statusViewData sublist." +
|
|
||||||
" newStatuses.size == %d newViewDatas.size == %d, statuses.size == %d",
|
|
||||||
newStatuses.size(), newViewDatas.size(), statuses.size());
|
|
||||||
throw new AssertionError(error);
|
|
||||||
}
|
|
||||||
if (fromId != null) {
|
if (fromId != null) {
|
||||||
bottomId = fromId;
|
bottomId = fromId;
|
||||||
}
|
}
|
||||||
adapter.addItems(newViewDatas);
|
updateAdapter();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -801,18 +940,18 @@ public class TimelineFragment extends SFragment implements
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ListUtils.isEmpty(newStatuses)) {
|
if (ListUtils.isEmpty(newStatuses)) {
|
||||||
adapter.update(statuses.getPairedCopy());
|
updateAdapter();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Either<Placeholder, Status>> liftedNew = listStatusList(newStatuses);
|
List<Either<Placeholder, Status>> liftedNew = liftStatusList(newStatuses);
|
||||||
|
|
||||||
if (fullFetch) {
|
if (fullFetch) {
|
||||||
liftedNew.add(Either.left(Placeholder.getInstance()));
|
liftedNew.add(Either.left(newPlaceholder()));
|
||||||
}
|
}
|
||||||
|
|
||||||
statuses.addAll(pos, liftedNew);
|
statuses.addAll(pos, liftedNew);
|
||||||
adapter.update(statuses.getPairedCopy());
|
updateAdapter();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -825,6 +964,19 @@ public class TimelineFragment extends SFragment implements
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int findStatusOrReblogPositionById(@NonNull String statusId) {
|
||||||
|
for (int i = 0; i < statuses.size(); i++) {
|
||||||
|
Status status = statuses.get(i).getAsRightOrNull();
|
||||||
|
if (status != null
|
||||||
|
&& (statusId.equals(status.getId())
|
||||||
|
|| (status.getReblog() != null
|
||||||
|
&& statusId.equals(status.getReblog().getId())))) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
private final Function<Status, Either<Placeholder, Status>> statusLifter =
|
private final Function<Status, Either<Placeholder, Status>> statusLifter =
|
||||||
Either::right;
|
Either::right;
|
||||||
|
|
||||||
|
@ -851,7 +1003,111 @@ public class TimelineFragment extends SFragment implements
|
||||||
return new Pair<>(statusToUpdate, positionToUpdate);
|
return new Pair<>(statusToUpdate, positionToUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Either<Placeholder, Status>> listStatusList(List<Status> list) {
|
private void handleReblogEvent(@NonNull ReblogEvent reblogEvent) {
|
||||||
|
int pos = findStatusOrReblogPositionById(reblogEvent.getStatusId());
|
||||||
|
if (pos < 0) return;
|
||||||
|
Status status = statuses.get(pos).getAsRight();
|
||||||
|
setRebloggedForStatus(pos, status, reblogEvent.getReblog());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleFavEvent(@NonNull FavoriteEvent favEvent) {
|
||||||
|
int pos = findStatusOrReblogPositionById(favEvent.getStatusId());
|
||||||
|
if (pos < 0) return;
|
||||||
|
Status status = statuses.get(pos).getAsRight();
|
||||||
|
setFavouriteForStatus(pos, status, favEvent.getFavourite());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleStatusComposeEvent(@NonNull Status status) {
|
||||||
|
switch (kind) {
|
||||||
|
case HOME:
|
||||||
|
case PUBLIC_FEDERATED:
|
||||||
|
case PUBLIC_LOCAL:
|
||||||
|
break;
|
||||||
|
case USER:
|
||||||
|
if (status.getAccount().getId().equals(hashtagOrId)) {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case TAG:
|
||||||
|
case FAVOURITES:
|
||||||
|
case LIST:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Either<Placeholder, Status>> liftStatusList(List<Status> list) {
|
||||||
return CollectionUtil.map(list, statusLifter);
|
return CollectionUtil.map(list, statusLifter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Placeholder newPlaceholder() {
|
||||||
|
Placeholder placeholder = Placeholder.getInstance(maxPlaceholderId);
|
||||||
|
maxPlaceholderId--;
|
||||||
|
return placeholder;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateAdapter() {
|
||||||
|
differ.submitList(statuses.getPairedCopy());
|
||||||
|
}
|
||||||
|
|
||||||
|
private final ListUpdateCallback listUpdateCallback = new ListUpdateCallback() {
|
||||||
|
@Override
|
||||||
|
public void onInserted(int position, int count) {
|
||||||
|
adapter.notifyItemRangeInserted(position, count);
|
||||||
|
if (position == 0
|
||||||
|
&& layoutManager.findFirstVisibleItemPosition() == 0
|
||||||
|
&& (swipeRefreshLayout.getVisibility() == View.VISIBLE
|
||||||
|
|| progressBar.getVisibility() == View.VISIBLE)) {
|
||||||
|
recyclerView.post(() -> layoutManager.scrollToPosition(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRemoved(int position, int count) {
|
||||||
|
adapter.notifyItemRangeRemoved(position, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMoved(int fromPosition, int toPosition) {
|
||||||
|
adapter.notifyItemMoved(fromPosition, toPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onChanged(int position, int count, Object payload) {
|
||||||
|
adapter.notifyItemRangeChanged(position, count, payload);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
private final AsyncListDiffer<StatusViewData>
|
||||||
|
differ = new AsyncListDiffer<>(listUpdateCallback,
|
||||||
|
new AsyncDifferConfig.Builder<>(diffCallback).build());
|
||||||
|
|
||||||
|
private final TimelineAdapter.AdapterDataSource<StatusViewData> dataSource =
|
||||||
|
new TimelineAdapter.AdapterDataSource<StatusViewData>() {
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
return differ.getCurrentList().size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public StatusViewData getItemAt(int pos) {
|
||||||
|
return differ.getCurrentList().get(pos);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private static final DiffUtil.ItemCallback<StatusViewData> diffCallback
|
||||||
|
= new DiffUtil.ItemCallback<StatusViewData>() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean areItemsTheSame(StatusViewData oldItem, StatusViewData newItem) {
|
||||||
|
return oldItem.getViewDataId() == newItem.getViewDataId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean areContentsTheSame(StatusViewData oldItem, StatusViewData newItem) {
|
||||||
|
return oldItem.deepEquals(newItem);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
package com.keylesspalace.tusky.fragment;
|
package com.keylesspalace.tusky.fragment;
|
||||||
|
|
||||||
import android.arch.core.util.Function;
|
import android.arch.core.util.Function;
|
||||||
|
import android.arch.lifecycle.Lifecycle;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
|
@ -24,11 +25,12 @@ import android.preference.PreferenceManager;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.design.widget.Snackbar;
|
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.v4.widget.SwipeRefreshLayout;
|
||||||
import android.support.v7.widget.DividerItemDecoration;
|
import android.support.v7.widget.DividerItemDecoration;
|
||||||
import android.support.v7.widget.LinearLayoutManager;
|
import android.support.v7.widget.LinearLayoutManager;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
import android.support.v7.widget.SimpleItemAnimator;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
|
@ -39,14 +41,19 @@ import com.keylesspalace.tusky.BuildConfig;
|
||||||
import com.keylesspalace.tusky.R;
|
import com.keylesspalace.tusky.R;
|
||||||
import com.keylesspalace.tusky.ViewThreadActivity;
|
import com.keylesspalace.tusky.ViewThreadActivity;
|
||||||
import com.keylesspalace.tusky.adapter.ThreadAdapter;
|
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.di.Injectable;
|
||||||
import com.keylesspalace.tusky.entity.Attachment;
|
|
||||||
import com.keylesspalace.tusky.entity.Card;
|
import com.keylesspalace.tusky.entity.Card;
|
||||||
import com.keylesspalace.tusky.entity.Status;
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
import com.keylesspalace.tusky.entity.StatusContext;
|
import com.keylesspalace.tusky.entity.StatusContext;
|
||||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||||
|
import com.keylesspalace.tusky.network.MastodonApi;
|
||||||
import com.keylesspalace.tusky.network.TimelineCases;
|
import com.keylesspalace.tusky.network.TimelineCases;
|
||||||
import com.keylesspalace.tusky.receiver.TimelineReceiver;
|
|
||||||
import com.keylesspalace.tusky.util.PairedList;
|
import com.keylesspalace.tusky.util.PairedList;
|
||||||
import com.keylesspalace.tusky.util.ThemeUtils;
|
import com.keylesspalace.tusky.util.ThemeUtils;
|
||||||
import com.keylesspalace.tusky.util.ViewDataUtils;
|
import com.keylesspalace.tusky.util.ViewDataUtils;
|
||||||
|
@ -59,22 +66,29 @@ import java.util.Locale;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
import retrofit2.Call;
|
import retrofit2.Call;
|
||||||
import retrofit2.Callback;
|
import retrofit2.Callback;
|
||||||
import retrofit2.Response;
|
import retrofit2.Response;
|
||||||
|
|
||||||
|
import static com.uber.autodispose.AutoDispose.*;
|
||||||
|
import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.*;
|
||||||
|
|
||||||
public final class ViewThreadFragment extends SFragment implements
|
public final class ViewThreadFragment extends SFragment implements
|
||||||
SwipeRefreshLayout.OnRefreshListener, StatusActionListener, Injectable {
|
SwipeRefreshLayout.OnRefreshListener, StatusActionListener, Injectable {
|
||||||
private static final String TAG = "ViewThreadFragment";
|
private static final String TAG = "ViewThreadFragment";
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public TimelineCases timelineCases;
|
public TimelineCases timelineCases;
|
||||||
|
@Inject
|
||||||
|
public MastodonApi mastodonApi;
|
||||||
|
@Inject
|
||||||
|
public EventHub eventHub;
|
||||||
|
|
||||||
private SwipeRefreshLayout swipeRefreshLayout;
|
private SwipeRefreshLayout swipeRefreshLayout;
|
||||||
private RecyclerView recyclerView;
|
private RecyclerView recyclerView;
|
||||||
private ThreadAdapter adapter;
|
private ThreadAdapter adapter;
|
||||||
private String thisThreadsStatusId;
|
private String thisThreadsStatusId;
|
||||||
private TimelineReceiver timelineReceiver;
|
|
||||||
private Card card;
|
private Card card;
|
||||||
private boolean alwaysShowSensitiveMedia;
|
private boolean alwaysShowSensitiveMedia;
|
||||||
|
|
||||||
|
@ -101,6 +115,36 @@ public final class ViewThreadFragment extends SFragment implements
|
||||||
return timelineCases;
|
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
|
@Override
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
|
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
|
||||||
@Nullable Bundle savedInstanceState) {
|
@Nullable Bundle savedInstanceState) {
|
||||||
|
@ -128,7 +172,6 @@ public final class ViewThreadFragment extends SFragment implements
|
||||||
R.drawable.conversation_thread_line_dark);
|
R.drawable.conversation_thread_line_dark);
|
||||||
recyclerView.addItemDecoration(new ConversationLineItemDecoration(context,
|
recyclerView.addItemDecoration(new ConversationLineItemDecoration(context,
|
||||||
threadLineDrawable));
|
threadLineDrawable));
|
||||||
adapter = new ThreadAdapter(this);
|
|
||||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(
|
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(
|
||||||
getActivity());
|
getActivity());
|
||||||
alwaysShowSensitiveMedia = preferences.getBoolean("alwaysShowSensitiveMedia", false);
|
alwaysShowSensitiveMedia = preferences.getBoolean("alwaysShowSensitiveMedia", false);
|
||||||
|
@ -139,19 +182,11 @@ public final class ViewThreadFragment extends SFragment implements
|
||||||
statuses.clear();
|
statuses.clear();
|
||||||
thisThreadsStatusId = null;
|
thisThreadsStatusId = null;
|
||||||
|
|
||||||
timelineReceiver = new TimelineReceiver(this, this);
|
((SimpleItemAnimator) recyclerView.getItemAnimator()).setSupportsChangeAnimations(false);
|
||||||
LocalBroadcastManager.getInstance(context.getApplicationContext())
|
|
||||||
.registerReceiver(timelineReceiver, TimelineReceiver.getFilter(null));
|
|
||||||
|
|
||||||
return rootView;
|
return rootView;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroyView() {
|
|
||||||
LocalBroadcastManager.getInstance(getContext())
|
|
||||||
.unregisterReceiver(timelineReceiver);
|
|
||||||
super.onDestroyView();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
||||||
|
@ -202,21 +237,8 @@ public final class ViewThreadFragment extends SFragment implements
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(@NonNull Call<Status> call, @NonNull Response<Status> response) {
|
public void onResponse(@NonNull Call<Status> call, @NonNull Response<Status> response) {
|
||||||
if (response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
status.setReblogged(reblog);
|
setReblogForStatus(position, status, reblog);
|
||||||
|
eventHub.dispatch(new ReblogEvent(status.getId(), 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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
@Override
|
||||||
public void onFavourite(final boolean favourite, final int position) {
|
public void onFavourite(final boolean favourite, final int position) {
|
||||||
final Status status = statuses.get(position);
|
final Status status = statuses.get(position);
|
||||||
|
@ -235,21 +275,8 @@ public final class ViewThreadFragment extends SFragment implements
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(@NonNull Call<Status> call, @NonNull Response<Status> response) {
|
public void onResponse(@NonNull Call<Status> call, @NonNull Response<Status> response) {
|
||||||
if (response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
status.setFavourited(favourite);
|
setFavForStatus(position, status, favourite);
|
||||||
|
eventHub.dispatch(new FavoriteEvent(status.getId(), 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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
@Override
|
||||||
public void onMore(View view, int position) {
|
public void onMore(View view, int position) {
|
||||||
super.more(statuses.get(position), view, position);
|
super.more(statuses.get(position), view, position);
|
||||||
|
@ -334,8 +379,7 @@ public final class ViewThreadFragment extends SFragment implements
|
||||||
adapter.setStatuses(statuses.getPairedCopy());
|
adapter.setStatuses(statuses.getPairedCopy());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private void removeAllByAccountId(String accountId) {
|
||||||
public void removeAllByAccountId(String accountId) {
|
|
||||||
Status status = null;
|
Status status = null;
|
||||||
if (!statuses.isEmpty()) {
|
if (!statuses.isEmpty()) {
|
||||||
status = statuses.get(statusIndex);
|
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() {
|
private void updateRevealIcon() {
|
||||||
ViewThreadActivity activity = ((ViewThreadActivity) getActivity());
|
ViewThreadActivity activity = ((ViewThreadActivity) getActivity());
|
||||||
if (activity == null) return;
|
if (activity == null) return;
|
||||||
|
@ -545,8 +652,8 @@ public final class ViewThreadFragment extends SFragment implements
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!hasAnyWarnings) {
|
if (!hasAnyWarnings) {
|
||||||
activity.setRevealButtonState(ViewThreadActivity.REVEAL_BUTTON_HIDDEN);
|
activity.setRevealButtonState(ViewThreadActivity.REVEAL_BUTTON_HIDDEN);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
activity.setRevealButtonState(allExpanded() ? ViewThreadActivity.REVEAL_BUTTON_HIDE :
|
activity.setRevealButtonState(allExpanded() ? ViewThreadActivity.REVEAL_BUTTON_HIDE :
|
||||||
ViewThreadActivity.REVEAL_BUTTON_REVEAL);
|
ViewThreadActivity.REVEAL_BUTTON_REVEAL);
|
||||||
|
|
|
@ -1,21 +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.interfaces;
|
|
||||||
|
|
||||||
public interface AdapterItemRemover {
|
|
||||||
void removeItem(int position);
|
|
||||||
void removeAllByAccountId(String accountId);
|
|
||||||
}
|
|
|
@ -15,11 +15,12 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.network
|
package com.keylesspalace.tusky.network
|
||||||
|
|
||||||
import android.content.Intent
|
import com.keylesspalace.tusky.appstore.EventHub
|
||||||
import android.support.v4.content.LocalBroadcastManager
|
import com.keylesspalace.tusky.appstore.BlockEvent
|
||||||
|
import com.keylesspalace.tusky.appstore.MuteEvent
|
||||||
|
import com.keylesspalace.tusky.appstore.StatusDeletedEvent
|
||||||
import com.keylesspalace.tusky.entity.Relationship
|
import com.keylesspalace.tusky.entity.Relationship
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.receiver.TimelineReceiver
|
|
||||||
import okhttp3.ResponseBody
|
import okhttp3.ResponseBody
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
import retrofit2.Callback
|
import retrofit2.Callback
|
||||||
|
@ -39,7 +40,7 @@ interface TimelineCases {
|
||||||
|
|
||||||
class TimelineCasesImpl(
|
class TimelineCasesImpl(
|
||||||
private val mastodonApi: MastodonApi,
|
private val mastodonApi: MastodonApi,
|
||||||
private val broadcastManager: LocalBroadcastManager
|
private val eventHub: EventHub
|
||||||
) : TimelineCases {
|
) : TimelineCases {
|
||||||
override fun reblogWithCallback(status: Status, reblog: Boolean, callback: Callback<Status>) {
|
override fun reblogWithCallback(status: Status, reblog: Boolean, callback: Callback<Status>) {
|
||||||
val id = status.actionableId
|
val id = status.actionableId
|
||||||
|
@ -70,9 +71,7 @@ class TimelineCasesImpl(
|
||||||
|
|
||||||
override fun onFailure(call: Call<Relationship>, t: Throwable) {}
|
override fun onFailure(call: Call<Relationship>, t: Throwable) {}
|
||||||
})
|
})
|
||||||
val intent = Intent(TimelineReceiver.Types.MUTE_ACCOUNT)
|
eventHub.dispatch(MuteEvent(id))
|
||||||
intent.putExtra("id", id)
|
|
||||||
broadcastManager.sendBroadcast(intent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun block(id: String) {
|
override fun block(id: String) {
|
||||||
|
@ -82,9 +81,8 @@ class TimelineCasesImpl(
|
||||||
|
|
||||||
override fun onFailure(call: Call<Relationship>, t: Throwable) {}
|
override fun onFailure(call: Call<Relationship>, t: Throwable) {}
|
||||||
})
|
})
|
||||||
val intent = Intent(TimelineReceiver.Types.BLOCK_ACCOUNT)
|
eventHub.dispatch(BlockEvent(id))
|
||||||
intent.putExtra("id", id)
|
|
||||||
broadcastManager.sendBroadcast(intent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun delete(id: String) {
|
override fun delete(id: String) {
|
||||||
|
@ -94,6 +92,7 @@ class TimelineCasesImpl(
|
||||||
|
|
||||||
override fun onFailure(call: Call<ResponseBody>, t: Throwable) {}
|
override fun onFailure(call: Call<ResponseBody>, t: Throwable) {}
|
||||||
})
|
})
|
||||||
|
eventHub.dispatch(StatusDeletedEvent(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,69 +0,0 @@
|
||||||
package com.keylesspalace.tusky.receiver;
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.IntentFilter;
|
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
import android.support.v4.widget.SwipeRefreshLayout;
|
|
||||||
|
|
||||||
import com.keylesspalace.tusky.fragment.TimelineFragment;
|
|
||||||
import com.keylesspalace.tusky.interfaces.AdapterItemRemover;
|
|
||||||
|
|
||||||
public class TimelineReceiver extends BroadcastReceiver {
|
|
||||||
public static final class Types {
|
|
||||||
public static final String UNFOLLOW_ACCOUNT = "UNFOLLOW_ACCOUNT";
|
|
||||||
public static final String BLOCK_ACCOUNT = "BLOCK_ACCOUNT";
|
|
||||||
public static final String MUTE_ACCOUNT = "MUTE_ACCOUNT";
|
|
||||||
public static final String STATUS_COMPOSED = "STATUS_COMPOSED";
|
|
||||||
}
|
|
||||||
|
|
||||||
private AdapterItemRemover adapter;
|
|
||||||
private SwipeRefreshLayout.OnRefreshListener refreshListener;
|
|
||||||
|
|
||||||
public TimelineReceiver(AdapterItemRemover adapter) {
|
|
||||||
super();
|
|
||||||
this.adapter = adapter;
|
|
||||||
}
|
|
||||||
|
|
||||||
public TimelineReceiver(AdapterItemRemover adapter,
|
|
||||||
SwipeRefreshLayout.OnRefreshListener refreshListener) {
|
|
||||||
super();
|
|
||||||
this.adapter = adapter;
|
|
||||||
this.refreshListener = refreshListener;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onReceive(Context context, final Intent intent) {
|
|
||||||
switch (intent.getAction()) {
|
|
||||||
case Types.STATUS_COMPOSED: {
|
|
||||||
if (refreshListener != null) {
|
|
||||||
refreshListener.onRefresh();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
String id = intent.getStringExtra("id");
|
|
||||||
adapter.removeAllByAccountId(id);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IntentFilter getFilter(@Nullable TimelineFragment.Kind kind) {
|
|
||||||
IntentFilter intentFilter = new IntentFilter();
|
|
||||||
if (kind == TimelineFragment.Kind.HOME) {
|
|
||||||
intentFilter.addAction(Types.UNFOLLOW_ACCOUNT);
|
|
||||||
}
|
|
||||||
intentFilter.addAction(Types.BLOCK_ACCOUNT);
|
|
||||||
intentFilter.addAction(Types.MUTE_ACCOUNT);
|
|
||||||
if (kind == null
|
|
||||||
|| kind == TimelineFragment.Kind.HOME
|
|
||||||
|| kind == TimelineFragment.Kind.PUBLIC_FEDERATED
|
|
||||||
|| kind == TimelineFragment.Kind.PUBLIC_LOCAL) {
|
|
||||||
intentFilter.addAction(Types.STATUS_COMPOSED);
|
|
||||||
}
|
|
||||||
|
|
||||||
return intentFilter;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -12,15 +12,15 @@ import android.os.Parcelable
|
||||||
import android.support.v4.app.NotificationCompat
|
import android.support.v4.app.NotificationCompat
|
||||||
import android.support.v4.app.ServiceCompat
|
import android.support.v4.app.ServiceCompat
|
||||||
import android.support.v4.content.ContextCompat
|
import android.support.v4.content.ContextCompat
|
||||||
import android.support.v4.content.LocalBroadcastManager
|
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.TuskyApplication
|
import com.keylesspalace.tusky.appstore.EventHub
|
||||||
|
import com.keylesspalace.tusky.appstore.StatusComposedEvent
|
||||||
import com.keylesspalace.tusky.db.AccountEntity
|
import com.keylesspalace.tusky.db.AccountEntity
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
|
import com.keylesspalace.tusky.db.AppDatabase
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.receiver.TimelineReceiver
|
|
||||||
import com.keylesspalace.tusky.util.SaveTootHelper
|
import com.keylesspalace.tusky.util.SaveTootHelper
|
||||||
import com.keylesspalace.tusky.util.StringUtils
|
import com.keylesspalace.tusky.util.StringUtils
|
||||||
import dagger.android.AndroidInjection
|
import dagger.android.AndroidInjection
|
||||||
|
@ -30,14 +30,19 @@ import retrofit2.Callback
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class SendTootService: Service(), Injectable {
|
class SendTootService : Service(), Injectable {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var mastodonApi: MastodonApi
|
lateinit var mastodonApi: MastodonApi
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var accountManager: AccountManager
|
lateinit var accountManager: AccountManager
|
||||||
|
@Inject
|
||||||
|
lateinit var eventHub: EventHub
|
||||||
|
@Inject
|
||||||
|
lateinit var database: AppDatabase
|
||||||
|
|
||||||
private lateinit var saveTootHelper: SaveTootHelper
|
private lateinit var saveTootHelper: SaveTootHelper
|
||||||
|
|
||||||
|
@ -50,7 +55,7 @@ class SendTootService: Service(), Injectable {
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
AndroidInjection.inject(this)
|
AndroidInjection.inject(this)
|
||||||
saveTootHelper = SaveTootHelper(TuskyApplication.getDB().tootDao(), this)
|
saveTootHelper = SaveTootHelper(database.tootDao(), this)
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,13 +65,9 @@ class SendTootService: Service(), Injectable {
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||||
|
|
||||||
if(intent.hasExtra(KEY_TOOT)) {
|
if (intent.hasExtra(KEY_TOOT)) {
|
||||||
|
|
||||||
val tootToSend = intent.getParcelableExtra<TootToSend>(KEY_TOOT)
|
val tootToSend = intent.getParcelableExtra<TootToSend>(KEY_TOOT)
|
||||||
|
?: throw IllegalStateException("SendTootService started without $KEY_TOOT extra")
|
||||||
if (tootToSend == null) {
|
|
||||||
throw IllegalStateException("SendTootService started without $KEY_TOOT extra")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
val channel = NotificationChannel(CHANNEL_ID, getString(R.string.send_toot_notification_channel_name), NotificationManager.IMPORTANCE_LOW)
|
val channel = NotificationChannel(CHANNEL_ID, getString(R.string.send_toot_notification_channel_name), NotificationManager.IMPORTANCE_LOW)
|
||||||
|
@ -88,7 +89,7 @@ class SendTootService: Service(), Injectable {
|
||||||
.setColor(ContextCompat.getColor(this, R.color.primary))
|
.setColor(ContextCompat.getColor(this, R.color.primary))
|
||||||
.addAction(0, getString(android.R.string.cancel), cancelSendingIntent(sendingNotificationId))
|
.addAction(0, getString(android.R.string.cancel), cancelSendingIntent(sendingNotificationId))
|
||||||
|
|
||||||
if(tootsToSend.size == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (tootsToSend.size == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_DETACH)
|
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_DETACH)
|
||||||
startForeground(sendingNotificationId, builder.build())
|
startForeground(sendingNotificationId, builder.build())
|
||||||
} else {
|
} else {
|
||||||
|
@ -100,7 +101,7 @@ class SendTootService: Service(), Injectable {
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
if(intent.hasExtra(KEY_CANCEL)) {
|
if (intent.hasExtra(KEY_CANCEL)) {
|
||||||
cancelSending(intent.getIntExtra(KEY_CANCEL, 0))
|
cancelSending(intent.getIntExtra(KEY_CANCEL, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,7 +119,7 @@ class SendTootService: Service(), Injectable {
|
||||||
// when account == null, user has logged out, cancel sending
|
// when account == null, user has logged out, cancel sending
|
||||||
val account = accountManager.getAccountById(tootToSend.accountId)
|
val account = accountManager.getAccountById(tootToSend.accountId)
|
||||||
|
|
||||||
if(account == null) {
|
if (account == null) {
|
||||||
tootsToSend.remove(tootId)
|
tootsToSend.remove(tootId)
|
||||||
notificationManager.cancel(tootId)
|
notificationManager.cancel(tootId)
|
||||||
stopSelfWhenDone()
|
stopSelfWhenDone()
|
||||||
|
@ -142,21 +143,19 @@ class SendTootService: Service(), Injectable {
|
||||||
|
|
||||||
sendCalls[tootId] = sendCall
|
sendCalls[tootId] = sendCall
|
||||||
|
|
||||||
val callback = object: Callback<Status> {
|
val callback = object : Callback<Status> {
|
||||||
override fun onResponse(call: Call<Status>, response: Response<Status>) {
|
override fun onResponse(call: Call<Status>, response: Response<Status>) {
|
||||||
|
|
||||||
tootsToSend.remove(tootId)
|
tootsToSend.remove(tootId)
|
||||||
|
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
|
|
||||||
val intent = Intent(TimelineReceiver.Types.STATUS_COMPOSED)
|
|
||||||
LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(intent)
|
|
||||||
|
|
||||||
// If the status was loaded from a draft, delete the draft and associated media files.
|
// If the status was loaded from a draft, delete the draft and associated media files.
|
||||||
if(tootToSend.savedTootUid != 0) {
|
if (tootToSend.savedTootUid != 0) {
|
||||||
saveTootHelper.deleteDraft(tootToSend.savedTootUid)
|
saveTootHelper.deleteDraft(tootToSend.savedTootUid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
response.body()?.let(::StatusComposedEvent)?.let(eventHub::dispatch)
|
||||||
|
|
||||||
notificationManager.cancel(tootId)
|
notificationManager.cancel(tootId)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
@ -179,7 +178,7 @@ class SendTootService: Service(), Injectable {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailure(call: Call<Status>, t: Throwable) {
|
override fun onFailure(call: Call<Status>, t: Throwable) {
|
||||||
var backoff = 1000L*tootToSend.retries
|
var backoff = TimeUnit.SECONDS.toMillis(tootToSend.retries.toLong())
|
||||||
if (backoff > MAX_RETRY_INTERVAL) {
|
if (backoff > MAX_RETRY_INTERVAL) {
|
||||||
backoff = MAX_RETRY_INTERVAL
|
backoff = MAX_RETRY_INTERVAL
|
||||||
}
|
}
|
||||||
|
@ -206,7 +205,7 @@ class SendTootService: Service(), Injectable {
|
||||||
|
|
||||||
private fun cancelSending(tootId: Int) {
|
private fun cancelSending(tootId: Int) {
|
||||||
val tootToCancel = tootsToSend.remove(tootId)
|
val tootToCancel = tootsToSend.remove(tootId)
|
||||||
if(tootToCancel != null) {
|
if (tootToCancel != null) {
|
||||||
val sendCall = sendCalls.remove(tootId)
|
val sendCall = sendCalls.remove(tootId)
|
||||||
sendCall?.cancel()
|
sendCall?.cancel()
|
||||||
|
|
||||||
|
@ -259,7 +258,7 @@ class SendTootService: Service(), Injectable {
|
||||||
private const val KEY_CANCEL = "cancel_id"
|
private const val KEY_CANCEL = "cancel_id"
|
||||||
private const val CHANNEL_ID = "send_toots"
|
private const val CHANNEL_ID = "send_toots"
|
||||||
|
|
||||||
private const val MAX_RETRY_INTERVAL = 60*1000L // 1 minute
|
private val MAX_RETRY_INTERVAL = TimeUnit.MINUTES.toMillis(1)
|
||||||
|
|
||||||
private var sendingNotificationId = -1 // use negative ids to not clash with other notis
|
private var sendingNotificationId = -1 // use negative ids to not clash with other notis
|
||||||
private var errorNotificationId = Int.MIN_VALUE // use even more negative ids to not clash with other notis
|
private var errorNotificationId = Int.MIN_VALUE // use even more negative ids to not clash with other notis
|
||||||
|
@ -320,4 +319,4 @@ data class TootToSend(val text: String,
|
||||||
val accountId: Long,
|
val accountId: Long,
|
||||||
val savedTootUid: Int,
|
val savedTootUid: Int,
|
||||||
val idempotencyKey: String,
|
val idempotencyKey: String,
|
||||||
var retries: Int): Parcelable
|
var retries: Int) : Parcelable
|
||||||
|
|
|
@ -20,18 +20,12 @@ import android.support.v7.widget.RecyclerView;
|
||||||
|
|
||||||
public abstract class EndlessOnScrollListener extends RecyclerView.OnScrollListener {
|
public abstract class EndlessOnScrollListener extends RecyclerView.OnScrollListener {
|
||||||
private static final int VISIBLE_THRESHOLD = 15;
|
private static final int VISIBLE_THRESHOLD = 15;
|
||||||
private int currentPage;
|
|
||||||
private int previousTotalItemCount;
|
private int previousTotalItemCount;
|
||||||
private boolean loading;
|
|
||||||
private int startingPageIndex;
|
|
||||||
private LinearLayoutManager layoutManager;
|
private LinearLayoutManager layoutManager;
|
||||||
|
|
||||||
public EndlessOnScrollListener(LinearLayoutManager layoutManager) {
|
public EndlessOnScrollListener(LinearLayoutManager layoutManager) {
|
||||||
this.layoutManager = layoutManager;
|
this.layoutManager = layoutManager;
|
||||||
currentPage = 0;
|
|
||||||
previousTotalItemCount = 0;
|
previousTotalItemCount = 0;
|
||||||
loading = true;
|
|
||||||
startingPageIndex = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -39,28 +33,21 @@ public abstract class EndlessOnScrollListener extends RecyclerView.OnScrollListe
|
||||||
int totalItemCount = layoutManager.getItemCount();
|
int totalItemCount = layoutManager.getItemCount();
|
||||||
int lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition();
|
int lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition();
|
||||||
if (totalItemCount < previousTotalItemCount) {
|
if (totalItemCount < previousTotalItemCount) {
|
||||||
currentPage = startingPageIndex;
|
|
||||||
previousTotalItemCount = totalItemCount;
|
previousTotalItemCount = totalItemCount;
|
||||||
if (totalItemCount == 0) {
|
|
||||||
loading = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (loading && totalItemCount > previousTotalItemCount) {
|
if (totalItemCount != previousTotalItemCount) {
|
||||||
loading = false;
|
|
||||||
previousTotalItemCount = totalItemCount;
|
previousTotalItemCount = totalItemCount;
|
||||||
}
|
}
|
||||||
if (!loading && lastVisibleItemPosition + VISIBLE_THRESHOLD > totalItemCount) {
|
|
||||||
currentPage++;
|
if (lastVisibleItemPosition + VISIBLE_THRESHOLD > totalItemCount) {
|
||||||
onLoadMore(currentPage, totalItemCount, view);
|
onLoadMore(totalItemCount, view);
|
||||||
loading = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void reset() {
|
public void reset() {
|
||||||
currentPage = startingPageIndex;
|
|
||||||
previousTotalItemCount = 0;
|
previousTotalItemCount = 0;
|
||||||
loading = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract void onLoadMore(int page, int totalItemsCount, RecyclerView view);
|
public abstract void onLoadMore(int totalItemsCount, RecyclerView view);
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,9 +24,11 @@ import com.keylesspalace.tusky.entity.Emoji;
|
||||||
import com.keylesspalace.tusky.entity.Status;
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by charlag on 11/07/2017.
|
* Created by charlag on 11/07/2017.
|
||||||
|
@ -40,6 +42,10 @@ public abstract class StatusViewData {
|
||||||
private StatusViewData() {
|
private StatusViewData() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public abstract long getViewDataId();
|
||||||
|
|
||||||
|
public abstract boolean deepEquals(StatusViewData other);
|
||||||
|
|
||||||
public static final class Concrete extends StatusViewData {
|
public static final class Concrete extends StatusViewData {
|
||||||
private final String id;
|
private final String id;
|
||||||
private final Spanned content;
|
private final Spanned content;
|
||||||
|
@ -214,18 +220,84 @@ public abstract class StatusViewData {
|
||||||
return card;
|
return card;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override public long getViewDataId() {
|
||||||
|
// Chance of collision is super low and impact of mistake is low as well
|
||||||
|
return getId().hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean deepEquals(StatusViewData o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
Concrete concrete = (Concrete) o;
|
||||||
|
return reblogged == concrete.reblogged &&
|
||||||
|
favourited == concrete.favourited &&
|
||||||
|
isSensitive == concrete.isSensitive &&
|
||||||
|
isExpanded == concrete.isExpanded &&
|
||||||
|
isShowingContent == concrete.isShowingContent &&
|
||||||
|
reblogsCount == concrete.reblogsCount &&
|
||||||
|
favouritesCount == concrete.favouritesCount &&
|
||||||
|
rebloggingEnabled == concrete.rebloggingEnabled &&
|
||||||
|
Objects.equals(id, concrete.id) &&
|
||||||
|
Objects.equals(content, concrete.content) &&
|
||||||
|
Objects.equals(spoilerText, concrete.spoilerText) &&
|
||||||
|
visibility == concrete.visibility &&
|
||||||
|
Objects.equals(attachments, concrete.attachments) &&
|
||||||
|
Objects.equals(rebloggedByUsername, concrete.rebloggedByUsername) &&
|
||||||
|
Objects.equals(rebloggedAvatar, concrete.rebloggedAvatar) &&
|
||||||
|
Objects.equals(userFullName, concrete.userFullName) &&
|
||||||
|
Objects.equals(nickname, concrete.nickname) &&
|
||||||
|
Objects.equals(avatar, concrete.avatar) &&
|
||||||
|
Objects.equals(createdAt, concrete.createdAt) &&
|
||||||
|
Objects.equals(inReplyToId, concrete.inReplyToId) &&
|
||||||
|
Arrays.equals(mentions, concrete.mentions) &&
|
||||||
|
Objects.equals(senderId, concrete.senderId) &&
|
||||||
|
Objects.equals(application, concrete.application) &&
|
||||||
|
Objects.equals(emojis, concrete.emojis) &&
|
||||||
|
Objects.equals(card, concrete.card);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final class Placeholder extends StatusViewData {
|
public static final class Placeholder extends StatusViewData {
|
||||||
private final boolean isLoading;
|
private final boolean isLoading;
|
||||||
|
private final long id;
|
||||||
|
|
||||||
public Placeholder(boolean isLoading) {
|
public Placeholder(long id, boolean isLoading) {
|
||||||
|
this.id = id;
|
||||||
this.isLoading = isLoading;
|
this.isLoading = isLoading;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isLoading() {
|
public boolean isLoading() {
|
||||||
return isLoading;
|
return isLoading;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public long getViewDataId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public boolean deepEquals(StatusViewData other) {
|
||||||
|
if (!(other instanceof Placeholder)) return false;
|
||||||
|
Placeholder that = (Placeholder) other;
|
||||||
|
return isLoading == that.isLoading && id == that.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
|
||||||
|
Placeholder that = (Placeholder) o;
|
||||||
|
|
||||||
|
return deepEquals(that);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public int hashCode() {
|
||||||
|
int result = (isLoading ? 1 : 0);
|
||||||
|
result = 31 * result + (int) (id ^ (id >>> 32));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Builder {
|
public static class Builder {
|
||||||
|
|
|
@ -1,12 +1,40 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<android.support.v4.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:id="@+id/swipe_refresh_layout"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent">
|
||||||
android:layout_gravity="top">
|
|
||||||
|
|
||||||
<android.support.v7.widget.RecyclerView
|
<android.support.v4.widget.SwipeRefreshLayout
|
||||||
android:id="@+id/recycler_view"
|
android:id="@+id/swipe_refresh_layout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent" />
|
android:layout_height="match_parent">
|
||||||
</android.support.v4.widget.SwipeRefreshLayout>
|
|
||||||
|
<android.support.v7.widget.RecyclerView
|
||||||
|
android:id="@+id/recycler_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
</android.support.v4.widget.SwipeRefreshLayout>
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progress_bar"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/nothing_message"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:drawablePadding="16dp"
|
||||||
|
android:text="@string/footer_empty"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:textSize="?attr/status_text_medium"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
</android.support.constraint.ConstraintLayout>
|
|
@ -1,9 +1,24 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<Button xmlns:android="http://schemas.android.com/apk/res/android"
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:id="@+id/button_load_more"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
style="@style/Widget.AppCompat.Button.Borderless"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="72dp"
|
android:layout_height="wrap_content">
|
||||||
android:text="@string/load_more_placeholder_text"
|
|
||||||
android:textColor="?attr/colorAccent"
|
<Button
|
||||||
android:textSize="?attr/status_text_medium" />
|
android:id="@+id/button_load_more"
|
||||||
|
style="@style/Widget.AppCompat.Button.Borderless"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="72dp"
|
||||||
|
android:text="@string/load_more_placeholder_text"
|
||||||
|
android:textColor="?attr/colorAccent"
|
||||||
|
android:textSize="?attr/status_text_medium" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progress_bar"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible"
|
||||||
|
android:layout_margin="8dp"/>
|
||||||
|
</FrameLayout>
|
|
@ -20,6 +20,8 @@ import android.text.SpannedString
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import com.keylesspalace.tusky.db.AccountEntity
|
import com.keylesspalace.tusky.db.AccountEntity
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
|
import com.keylesspalace.tusky.db.AppDatabase
|
||||||
|
import com.keylesspalace.tusky.db.InstanceDao
|
||||||
import com.keylesspalace.tusky.entity.Account
|
import com.keylesspalace.tusky.entity.Account
|
||||||
import com.keylesspalace.tusky.entity.Emoji
|
import com.keylesspalace.tusky.entity.Emoji
|
||||||
import com.keylesspalace.tusky.entity.Instance
|
import com.keylesspalace.tusky.entity.Instance
|
||||||
|
@ -33,6 +35,7 @@ import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.mockito.Mockito
|
import org.mockito.Mockito
|
||||||
import org.mockito.Mockito.`when`
|
import org.mockito.Mockito.`when`
|
||||||
|
import org.mockito.Mockito.mock
|
||||||
import org.robolectric.Robolectric
|
import org.robolectric.Robolectric
|
||||||
import org.robolectric.RobolectricTestRunner
|
import org.robolectric.RobolectricTestRunner
|
||||||
import org.robolectric.annotation.Config
|
import org.robolectric.annotation.Config
|
||||||
|
@ -50,8 +53,6 @@ import retrofit2.Response
|
||||||
class ComposeActivityTest {
|
class ComposeActivityTest {
|
||||||
|
|
||||||
lateinit var activity: ComposeActivity
|
lateinit var activity: ComposeActivity
|
||||||
lateinit var application: FakeTuskyApplication
|
|
||||||
lateinit var serviceLocator: TuskyApplication.ServiceLocator
|
|
||||||
lateinit var accountManagerMock: AccountManager
|
lateinit var accountManagerMock: AccountManager
|
||||||
lateinit var apiMock: MastodonApi
|
lateinit var apiMock: MastodonApi
|
||||||
|
|
||||||
|
@ -81,9 +82,6 @@ class ComposeActivityTest {
|
||||||
activity = controller.get()
|
activity = controller.get()
|
||||||
|
|
||||||
accountManagerMock = Mockito.mock(AccountManager::class.java)
|
accountManagerMock = Mockito.mock(AccountManager::class.java)
|
||||||
serviceLocator = Mockito.mock(TuskyApplication.ServiceLocator::class.java)
|
|
||||||
`when`(serviceLocator.get(AccountManager::class.java)).thenReturn(accountManagerMock)
|
|
||||||
`when`(accountManagerMock.activeAccount).thenReturn(account)
|
|
||||||
|
|
||||||
apiMock = Mockito.mock(MastodonApi::class.java)
|
apiMock = Mockito.mock(MastodonApi::class.java)
|
||||||
`when`(apiMock.customEmojis).thenReturn(object: Call<List<Emoji>> {
|
`when`(apiMock.customEmojis).thenReturn(object: Call<List<Emoji>> {
|
||||||
|
@ -133,10 +131,13 @@ class ComposeActivityTest {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
val instanceDaoMock = mock(InstanceDao::class.java)
|
||||||
|
val dbMock = mock(AppDatabase::class.java)
|
||||||
|
`when`(dbMock.instanceDao()).thenReturn(instanceDaoMock)
|
||||||
|
|
||||||
activity.mastodonApi = apiMock
|
activity.mastodonApi = apiMock
|
||||||
activity.accountManager = accountManagerMock
|
activity.accountManager = accountManagerMock
|
||||||
application = activity.application as FakeTuskyApplication
|
activity.database = dbMock
|
||||||
application.locator = serviceLocator
|
|
||||||
|
|
||||||
`when`(accountManagerMock.activeAccount).thenReturn(account)
|
`when`(accountManagerMock.activeAccount).thenReturn(account)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
package com.keylesspalace.tusky
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
package com.keylesspalace.tusky.di
|
||||||
|
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -1,4 +1,4 @@
|
||||||
#Wed Oct 25 20:21:12 CEST 2017
|
#Fri Apr 06 21:32:27 MSK 2018
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|
Loading…
Reference in a new issue