Merge branch 'master' into #142/SaveToots
# Conflicts: # app/src/main/java/com/keylesspalace/tusky/TuskyApplication.java
This commit is contained in:
commit
b6d4b388a5
|
@ -53,6 +53,7 @@ dependencies {
|
|||
compile('org.eclipse.paho:org.eclipse.paho.android.service:1.1.1') {
|
||||
exclude module: 'support-v4'
|
||||
}
|
||||
compile 'org.bouncycastle:bcprov-jdk15on:1.57'
|
||||
testCompile 'junit:junit:4.12'
|
||||
|
||||
//room
|
||||
|
|
|
@ -244,7 +244,6 @@ public class AccountActivity extends BaseActivity {
|
|||
String subtitle = String.format(getString(R.string.status_username_format),
|
||||
account.username);
|
||||
getSupportActionBar().setSubtitle(subtitle);
|
||||
|
||||
}
|
||||
|
||||
boolean useCustomTabs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
|
|
@ -92,6 +92,7 @@ import com.keylesspalace.tusky.util.MediaUtils;
|
|||
import com.keylesspalace.tusky.util.MentionTokenizer;
|
||||
import com.keylesspalace.tusky.util.ParserUtils;
|
||||
import com.keylesspalace.tusky.util.SpanUtils;
|
||||
import com.keylesspalace.tusky.util.StringUtils;
|
||||
import com.keylesspalace.tusky.util.ThemeUtils;
|
||||
import com.keylesspalace.tusky.view.EditTextTyped;
|
||||
import com.keylesspalace.tusky.view.RoundedTransformation;
|
||||
|
@ -117,12 +118,6 @@ import retrofit2.Call;
|
|||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
import static com.keylesspalace.tusky.util.MediaUtils.MEDIA_SIZE_UNKNOWN;
|
||||
import static com.keylesspalace.tusky.util.MediaUtils.getMediaSize;
|
||||
import static com.keylesspalace.tusky.util.MediaUtils.inputStreamGetBytes;
|
||||
import static com.keylesspalace.tusky.util.StringUtils.carriageReturn;
|
||||
import static com.keylesspalace.tusky.util.StringUtils.randomAlphanumericString;
|
||||
|
||||
public class ComposeActivity extends BaseActivity implements ComposeOptionsFragment.Listener, ParserUtils.ParserListener {
|
||||
private static final String TAG = "ComposeActivity"; // logging tag
|
||||
private static final int STATUS_CHARACTER_LIMIT = 500;
|
||||
|
@ -260,11 +255,13 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm
|
|||
if (previousInputContentInfo != null) {
|
||||
onCommitContentInternal(previousInputContentInfo, previousFlags);
|
||||
}
|
||||
photoUploadUri = savedInstanceState.getParcelable("photoUploadUri");
|
||||
} else {
|
||||
showMarkSensitive = false;
|
||||
startingVisibility = preferences.getString("rememberedVisibility", "public");
|
||||
statusMarkSensitive = false;
|
||||
startingHideText = false;
|
||||
photoUploadUri = null;
|
||||
}
|
||||
|
||||
/* If the composer is started up as a reply to another post, override the "starting" state
|
||||
|
@ -435,7 +432,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm
|
|||
}
|
||||
}
|
||||
for (Uri uri : uriList) {
|
||||
long mediaSize = getMediaSize(getContentResolver(), uri);
|
||||
long mediaSize = MediaUtils.getMediaSize(getContentResolver(), uri);
|
||||
pickMedia(uri, mediaSize);
|
||||
}
|
||||
} else if (type.equals("text/plain")) {
|
||||
|
@ -477,6 +474,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm
|
|||
}
|
||||
currentInputContentInfo = null;
|
||||
currentFlags = 0;
|
||||
outState.putParcelable("photoUploadUri", photoUploadUri);
|
||||
super.onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
|
@ -732,7 +730,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm
|
|||
// Just eat this exception.
|
||||
}
|
||||
} else {
|
||||
mediaSize = MEDIA_SIZE_UNKNOWN;
|
||||
mediaSize = MediaUtils.MEDIA_SIZE_UNKNOWN;
|
||||
}
|
||||
pickMedia(uri, mediaSize);
|
||||
|
||||
|
@ -875,7 +873,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm
|
|||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[],
|
||||
@NonNull int[] grantResults) {
|
||||
@NonNull int[] grantResults) {
|
||||
switch (requestCode) {
|
||||
case PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE: {
|
||||
if (grantResults.length > 0
|
||||
|
@ -895,6 +893,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm
|
|||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private File createNewImageFile() throws IOException {
|
||||
// Create an image file name
|
||||
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date());
|
||||
|
@ -1073,7 +1072,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm
|
|||
final String filename = String.format("%s_%s_%s.%s",
|
||||
getString(R.string.app_name),
|
||||
String.valueOf(new Date().getTime()),
|
||||
randomAlphanumericString(10),
|
||||
StringUtils.randomAlphanumericString(10),
|
||||
fileExtension);
|
||||
|
||||
byte[] content = item.content;
|
||||
|
@ -1088,7 +1087,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm
|
|||
return;
|
||||
}
|
||||
|
||||
content = inputStreamGetBytes(stream);
|
||||
content = MediaUtils.inputStreamGetBytes(stream);
|
||||
IOUtils.closeQuietly(stream);
|
||||
|
||||
if (content == null) {
|
||||
|
@ -1114,8 +1113,8 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm
|
|||
|
||||
@Override
|
||||
public void onFailure(Call<Media> call, Throwable t) {
|
||||
Log.d(TAG, t.getMessage());
|
||||
onUploadFailure(item, false);
|
||||
Log.d(TAG, "Upload request failed. " + t.getMessage());
|
||||
onUploadFailure(item, call.isCanceled());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -1149,7 +1148,10 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm
|
|||
if (finishingUploadDialog != null) {
|
||||
finishingUploadDialog.cancel();
|
||||
}
|
||||
removeMediaFromQueue(item);
|
||||
if (!isCanceled) {
|
||||
// If it is canceled, it's already been removed, otherwise do it.
|
||||
removeMediaFromQueue(item);
|
||||
}
|
||||
}
|
||||
|
||||
private void cancelReadyingMedia(QueuedMedia item) {
|
||||
|
@ -1166,19 +1168,19 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm
|
|||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (requestCode == MEDIA_PICK_RESULT && resultCode == RESULT_OK && data != null) {
|
||||
if (resultCode == RESULT_OK && requestCode == MEDIA_PICK_RESULT && data != null) {
|
||||
Uri uri = data.getData();
|
||||
long mediaSize = getMediaSize(getContentResolver(), uri);
|
||||
long mediaSize = MediaUtils.getMediaSize(getContentResolver(), uri);
|
||||
pickMedia(uri, mediaSize);
|
||||
} else if (requestCode == MEDIA_TAKE_PHOTO_RESULT && resultCode == RESULT_OK) {
|
||||
long mediaSize = getMediaSize(getContentResolver(), photoUploadUri);
|
||||
} else if (resultCode == RESULT_OK && requestCode == MEDIA_TAKE_PHOTO_RESULT) {
|
||||
long mediaSize = MediaUtils.getMediaSize(getContentResolver(), photoUploadUri);
|
||||
pickMedia(photoUploadUri, mediaSize);
|
||||
}
|
||||
}
|
||||
|
||||
private void pickMedia(Uri uri, long mediaSize) {
|
||||
ContentResolver contentResolver = getContentResolver();
|
||||
if (mediaSize == MEDIA_SIZE_UNKNOWN) {
|
||||
if (mediaSize == MediaUtils.MEDIA_SIZE_UNKNOWN) {
|
||||
displayTransientError(R.string.error_media_upload_opening);
|
||||
return;
|
||||
}
|
||||
|
@ -1280,7 +1282,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm
|
|||
if (!TextUtils.isEmpty(headerInfo.title)) {
|
||||
cleanBaseUrl(headerInfo);
|
||||
textEditor.append(headerInfo.title);
|
||||
textEditor.append(carriageReturn);
|
||||
textEditor.append(StringUtils.carriageReturn);
|
||||
textEditor.append(headerInfo.baseUrl);
|
||||
}
|
||||
if (!TextUtils.isEmpty(headerInfo.image)) {
|
||||
|
@ -1299,7 +1301,8 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm
|
|||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
long mediaSize = getMediaSize(getContentResolver(), headerInfo);
|
||||
long mediaSize = MediaUtils.getMediaSize(getContentResolver(),
|
||||
headerInfo);
|
||||
pickMedia(headerInfo, mediaSize);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -170,7 +170,8 @@ public class EditProfileActivity extends BaseActivity {
|
|||
Account me = response.body();
|
||||
priorDisplayName = me.getDisplayName();
|
||||
priorNote = me.note.toString();
|
||||
CircularImageView avatar = (CircularImageView) findViewById(R.id.edit_profile_avatar_preview);
|
||||
CircularImageView avatar =
|
||||
(CircularImageView) findViewById(R.id.edit_profile_avatar_preview);
|
||||
ImageView header = (ImageView) findViewById(R.id.edit_profile_header_preview);
|
||||
|
||||
displayNameEditText.setText(priorDisplayName);
|
||||
|
|
|
@ -18,14 +18,20 @@ package com.keylesspalace.tusky;
|
|||
import android.app.Application;
|
||||
import android.arch.persistence.room.Room;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import com.jakewharton.picasso.OkHttp3Downloader;
|
||||
import com.keylesspalace.tusky.db.AppDatabase;
|
||||
import com.keylesspalace.tusky.util.OkHttpUtils;
|
||||
import com.squareup.picasso.Picasso;
|
||||
|
||||
public class TuskyApplication extends Application {
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
|
||||
import java.security.Provider;
|
||||
import java.security.Security;
|
||||
|
||||
public class TuskyApplication extends Application {
|
||||
private static final String TAG = "TuskyApplication"; // logging tag
|
||||
private static AppDatabase db;
|
||||
|
||||
public static AppDatabase getDB() {
|
||||
|
@ -57,6 +63,36 @@ public class TuskyApplication extends Application {
|
|||
Picasso.with(this).setLoggingEnabled(true);
|
||||
}
|
||||
|
||||
|
||||
/* Install the new provider or, if there's a pre-existing older version, replace the
|
||||
* existing version of it. */
|
||||
final String providerName = "BC";
|
||||
Provider existingProvider = Security.getProvider(providerName);
|
||||
if (existingProvider == null) {
|
||||
try {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
} catch (SecurityException e) {
|
||||
Log.e(TAG, "Permission to add the security provider was denied.");
|
||||
}
|
||||
} else {
|
||||
Provider replacement = new BouncyCastleProvider();
|
||||
if (existingProvider.getVersion() < replacement.getVersion()) {
|
||||
Provider[] providers = Security.getProviders();
|
||||
int priority = 1;
|
||||
for (int i = 0; i < providers.length; i++) {
|
||||
if (providers[i].getName().equals(providerName)) {
|
||||
priority = i + 1;
|
||||
}
|
||||
}
|
||||
try {
|
||||
Security.removeProvider(providerName);
|
||||
Security.insertProviderAt(replacement, priority);
|
||||
} catch (SecurityException e) {
|
||||
Log.e(TAG, "Permission to update a security provider was denied.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db = Room.databaseBuilder(getApplicationContext(),
|
||||
AppDatabase.class, "tuskyDB").allowMainThreadQueries().build();
|
||||
}
|
||||
|
|
|
@ -22,16 +22,22 @@ import com.keylesspalace.tusky.entity.Account;
|
|||
import com.keylesspalace.tusky.interfaces.AccountActionListener;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class AccountAdapter extends RecyclerView.Adapter {
|
||||
List<Account> accountList;
|
||||
AccountActionListener accountActionListener;
|
||||
FooterViewHolder.State footerState;
|
||||
|
||||
private String topId;
|
||||
private String bottomId;
|
||||
|
||||
AccountAdapter(AccountActionListener accountActionListener) {
|
||||
super();
|
||||
accountList = new ArrayList<>();
|
||||
this.accountActionListener = accountActionListener;
|
||||
footerState = FooterViewHolder.State.END;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -39,12 +45,20 @@ public abstract class AccountAdapter extends RecyclerView.Adapter {
|
|||
return accountList.size() + 1;
|
||||
}
|
||||
|
||||
public void update(List<Account> newAccounts) {
|
||||
public void update(@Nullable List<Account> newAccounts, @Nullable String fromId,
|
||||
@Nullable String uptoId) {
|
||||
if (newAccounts == null || newAccounts.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (fromId != null) {
|
||||
bottomId = fromId;
|
||||
}
|
||||
if (uptoId != null) {
|
||||
topId = uptoId;
|
||||
}
|
||||
if (accountList.isEmpty()) {
|
||||
accountList = newAccounts;
|
||||
// This construction removes duplicates.
|
||||
accountList = new ArrayList<>(new HashSet<>(newAccounts));
|
||||
} else {
|
||||
int index = accountList.indexOf(newAccounts.get(newAccounts.size() - 1));
|
||||
for (int i = 0; i < index; i++) {
|
||||
|
@ -60,10 +74,25 @@ public abstract class AccountAdapter extends RecyclerView.Adapter {
|
|||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void addItems(List<Account> newAccounts) {
|
||||
public void addItems(List<Account> newAccounts, @Nullable String fromId) {
|
||||
if (fromId != null) {
|
||||
bottomId = fromId;
|
||||
}
|
||||
int end = accountList.size();
|
||||
accountList.addAll(newAccounts);
|
||||
notifyItemRangeInserted(end, newAccounts.size());
|
||||
Account last = accountList.get(end - 1);
|
||||
if (last != null && !findAccount(newAccounts, last.id)) {
|
||||
accountList.addAll(newAccounts);
|
||||
notifyItemRangeInserted(end, newAccounts.size());
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean findAccount(List<Account> accounts, String id) {
|
||||
for (Account account : accounts) {
|
||||
if (account.id.equals(id)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
@ -84,10 +113,25 @@ public abstract class AccountAdapter extends RecyclerView.Adapter {
|
|||
notifyItemInserted(position);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Account getItem(int position) {
|
||||
if (position >= 0 && position < accountList.size()) {
|
||||
return accountList.get(position);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void setFooterState(FooterViewHolder.State newFooterState) {
|
||||
footerState = newFooterState;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getBottomId() {
|
||||
return bottomId;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getTopId() {
|
||||
return topId;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ class AccountViewHolder extends RecyclerView.ViewHolder {
|
|||
private TextView username;
|
||||
private TextView displayName;
|
||||
private CircularImageView avatar;
|
||||
private String id;
|
||||
private String accountId;
|
||||
|
||||
AccountViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
|
@ -28,7 +28,7 @@ class AccountViewHolder extends RecyclerView.ViewHolder {
|
|||
}
|
||||
|
||||
void setupWithAccount(Account account) {
|
||||
id = account.id;
|
||||
accountId = account.id;
|
||||
String format = username.getContext().getString(R.string.status_username_format);
|
||||
String formattedUsername = String.format(format, account.username);
|
||||
username.setText(formattedUsername);
|
||||
|
@ -45,7 +45,7 @@ class AccountViewHolder extends RecyclerView.ViewHolder {
|
|||
container.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
listener.onViewAccount(id);
|
||||
listener.onViewAccount(accountId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ class AccountViewHolder extends RecyclerView.ViewHolder {
|
|||
container.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
listener.onViewAccount(id);
|
||||
listener.onViewAccount(accountId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -59,6 +59,9 @@ public class BlocksAdapter extends AccountAdapter {
|
|||
BlockedUserViewHolder holder = (BlockedUserViewHolder) viewHolder;
|
||||
holder.setupWithAccount(accountList.get(position));
|
||||
holder.setupActionListener(accountActionListener, true);
|
||||
} else {
|
||||
FooterViewHolder holder = (FooterViewHolder) viewHolder;
|
||||
holder.setState(footerState);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -55,6 +55,9 @@ public class FollowAdapter extends AccountAdapter {
|
|||
AccountViewHolder holder = (AccountViewHolder) viewHolder;
|
||||
holder.setupWithAccount(accountList.get(position));
|
||||
holder.setupActionListener(accountActionListener);
|
||||
} else {
|
||||
FooterViewHolder holder = (FooterViewHolder) viewHolder;
|
||||
holder.setState(footerState);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -59,6 +59,9 @@ public class FollowRequestsAdapter extends AccountAdapter {
|
|||
FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder;
|
||||
holder.setupWithAccount(accountList.get(position));
|
||||
holder.setupActionListener(accountActionListener);
|
||||
} else {
|
||||
FooterViewHolder holder = (FooterViewHolder) viewHolder;
|
||||
holder.setState(footerState);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -15,18 +15,69 @@
|
|||
|
||||
package com.keylesspalace.tusky.adapter;
|
||||
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.support.v7.content.res.AppCompatResources;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.View;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import android.support.v7.widget.RecyclerView.LayoutParams;
|
||||
|
||||
import com.keylesspalace.tusky.R;
|
||||
|
||||
class FooterViewHolder extends RecyclerView.ViewHolder {
|
||||
public class FooterViewHolder extends RecyclerView.ViewHolder {
|
||||
public enum State {
|
||||
EMPTY,
|
||||
END,
|
||||
LOADING
|
||||
}
|
||||
|
||||
private View container;
|
||||
private ProgressBar progressBar;
|
||||
private TextView endMessage;
|
||||
|
||||
FooterViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
ProgressBar progressBar = (ProgressBar) itemView.findViewById(R.id.footer_progress_bar);
|
||||
if (progressBar != null) {
|
||||
progressBar.setIndeterminate(true);
|
||||
container = itemView.findViewById(R.id.footer_container);
|
||||
progressBar = (ProgressBar) itemView.findViewById(R.id.footer_progress_bar);
|
||||
endMessage = (TextView) itemView.findViewById(R.id.footer_end_message);
|
||||
Drawable top = AppCompatResources.getDrawable(itemView.getContext(),
|
||||
R.drawable.elephant_friend);
|
||||
if (top != null) {
|
||||
top.setBounds(0, 0, top.getIntrinsicWidth() / 2, top.getIntrinsicHeight() / 2);
|
||||
}
|
||||
endMessage.setCompoundDrawables(null, top, null, null);
|
||||
}
|
||||
|
||||
public void setState(State state) {
|
||||
switch (state) {
|
||||
case LOADING: {
|
||||
RecyclerView.LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT,
|
||||
LayoutParams.MATCH_PARENT);
|
||||
container.setLayoutParams(layoutParams);
|
||||
container.setVisibility(View.VISIBLE);
|
||||
progressBar.setVisibility(View.VISIBLE);
|
||||
endMessage.setVisibility(View.GONE);
|
||||
break;
|
||||
}
|
||||
case END: {
|
||||
RecyclerView.LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT,
|
||||
LayoutParams.WRAP_CONTENT);
|
||||
container.setLayoutParams(layoutParams);
|
||||
container.setVisibility(View.GONE);
|
||||
progressBar.setVisibility(View.GONE);
|
||||
endMessage.setVisibility(View.GONE);
|
||||
break;
|
||||
}
|
||||
case EMPTY: {
|
||||
RecyclerView.LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT,
|
||||
LayoutParams.MATCH_PARENT);
|
||||
container.setLayoutParams(layoutParams);
|
||||
container.setVisibility(View.VISIBLE);
|
||||
progressBar.setVisibility(View.GONE);
|
||||
endMessage.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -44,6 +44,9 @@ public class MutesAdapter extends AccountAdapter {
|
|||
MutedUserViewHolder holder = (MutedUserViewHolder) viewHolder;
|
||||
holder.setupWithAccount(accountList.get(position));
|
||||
holder.setupActionListener(accountActionListener, true, position);
|
||||
} else {
|
||||
FooterViewHolder holder = (FooterViewHolder) viewHolder;
|
||||
holder.setState(footerState);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -37,6 +37,7 @@ import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
|||
import com.squareup.picasso.Picasso;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
public class NotificationsAdapter extends RecyclerView.Adapter implements AdapterItemRemover {
|
||||
|
@ -45,17 +46,13 @@ public class NotificationsAdapter extends RecyclerView.Adapter implements Adapte
|
|||
private static final int VIEW_TYPE_STATUS_NOTIFICATION = 2;
|
||||
private static final int VIEW_TYPE_FOLLOW = 3;
|
||||
|
||||
public enum FooterState {
|
||||
EMPTY,
|
||||
END,
|
||||
LOADING
|
||||
}
|
||||
|
||||
private List<Notification> notifications;
|
||||
private StatusActionListener statusListener;
|
||||
private NotificationActionListener notificationActionListener;
|
||||
private FooterState footerState = FooterState.END;
|
||||
private FooterViewHolder.State footerState;
|
||||
private boolean mediaPreviewEnabled;
|
||||
private String bottomId;
|
||||
private String topId;
|
||||
|
||||
public NotificationsAdapter(StatusActionListener statusListener,
|
||||
NotificationActionListener notificationActionListener) {
|
||||
|
@ -63,6 +60,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter implements Adapte
|
|||
notifications = new ArrayList<>();
|
||||
this.statusListener = statusListener;
|
||||
this.notificationActionListener = notificationActionListener;
|
||||
footerState = FooterViewHolder.State.END;
|
||||
mediaPreviewEnabled = true;
|
||||
}
|
||||
|
||||
|
@ -76,24 +74,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter implements Adapte
|
|||
return new StatusViewHolder(view);
|
||||
}
|
||||
case VIEW_TYPE_FOOTER: {
|
||||
View view;
|
||||
switch (footerState) {
|
||||
default:
|
||||
case LOADING:
|
||||
view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_footer, parent, false);
|
||||
break;
|
||||
case END: {
|
||||
view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_footer_end, parent, false);
|
||||
break;
|
||||
}
|
||||
case EMPTY: {
|
||||
view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_footer_empty, parent, false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_footer, parent, false);
|
||||
return new FooterViewHolder(view);
|
||||
}
|
||||
case VIEW_TYPE_STATUS_NOTIFICATION: {
|
||||
|
@ -137,6 +119,9 @@ public class NotificationsAdapter extends RecyclerView.Adapter implements Adapte
|
|||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
FooterViewHolder holder = (FooterViewHolder) viewHolder;
|
||||
holder.setState(footerState);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -186,19 +171,28 @@ public class NotificationsAdapter extends RecyclerView.Adapter implements Adapte
|
|||
}
|
||||
}
|
||||
|
||||
public @Nullable Notification getItem(int position) {
|
||||
@Nullable
|
||||
public Notification getItem(int position) {
|
||||
if (position >= 0 && position < notifications.size()) {
|
||||
return notifications.get(position);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void update(List<Notification> newNotifications) {
|
||||
public void update(@Nullable List<Notification> newNotifications, @Nullable String fromId,
|
||||
@Nullable String uptoId) {
|
||||
if (newNotifications == null || newNotifications.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (fromId != null) {
|
||||
bottomId = fromId;
|
||||
}
|
||||
if (uptoId != null) {
|
||||
topId = uptoId;
|
||||
}
|
||||
if (notifications.isEmpty()) {
|
||||
notifications = newNotifications;
|
||||
// This construction removes duplicates.
|
||||
notifications = new ArrayList<>(new HashSet<>(newNotifications));
|
||||
} else {
|
||||
int index = notifications.indexOf(newNotifications.get(newNotifications.size() - 1));
|
||||
for (int i = 0; i < index; i++) {
|
||||
|
@ -214,10 +208,25 @@ public class NotificationsAdapter extends RecyclerView.Adapter implements Adapte
|
|||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void addItems(List<Notification> new_notifications) {
|
||||
public void addItems(List<Notification> newNotifications, @Nullable String fromId) {
|
||||
if (fromId != null) {
|
||||
bottomId = fromId;
|
||||
}
|
||||
int end = notifications.size();
|
||||
notifications.addAll(new_notifications);
|
||||
notifyItemRangeInserted(end, new_notifications.size());
|
||||
Notification last = notifications.get(end - 1);
|
||||
if (last != null && !findNotification(newNotifications, last.id)) {
|
||||
notifications.addAll(newNotifications);
|
||||
notifyItemRangeInserted(end, newNotifications.size());
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean findNotification(List<Notification> notifications, String id) {
|
||||
for (Notification notification : notifications) {
|
||||
if (notification.id.equals(id)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
|
@ -225,12 +234,18 @@ public class NotificationsAdapter extends RecyclerView.Adapter implements Adapte
|
|||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void setFooterState(FooterState newFooterState) {
|
||||
FooterState oldValue = footerState;
|
||||
public void setFooterState(FooterViewHolder.State newFooterState) {
|
||||
footerState = newFooterState;
|
||||
if (footerState != oldValue) {
|
||||
notifyItemChanged(notifications.size());
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getBottomId() {
|
||||
return bottomId;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getTopId() {
|
||||
return topId;
|
||||
}
|
||||
|
||||
public void setMediaPreviewEnabled(boolean enabled) {
|
||||
|
|
|
@ -425,8 +425,8 @@ class StatusViewHolder extends RecyclerView.ViewHolder {
|
|||
container.setOnClickListener(viewThreadListener);
|
||||
}
|
||||
|
||||
void setupWithStatus(Status status, StatusActionListener listener,
|
||||
boolean mediaPreviewEnabled) {
|
||||
void setupWithStatus(Status status, final StatusActionListener listener,
|
||||
boolean mediaPreviewEnabled) {
|
||||
Status realStatus = status.getActionableStatus();
|
||||
|
||||
setDisplayName(realStatus.account.getDisplayName());
|
||||
|
@ -474,5 +474,15 @@ class StatusViewHolder extends RecyclerView.ViewHolder {
|
|||
} else {
|
||||
setSpoilerText(realStatus.spoilerText);
|
||||
}
|
||||
|
||||
// I think it's not efficient to create new object every time we bind a holder.
|
||||
// More efficient approach would be creating View.OnClickListener during holder creation
|
||||
// and storing StatusActionListener in a variable after binding.
|
||||
rebloggedBar.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
listener.onOpenReblog(getAdapterPosition());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -103,7 +103,7 @@ public class ThreadAdapter extends RecyclerView.Adapter implements AdapterItemRe
|
|||
// In case of refresh, remove old ancestors and descendants first. We'll remove all blindly,
|
||||
// as we have no guarantee on their order to be the same as before
|
||||
int oldSize = statuses.size();
|
||||
if (oldSize > 0) {
|
||||
if (oldSize > 1) {
|
||||
mainStatus = statuses.get(statusIndex);
|
||||
statuses.clear();
|
||||
notifyItemRangeRemoved(0, oldSize);
|
||||
|
|
|
@ -27,27 +27,25 @@ import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
|||
import com.keylesspalace.tusky.entity.Status;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
public class TimelineAdapter extends RecyclerView.Adapter implements AdapterItemRemover {
|
||||
private static final int VIEW_TYPE_STATUS = 0;
|
||||
private static final int VIEW_TYPE_FOOTER = 1;
|
||||
|
||||
public enum FooterState {
|
||||
EMPTY,
|
||||
END,
|
||||
LOADING
|
||||
}
|
||||
|
||||
private List<Status> statuses;
|
||||
private StatusActionListener statusListener;
|
||||
private FooterState footerState = FooterState.END;
|
||||
private FooterViewHolder.State footerState;
|
||||
private boolean mediaPreviewEnabled;
|
||||
private String topId;
|
||||
private String bottomId;
|
||||
|
||||
public TimelineAdapter(StatusActionListener statusListener) {
|
||||
super();
|
||||
statuses = new ArrayList<>();
|
||||
this.statusListener = statusListener;
|
||||
footerState = FooterViewHolder.State.END;
|
||||
mediaPreviewEnabled = true;
|
||||
}
|
||||
|
||||
|
@ -61,24 +59,8 @@ public class TimelineAdapter extends RecyclerView.Adapter implements AdapterItem
|
|||
return new StatusViewHolder(view);
|
||||
}
|
||||
case VIEW_TYPE_FOOTER: {
|
||||
View view;
|
||||
switch (footerState) {
|
||||
default:
|
||||
case LOADING:
|
||||
view = LayoutInflater.from(viewGroup.getContext())
|
||||
.inflate(R.layout.item_footer, viewGroup, false);
|
||||
break;
|
||||
case END: {
|
||||
view = LayoutInflater.from(viewGroup.getContext())
|
||||
.inflate(R.layout.item_footer_end, viewGroup, false);
|
||||
break;
|
||||
}
|
||||
case EMPTY: {
|
||||
view = LayoutInflater.from(viewGroup.getContext())
|
||||
.inflate(R.layout.item_footer_empty, viewGroup, false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
View view = LayoutInflater.from(viewGroup.getContext())
|
||||
.inflate(R.layout.item_footer, viewGroup, false);
|
||||
return new FooterViewHolder(view);
|
||||
}
|
||||
}
|
||||
|
@ -90,6 +72,9 @@ public class TimelineAdapter extends RecyclerView.Adapter implements AdapterItem
|
|||
StatusViewHolder holder = (StatusViewHolder) viewHolder;
|
||||
Status status = statuses.get(position);
|
||||
holder.setupWithStatus(status, statusListener, mediaPreviewEnabled);
|
||||
} else {
|
||||
FooterViewHolder holder = (FooterViewHolder) viewHolder;
|
||||
holder.setState(footerState);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -126,12 +111,20 @@ public class TimelineAdapter extends RecyclerView.Adapter implements AdapterItem
|
|||
}
|
||||
}
|
||||
|
||||
public void update(List<Status> newStatuses) {
|
||||
public void update(@Nullable List<Status> newStatuses, @Nullable String fromId,
|
||||
@Nullable String uptoId) {
|
||||
if (newStatuses == null || newStatuses.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (fromId != null) {
|
||||
bottomId = fromId;
|
||||
}
|
||||
if (uptoId != null) {
|
||||
topId = uptoId;
|
||||
}
|
||||
if (statuses.isEmpty()) {
|
||||
statuses = newStatuses;
|
||||
// This construction removes duplicates.
|
||||
statuses = new ArrayList<>(new HashSet<>(newStatuses));
|
||||
} else {
|
||||
int index = statuses.indexOf(newStatuses.get(newStatuses.size() - 1));
|
||||
for (int i = 0; i < index; i++) {
|
||||
|
@ -147,10 +140,25 @@ public class TimelineAdapter extends RecyclerView.Adapter implements AdapterItem
|
|||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void addItems(List<Status> newStatuses) {
|
||||
public void addItems(List<Status> newStatuses, @Nullable String fromId) {
|
||||
if (fromId != null) {
|
||||
bottomId = fromId;
|
||||
}
|
||||
int end = statuses.size();
|
||||
statuses.addAll(newStatuses);
|
||||
notifyItemRangeInserted(end, newStatuses.size());
|
||||
Status last = statuses.get(end - 1);
|
||||
if (last != null && !findStatus(newStatuses, last.id)) {
|
||||
statuses.addAll(newStatuses);
|
||||
notifyItemRangeInserted(end, newStatuses.size());
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean findStatus(List<Status> statuses, String id) {
|
||||
for (Status status : statuses) {
|
||||
if (status.id.equals(id)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
|
@ -166,8 +174,8 @@ public class TimelineAdapter extends RecyclerView.Adapter implements AdapterItem
|
|||
return null;
|
||||
}
|
||||
|
||||
public void setFooterState(FooterState newFooterState) {
|
||||
FooterState oldValue = footerState;
|
||||
public void setFooterState(FooterViewHolder.State newFooterState) {
|
||||
FooterViewHolder.State oldValue = footerState;
|
||||
footerState = newFooterState;
|
||||
if (footerState != oldValue) {
|
||||
notifyItemChanged(statuses.size());
|
||||
|
@ -177,4 +185,14 @@ public class TimelineAdapter extends RecyclerView.Adapter implements AdapterItem
|
|||
public void setMediaPreviewEnabled(boolean enabled) {
|
||||
mediaPreviewEnabled = enabled;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getBottomId() {
|
||||
return bottomId;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getTopId() {
|
||||
return topId;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@ import com.keylesspalace.tusky.adapter.AccountAdapter;
|
|||
import com.keylesspalace.tusky.adapter.BlocksAdapter;
|
||||
import com.keylesspalace.tusky.adapter.FollowAdapter;
|
||||
import com.keylesspalace.tusky.adapter.FollowRequestsAdapter;
|
||||
import com.keylesspalace.tusky.adapter.FooterViewHolder;
|
||||
import com.keylesspalace.tusky.adapter.MutesAdapter;
|
||||
import com.keylesspalace.tusky.BaseActivity;
|
||||
import com.keylesspalace.tusky.entity.Account;
|
||||
|
@ -43,6 +44,7 @@ import com.keylesspalace.tusky.entity.Relationship;
|
|||
import com.keylesspalace.tusky.interfaces.AccountActionListener;
|
||||
import com.keylesspalace.tusky.network.MastodonApi;
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.util.HttpHeaderLink;
|
||||
import com.keylesspalace.tusky.util.ThemeUtils;
|
||||
import com.keylesspalace.tusky.view.EndlessOnScrollListener;
|
||||
|
||||
|
@ -71,6 +73,10 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
|
|||
private AccountAdapter adapter;
|
||||
private TabLayout.OnTabSelectedListener onTabSelectedListener;
|
||||
private MastodonApi api;
|
||||
private boolean bottomLoading;
|
||||
private int bottomFetches;
|
||||
private boolean topLoading;
|
||||
private int topFetches;
|
||||
|
||||
public static AccountListFragment newInstance(Type type) {
|
||||
Bundle arguments = new Bundle();
|
||||
|
@ -160,13 +166,7 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
|
|||
scrollListener = new EndlessOnScrollListener(layoutManager) {
|
||||
@Override
|
||||
public void onLoadMore(int page, int totalItemsCount, RecyclerView view) {
|
||||
AccountAdapter adapter = (AccountAdapter) view.getAdapter();
|
||||
Account account = adapter.getItem(adapter.getItemCount() - 2);
|
||||
if (account != null) {
|
||||
fetchAccounts(account.id, null);
|
||||
} else {
|
||||
fetchAccounts();
|
||||
}
|
||||
AccountListFragment.this.onLoadMore(view);
|
||||
}
|
||||
};
|
||||
recyclerView.addOnScrollListener(scrollListener);
|
||||
|
@ -181,78 +181,6 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
|
|||
super.onDestroyView();
|
||||
}
|
||||
|
||||
private void fetchAccounts(final String fromId, String uptoId) {
|
||||
Callback<List<Account>> cb = new Callback<List<Account>>() {
|
||||
@Override
|
||||
public void onResponse(Call<List<Account>> call, Response<List<Account>> response) {
|
||||
if (response.isSuccessful()) {
|
||||
onFetchAccountsSuccess(response.body(), fromId);
|
||||
} else {
|
||||
onFetchAccountsFailure(new Exception(response.message()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<List<Account>> call, Throwable t) {
|
||||
onFetchAccountsFailure((Exception) t);
|
||||
}
|
||||
};
|
||||
|
||||
Call<List<Account>> listCall;
|
||||
switch (type) {
|
||||
default:
|
||||
case FOLLOWS: {
|
||||
listCall = api.accountFollowing(accountId, fromId, uptoId, null);
|
||||
break;
|
||||
}
|
||||
case FOLLOWERS: {
|
||||
listCall = api.accountFollowers(accountId, fromId, uptoId, null);
|
||||
break;
|
||||
}
|
||||
case BLOCKS: {
|
||||
listCall = api.blocks(fromId, uptoId, null);
|
||||
break;
|
||||
}
|
||||
case MUTES: {
|
||||
listCall = api.mutes(fromId, uptoId, null);
|
||||
break;
|
||||
}
|
||||
case FOLLOW_REQUESTS: {
|
||||
listCall = api.followRequests(fromId, uptoId, null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
callList.add(listCall);
|
||||
listCall.enqueue(cb);
|
||||
}
|
||||
|
||||
private void fetchAccounts() {
|
||||
fetchAccounts(null, null);
|
||||
}
|
||||
|
||||
private static boolean findAccount(List<Account> accounts, String id) {
|
||||
for (Account account : accounts) {
|
||||
if (account.id.equals(id)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void onFetchAccountsSuccess(List<Account> accounts, String fromId) {
|
||||
if (fromId != null) {
|
||||
if (accounts.size() > 0 && !findAccount(accounts, fromId)) {
|
||||
adapter.addItems(accounts);
|
||||
}
|
||||
} else {
|
||||
adapter.update(accounts);
|
||||
}
|
||||
}
|
||||
|
||||
private void onFetchAccountsFailure(Exception exception) {
|
||||
Log.e(TAG, "Fetch failure: " + exception.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewAccount(String id) {
|
||||
Intent intent = new Intent(getContext(), AccountActivity.class);
|
||||
|
@ -431,7 +359,12 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
|
|||
}
|
||||
|
||||
private void onRespondToFollowRequestFailure(boolean accept, String accountId) {
|
||||
String verb = (accept) ? "accept" : "reject";
|
||||
String verb;
|
||||
if (accept) {
|
||||
verb = "accept";
|
||||
} else {
|
||||
verb = "reject";
|
||||
}
|
||||
String message = String.format("Failed to %s account id %s.", verb, accountId);
|
||||
Log.e(TAG, message);
|
||||
}
|
||||
|
@ -444,4 +377,143 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
|
|||
layoutManager.scrollToPositionWithOffset(0, 0);
|
||||
scrollListener.reset();
|
||||
}
|
||||
|
||||
private enum FetchEnd {
|
||||
TOP,
|
||||
BOTTOM
|
||||
}
|
||||
|
||||
private Call<List<Account>> getFetchCallByListType(Type type, String fromId, String uptoId) {
|
||||
switch (type) {
|
||||
default:
|
||||
case FOLLOWS: return api.accountFollowing(accountId, fromId, uptoId, null);
|
||||
case FOLLOWERS: return api.accountFollowers(accountId, fromId, uptoId, null);
|
||||
case BLOCKS: return api.blocks(fromId, uptoId, null);
|
||||
case MUTES: return api.mutes(fromId, uptoId, null);
|
||||
case FOLLOW_REQUESTS: return api.followRequests(fromId, uptoId, null);
|
||||
}
|
||||
}
|
||||
|
||||
private void fetchAccounts(String fromId, String uptoId, final FetchEnd fetchEnd) {
|
||||
/* If there is a fetch already ongoing, record however many fetches are requested and
|
||||
* fulfill them after it's complete. */
|
||||
if (fetchEnd == FetchEnd.TOP && topLoading) {
|
||||
topFetches++;
|
||||
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(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
adapter.setFooterState(FooterViewHolder.State.LOADING);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Callback<List<Account>> cb = new Callback<List<Account>>() {
|
||||
@Override
|
||||
public void onResponse(Call<List<Account>> call, Response<List<Account>> response) {
|
||||
if (response.isSuccessful()) {
|
||||
String linkHeader = response.headers().get("Link");
|
||||
onFetchAccountsSuccess(response.body(), linkHeader, fetchEnd);
|
||||
} else {
|
||||
onFetchAccountsFailure(new Exception(response.message()), fetchEnd);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<List<Account>> call, Throwable t) {
|
||||
onFetchAccountsFailure((Exception) t, fetchEnd);
|
||||
}
|
||||
};
|
||||
Call<List<Account>> listCall = getFetchCallByListType(type, fromId, uptoId);
|
||||
callList.add(listCall);
|
||||
listCall.enqueue(cb);
|
||||
}
|
||||
|
||||
private void onFetchAccountsSuccess(List<Account> accounts, String linkHeader,
|
||||
FetchEnd fetchEnd) {
|
||||
List<HttpHeaderLink> links = HttpHeaderLink.parse(linkHeader);
|
||||
switch (fetchEnd) {
|
||||
case TOP: {
|
||||
HttpHeaderLink previous = HttpHeaderLink.findByRelationType(links, "prev");
|
||||
String uptoId = null;
|
||||
if (previous != null) {
|
||||
uptoId = previous.uri.getQueryParameter("since_id");
|
||||
}
|
||||
adapter.update(accounts, null, uptoId);
|
||||
break;
|
||||
}
|
||||
case BOTTOM: {
|
||||
HttpHeaderLink next = HttpHeaderLink.findByRelationType(links, "next");
|
||||
String fromId = null;
|
||||
if (next != null) {
|
||||
fromId = next.uri.getQueryParameter("max_id");
|
||||
}
|
||||
if (adapter.getItemCount() > 1) {
|
||||
adapter.addItems(accounts, fromId);
|
||||
} else {
|
||||
/* If this is the first fetch, also save the id from the "previous" link and
|
||||
* treat this operation as a refresh so the scroll position doesn't get pushed
|
||||
* down to the end. */
|
||||
HttpHeaderLink previous = HttpHeaderLink.findByRelationType(links, "prev");
|
||||
String uptoId = null;
|
||||
if (previous != null) {
|
||||
uptoId = previous.uri.getQueryParameter("since_id");
|
||||
}
|
||||
adapter.update(accounts, fromId, uptoId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
fulfillAnyQueuedFetches(fetchEnd);
|
||||
if (accounts.size() == 0 && adapter.getItemCount() == 1) {
|
||||
adapter.setFooterState(FooterViewHolder.State.EMPTY);
|
||||
} else {
|
||||
adapter.setFooterState(FooterViewHolder.State.END);
|
||||
}
|
||||
}
|
||||
|
||||
private void onFetchAccountsFailure(Exception exception, FetchEnd fetchEnd) {
|
||||
Log.e(TAG, "Fetch failure: " + exception.getMessage());
|
||||
fulfillAnyQueuedFetches(fetchEnd);
|
||||
}
|
||||
|
||||
private void onRefresh() {
|
||||
fetchAccounts(null, adapter.getTopId(), FetchEnd.TOP);
|
||||
}
|
||||
|
||||
private void onLoadMore(RecyclerView recyclerView) {
|
||||
AccountAdapter adapter = (AccountAdapter) recyclerView.getAdapter();
|
||||
fetchAccounts(adapter.getBottomId(), null, FetchEnd.BOTTOM);
|
||||
}
|
||||
|
||||
private void fulfillAnyQueuedFetches(FetchEnd fetchEnd) {
|
||||
switch (fetchEnd) {
|
||||
case BOTTOM: {
|
||||
bottomLoading = false;
|
||||
if (bottomFetches > 0) {
|
||||
bottomFetches--;
|
||||
onLoadMore(recyclerView);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TOP: {
|
||||
topLoading = false;
|
||||
if (topFetches > 0) {
|
||||
topFetches--;
|
||||
onRefresh();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,12 +34,14 @@ import android.view.View;
|
|||
import android.view.ViewGroup;
|
||||
|
||||
import com.keylesspalace.tusky.MainActivity;
|
||||
import com.keylesspalace.tusky.adapter.FooterViewHolder;
|
||||
import com.keylesspalace.tusky.adapter.NotificationsAdapter;
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.entity.Notification;
|
||||
import com.keylesspalace.tusky.entity.Status;
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||
import com.keylesspalace.tusky.receiver.TimelineReceiver;
|
||||
import com.keylesspalace.tusky.util.HttpHeaderLink;
|
||||
import com.keylesspalace.tusky.util.ThemeUtils;
|
||||
import com.keylesspalace.tusky.view.EndlessOnScrollListener;
|
||||
|
||||
|
@ -55,15 +57,23 @@ public class NotificationsFragment extends SFragment implements
|
|||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private static final String TAG = "Notifications"; // logging tag
|
||||
|
||||
private enum FetchEnd {
|
||||
TOP,
|
||||
BOTTOM,
|
||||
}
|
||||
|
||||
private SwipeRefreshLayout swipeRefreshLayout;
|
||||
private LinearLayoutManager layoutManager;
|
||||
private RecyclerView recyclerView;
|
||||
private EndlessOnScrollListener scrollListener;
|
||||
private NotificationsAdapter adapter;
|
||||
private TabLayout.OnTabSelectedListener onTabSelectedListener;
|
||||
private Call<List<Notification>> listCall;
|
||||
private boolean hideFab;
|
||||
private TimelineReceiver timelineReceiver;
|
||||
private boolean topLoading;
|
||||
private int topFetches;
|
||||
private boolean bottomLoading;
|
||||
private int bottomFetches;
|
||||
|
||||
public static NotificationsFragment newInstance() {
|
||||
NotificationsFragment fragment = new NotificationsFragment();
|
||||
|
@ -157,27 +167,13 @@ public class NotificationsFragment extends SFragment implements
|
|||
|
||||
@Override
|
||||
public void onLoadMore(int page, int totalItemsCount, RecyclerView view) {
|
||||
NotificationsAdapter adapter = (NotificationsAdapter) view.getAdapter();
|
||||
Notification notification = adapter.getItem(adapter.getItemCount() - 2);
|
||||
if (notification != null) {
|
||||
sendFetchNotificationsRequest(notification.id, null);
|
||||
} else {
|
||||
sendFetchNotificationsRequest();
|
||||
}
|
||||
NotificationsFragment.this.onLoadMore(view);
|
||||
}
|
||||
};
|
||||
|
||||
recyclerView.addOnScrollListener(scrollListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (listCall != null) {
|
||||
listCall.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
TabLayout tabLayout = (TabLayout) getActivity().findViewById(R.id.tab_layout);
|
||||
|
@ -189,88 +185,9 @@ public class NotificationsFragment extends SFragment implements
|
|||
super.onDestroyView();
|
||||
}
|
||||
|
||||
private void jumpToTop() {
|
||||
layoutManager.scrollToPosition(0);
|
||||
scrollListener.reset();
|
||||
}
|
||||
|
||||
private void sendFetchNotificationsRequest(final String fromId, String uptoId) {
|
||||
if (fromId != null || adapter.getItemCount() <= 1) {
|
||||
adapter.setFooterState(NotificationsAdapter.FooterState.LOADING);
|
||||
}
|
||||
|
||||
listCall = mastodonAPI.notifications(fromId, uptoId, null);
|
||||
|
||||
listCall.enqueue(new Callback<List<Notification>>() {
|
||||
@Override
|
||||
public void onResponse(Call<List<Notification>> call,
|
||||
Response<List<Notification>> response) {
|
||||
if (response.isSuccessful()) {
|
||||
onFetchNotificationsSuccess(response.body(), fromId);
|
||||
} else {
|
||||
onFetchNotificationsFailure(new Exception(response.message()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<List<Notification>> call, Throwable t) {
|
||||
onFetchNotificationsFailure((Exception) t);
|
||||
}
|
||||
});
|
||||
callList.add(listCall);
|
||||
}
|
||||
|
||||
private void sendFetchNotificationsRequest() {
|
||||
sendFetchNotificationsRequest(null, null);
|
||||
}
|
||||
|
||||
private static boolean findNotification(List<Notification> notifications, String id) {
|
||||
for (Notification notification : notifications) {
|
||||
if (notification.id.equals(id)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void onFetchNotificationsSuccess(List<Notification> notifications, String fromId) {
|
||||
if (fromId != null) {
|
||||
if (notifications.size() > 0 && !findNotification(notifications, fromId)) {
|
||||
adapter.addItems(notifications);
|
||||
|
||||
// Set last update id for pull notifications so that we don't get notified
|
||||
// about things we already loaded here
|
||||
SharedPreferences preferences = getActivity()
|
||||
.getSharedPreferences(getString(R.string.preferences_file_key),
|
||||
Context.MODE_PRIVATE);
|
||||
SharedPreferences.Editor editor = preferences.edit();
|
||||
editor.putString("lastUpdateId", notifications.get(0).id);
|
||||
editor.apply();
|
||||
}
|
||||
} else {
|
||||
adapter.update(notifications);
|
||||
}
|
||||
if (notifications.size() == 0 && adapter.getItemCount() == 1) {
|
||||
adapter.setFooterState(NotificationsAdapter.FooterState.EMPTY);
|
||||
} else if (fromId != null) {
|
||||
adapter.setFooterState(NotificationsAdapter.FooterState.END);
|
||||
}
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
}
|
||||
|
||||
private void onFetchNotificationsFailure(Exception exception) {
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
Log.e(TAG, "Fetch failure: " + exception.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
Notification notification = adapter.getItem(0);
|
||||
if (notification != null) {
|
||||
sendFetchNotificationsRequest(null, notification.id);
|
||||
} else {
|
||||
sendFetchNotificationsRequest();
|
||||
}
|
||||
sendFetchNotificationsRequest(null, adapter.getTopId(), FetchEnd.TOP);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -308,6 +225,12 @@ public class NotificationsFragment extends SFragment implements
|
|||
super.viewThread(notification.status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOpenReblog(int position) {
|
||||
Notification notification = adapter.getItem(position);
|
||||
if (notification != null) onViewAccount(notification.account.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewTag(String tag) {
|
||||
super.viewTag(tag);
|
||||
|
@ -334,8 +257,141 @@ public class NotificationsFragment extends SFragment implements
|
|||
}
|
||||
}
|
||||
|
||||
private void onLoadMore(RecyclerView view) {
|
||||
NotificationsAdapter adapter = (NotificationsAdapter) view.getAdapter();
|
||||
sendFetchNotificationsRequest(adapter.getBottomId(), null, FetchEnd.BOTTOM);
|
||||
}
|
||||
|
||||
private void jumpToTop() {
|
||||
layoutManager.scrollToPosition(0);
|
||||
scrollListener.reset();
|
||||
}
|
||||
|
||||
private void sendFetchNotificationsRequest(String fromId, String uptoId,
|
||||
final FetchEnd fetchEnd) {
|
||||
/* If there is a fetch already ongoing, record however many fetches are requested and
|
||||
* fulfill them after it's complete. */
|
||||
if (fetchEnd == FetchEnd.TOP && topLoading) {
|
||||
topFetches++;
|
||||
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(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
adapter.setFooterState(FooterViewHolder.State.LOADING);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Call<List<Notification>> call = mastodonApi.notifications(fromId, uptoId, null);
|
||||
|
||||
call.enqueue(new Callback<List<Notification>>() {
|
||||
@Override
|
||||
public void onResponse(Call<List<Notification>> call,
|
||||
Response<List<Notification>> response) {
|
||||
if (response.isSuccessful()) {
|
||||
String linkHeader = response.headers().get("Link");
|
||||
onFetchNotificationsSuccess(response.body(), linkHeader, fetchEnd);
|
||||
} else {
|
||||
onFetchNotificationsFailure(new Exception(response.message()), fetchEnd);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<List<Notification>> call, Throwable t) {
|
||||
onFetchNotificationsFailure((Exception) t, fetchEnd);
|
||||
}
|
||||
});
|
||||
callList.add(call);
|
||||
}
|
||||
|
||||
private void onFetchNotificationsSuccess(List<Notification> notifications, String linkHeader,
|
||||
FetchEnd fetchEnd) {
|
||||
List<HttpHeaderLink> links = HttpHeaderLink.parse(linkHeader);
|
||||
switch (fetchEnd) {
|
||||
case TOP: {
|
||||
HttpHeaderLink previous = HttpHeaderLink.findByRelationType(links, "prev");
|
||||
String uptoId = null;
|
||||
if (previous != null) {
|
||||
uptoId = previous.uri.getQueryParameter("since_id");
|
||||
}
|
||||
adapter.update(notifications, null, uptoId);
|
||||
break;
|
||||
}
|
||||
case BOTTOM: {
|
||||
HttpHeaderLink next = HttpHeaderLink.findByRelationType(links, "next");
|
||||
String fromId = null;
|
||||
if (next != null) {
|
||||
fromId = next.uri.getQueryParameter("max_id");
|
||||
}
|
||||
if (adapter.getItemCount() > 1) {
|
||||
adapter.addItems(notifications, fromId);
|
||||
} else {
|
||||
/* If this is the first fetch, also save the id from the "previous" link and
|
||||
* treat this operation as a refresh so the scroll position doesn't get pushed
|
||||
* down to the end. */
|
||||
HttpHeaderLink previous = HttpHeaderLink.findByRelationType(links, "prev");
|
||||
String uptoId = null;
|
||||
if (previous != null) {
|
||||
uptoId = previous.uri.getQueryParameter("since_id");
|
||||
}
|
||||
adapter.update(notifications, fromId, uptoId);
|
||||
}
|
||||
/* Set last update id for pull notifications so that we don't get notified
|
||||
* about things we already loaded here */
|
||||
getPrivatePreferences().edit()
|
||||
.putString("lastUpdateId", fromId)
|
||||
.apply();
|
||||
break;
|
||||
}
|
||||
}
|
||||
fulfillAnyQueuedFetches(fetchEnd);
|
||||
if (notifications.size() == 0 && adapter.getItemCount() == 1) {
|
||||
adapter.setFooterState(FooterViewHolder.State.EMPTY);
|
||||
} else {
|
||||
adapter.setFooterState(FooterViewHolder.State.END);
|
||||
}
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
}
|
||||
|
||||
private void onFetchNotificationsFailure(Exception exception, FetchEnd fetchEnd) {
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
Log.e(TAG, "Fetch failure: " + exception.getMessage());
|
||||
fulfillAnyQueuedFetches(fetchEnd);
|
||||
}
|
||||
|
||||
private void fulfillAnyQueuedFetches(FetchEnd fetchEnd) {
|
||||
switch (fetchEnd) {
|
||||
case BOTTOM: {
|
||||
bottomLoading = false;
|
||||
if (bottomFetches > 0) {
|
||||
bottomFetches--;
|
||||
onLoadMore(recyclerView);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TOP: {
|
||||
topLoading = false;
|
||||
if (topFetches > 0) {
|
||||
topFetches--;
|
||||
onRefresh();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void fullyRefresh() {
|
||||
adapter.clear();
|
||||
sendFetchNotificationsRequest(null, null);
|
||||
sendFetchNotificationsRequest(null, null, FetchEnd.TOP);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,10 +57,11 @@ import retrofit2.Response;
|
|||
* overlap functionality. So, I'm momentarily leaving it and hopefully working on those will clear
|
||||
* up what needs to be where. */
|
||||
public abstract class SFragment extends BaseFragment {
|
||||
protected static final int COMPOSE_RESULT = 1;
|
||||
|
||||
protected String loggedInAccountId;
|
||||
protected String loggedInUsername;
|
||||
protected MastodonApi mastodonAPI;
|
||||
protected static int COMPOSE_RESULT = 1;
|
||||
protected MastodonApi mastodonApi;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
|
@ -75,7 +76,13 @@ public abstract class SFragment extends BaseFragment {
|
|||
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
BaseActivity activity = (BaseActivity) getActivity();
|
||||
mastodonAPI = activity.mastodonApi;
|
||||
mastodonApi = activity.mastodonApi;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startActivity(Intent intent) {
|
||||
super.startActivity(intent);
|
||||
getActivity().overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left);
|
||||
}
|
||||
|
||||
protected void reply(Status status) {
|
||||
|
@ -122,9 +129,9 @@ public abstract class SFragment extends BaseFragment {
|
|||
|
||||
Call<Status> call;
|
||||
if (reblog) {
|
||||
call = mastodonAPI.reblogStatus(id);
|
||||
call = mastodonApi.reblogStatus(id);
|
||||
} else {
|
||||
call = mastodonAPI.unreblogStatus(id);
|
||||
call = mastodonApi.unreblogStatus(id);
|
||||
}
|
||||
call.enqueue(cb);
|
||||
callList.add(call);
|
||||
|
@ -154,16 +161,21 @@ public abstract class SFragment extends BaseFragment {
|
|||
|
||||
Call<Status> call;
|
||||
if (favourite) {
|
||||
call = mastodonAPI.favouriteStatus(id);
|
||||
call = mastodonApi.favouriteStatus(id);
|
||||
} else {
|
||||
call = mastodonAPI.unfavouriteStatus(id);
|
||||
call = mastodonApi.unfavouriteStatus(id);
|
||||
}
|
||||
call.enqueue(cb);
|
||||
callList.add(call);
|
||||
}
|
||||
|
||||
protected void openReblog(@Nullable final Status status) {
|
||||
if (status == null) return;
|
||||
viewAccount(status.account.id);
|
||||
}
|
||||
|
||||
private void mute(String id) {
|
||||
Call<Relationship> call = mastodonAPI.muteAccount(id);
|
||||
Call<Relationship> call = mastodonApi.muteAccount(id);
|
||||
call.enqueue(new Callback<Relationship>() {
|
||||
@Override
|
||||
public void onResponse(Call<Relationship> call, Response<Relationship> response) {}
|
||||
|
@ -179,7 +191,7 @@ public abstract class SFragment extends BaseFragment {
|
|||
}
|
||||
|
||||
private void block(String id) {
|
||||
Call<Relationship> call = mastodonAPI.blockAccount(id);
|
||||
Call<Relationship> call = mastodonApi.blockAccount(id);
|
||||
call.enqueue(new Callback<Relationship>() {
|
||||
@Override
|
||||
public void onResponse(Call<Relationship> call, retrofit2.Response<Relationship> response) {}
|
||||
|
@ -195,7 +207,7 @@ public abstract class SFragment extends BaseFragment {
|
|||
}
|
||||
|
||||
private void delete(String id) {
|
||||
Call<ResponseBody> call = mastodonAPI.deleteStatus(id);
|
||||
Call<ResponseBody> call = mastodonApi.deleteStatus(id);
|
||||
call.enqueue(new Callback<ResponseBody>() {
|
||||
@Override
|
||||
public void onResponse(Call<ResponseBody> call, retrofit2.Response<ResponseBody> response) {}
|
||||
|
@ -313,14 +325,8 @@ public abstract class SFragment extends BaseFragment {
|
|||
startActivity(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startActivity(Intent intent) {
|
||||
super.startActivity(intent);
|
||||
getActivity().overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left);
|
||||
}
|
||||
|
||||
protected void openReportPage(String accountId, String accountUsername, String statusId,
|
||||
Spanned statusContent) {
|
||||
Spanned statusContent) {
|
||||
Intent intent = new Intent(getContext(), ReportActivity.class);
|
||||
intent.putExtra("account_id", accountId);
|
||||
intent.putExtra("account_username", accountUsername);
|
||||
|
|
|
@ -35,10 +35,13 @@ import android.view.ViewGroup;
|
|||
|
||||
import com.keylesspalace.tusky.MainActivity;
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.adapter.FooterViewHolder;
|
||||
import com.keylesspalace.tusky.adapter.TimelineAdapter;
|
||||
import com.keylesspalace.tusky.entity.Status;
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||
import com.keylesspalace.tusky.network.MastodonApi;
|
||||
import com.keylesspalace.tusky.receiver.TimelineReceiver;
|
||||
import com.keylesspalace.tusky.util.HttpHeaderLink;
|
||||
import com.keylesspalace.tusky.util.ThemeUtils;
|
||||
import com.keylesspalace.tusky.view.EndlessOnScrollListener;
|
||||
|
||||
|
@ -64,6 +67,11 @@ public class TimelineFragment extends SFragment implements
|
|||
FAVOURITES
|
||||
}
|
||||
|
||||
private enum FetchEnd {
|
||||
TOP,
|
||||
BOTTOM,
|
||||
}
|
||||
|
||||
private SwipeRefreshLayout swipeRefreshLayout;
|
||||
private TimelineAdapter adapter;
|
||||
private Kind kind;
|
||||
|
@ -72,11 +80,14 @@ public class TimelineFragment extends SFragment implements
|
|||
private LinearLayoutManager layoutManager;
|
||||
private EndlessOnScrollListener scrollListener;
|
||||
private TabLayout.OnTabSelectedListener onTabSelectedListener;
|
||||
private SharedPreferences preferences;
|
||||
private boolean filterRemoveReplies;
|
||||
private boolean filterRemoveReblogs;
|
||||
private boolean hideFab;
|
||||
private TimelineReceiver timelineReceiver;
|
||||
private boolean topLoading;
|
||||
private int topFetches;
|
||||
private boolean bottomLoading;
|
||||
private int bottomFetches;
|
||||
|
||||
public static TimelineFragment newInstance(Kind kind) {
|
||||
TimelineFragment fragment = new TimelineFragment();
|
||||
|
@ -198,8 +209,6 @@ public class TimelineFragment extends SFragment implements
|
|||
};
|
||||
}
|
||||
recyclerView.addOnScrollListener(scrollListener);
|
||||
|
||||
preferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -212,20 +221,9 @@ public class TimelineFragment extends SFragment implements
|
|||
super.onDestroyView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
setFiltersFromSettings();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
Status status = adapter.getItem(0);
|
||||
if (status != null) {
|
||||
sendFetchTimelineRequest(null, status.id);
|
||||
} else {
|
||||
sendFetchTimelineRequest(null, null);
|
||||
}
|
||||
sendFetchTimelineRequest(null, adapter.getTopId(), FetchEnd.TOP);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -248,6 +246,11 @@ public class TimelineFragment extends SFragment implements
|
|||
super.more(adapter.getItem(position), view, adapter, position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOpenReblog(int position) {
|
||||
super.openReblog(adapter.getItem(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewMedia(String[] urls, int urlIndex, Status.MediaAttachment.Type type) {
|
||||
super.viewMedia(urls, urlIndex, type);
|
||||
|
@ -290,22 +293,35 @@ public class TimelineFragment extends SFragment implements
|
|||
fullyRefresh();
|
||||
break;
|
||||
}
|
||||
case "tabFilterHomeReplies": {
|
||||
boolean filter = sharedPreferences.getBoolean("tabFilterHomeReplies", true);
|
||||
boolean oldRemoveReplies = filterRemoveReplies;
|
||||
filterRemoveReplies = kind == Kind.HOME && !filter;
|
||||
if (adapter.getItemCount() > 1 && oldRemoveReplies != filterRemoveReplies) {
|
||||
fullyRefresh();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "tabFilterHomeBoosts": {
|
||||
boolean filter = sharedPreferences.getBoolean("tabFilterHomeBoosts", true);
|
||||
boolean oldRemoveReblogs = filterRemoveReblogs;
|
||||
filterRemoveReblogs = kind == Kind.HOME && !filter;
|
||||
if (adapter.getItemCount() > 1 && oldRemoveReblogs != filterRemoveReblogs) {
|
||||
fullyRefresh();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onLoadMore(RecyclerView view) {
|
||||
TimelineAdapter adapter = (TimelineAdapter) view.getAdapter();
|
||||
Status status = adapter.getItem(adapter.getItemCount() - 2);
|
||||
if (status != null) {
|
||||
sendFetchTimelineRequest(status.id, null);
|
||||
} else {
|
||||
sendFetchTimelineRequest(null, null);
|
||||
}
|
||||
sendFetchTimelineRequest(adapter.getBottomId(), null, FetchEnd.BOTTOM);
|
||||
}
|
||||
|
||||
private void fullyRefresh() {
|
||||
adapter.clear();
|
||||
sendFetchTimelineRequest(null, null);
|
||||
sendFetchTimelineRequest(null, null, FetchEnd.TOP);
|
||||
}
|
||||
|
||||
private boolean jumpToTopAllowed() {
|
||||
|
@ -321,108 +337,147 @@ public class TimelineFragment extends SFragment implements
|
|||
scrollListener.reset();
|
||||
}
|
||||
|
||||
private void sendFetchTimelineRequest(@Nullable final String fromId, @Nullable String uptoId) {
|
||||
private Call<List<Status>> getFetchCallByTimelineType(Kind kind, String tagOrId, String fromId,
|
||||
String uptoId) {
|
||||
MastodonApi api = mastodonApi;
|
||||
switch (kind) {
|
||||
default:
|
||||
case HOME: return api.homeTimeline(fromId, uptoId, null);
|
||||
case PUBLIC_FEDERATED: return api.publicTimeline(null, fromId, uptoId, null);
|
||||
case PUBLIC_LOCAL: return api.publicTimeline(true, fromId, uptoId, null);
|
||||
case TAG: return api.hashtagTimeline(tagOrId, null, fromId, uptoId, null);
|
||||
case USER: return api.accountStatuses(tagOrId, fromId, uptoId, null);
|
||||
case FAVOURITES: return api.favourites(fromId, uptoId, null);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendFetchTimelineRequest(@Nullable String fromId, @Nullable String uptoId,
|
||||
final FetchEnd fetchEnd) {
|
||||
/* If there is a fetch already ongoing, record however many fetches are requested and
|
||||
* fulfill them after it's complete. */
|
||||
if (fetchEnd == FetchEnd.TOP && topLoading) {
|
||||
topFetches++;
|
||||
return;
|
||||
}
|
||||
if (fetchEnd == FetchEnd.BOTTOM && bottomLoading) {
|
||||
bottomFetches++;
|
||||
return;
|
||||
}
|
||||
|
||||
if (fromId != null || adapter.getItemCount() <= 1) {
|
||||
adapter.setFooterState(TimelineAdapter.FooterState.LOADING);
|
||||
/* 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(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
adapter.setFooterState(FooterViewHolder.State.LOADING);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Callback<List<Status>> callback = new Callback<List<Status>>() {
|
||||
@Override
|
||||
public void onResponse(Call<List<Status>> call, Response<List<Status>> response) {
|
||||
if (response.isSuccessful()) {
|
||||
onFetchTimelineSuccess(response.body(), fromId);
|
||||
String linkHeader = response.headers().get("Link");
|
||||
onFetchTimelineSuccess(response.body(), linkHeader, fetchEnd);
|
||||
} else {
|
||||
onFetchTimelineFailure(new Exception(response.message()));
|
||||
onFetchTimelineFailure(new Exception(response.message()), fetchEnd);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<List<Status>> call, Throwable t) {
|
||||
onFetchTimelineFailure((Exception) t);
|
||||
onFetchTimelineFailure((Exception) t, fetchEnd);
|
||||
}
|
||||
};
|
||||
|
||||
Call<List<Status>> listCall;
|
||||
switch (kind) {
|
||||
default:
|
||||
case HOME: {
|
||||
listCall = mastodonAPI.homeTimeline(fromId, uptoId, null);
|
||||
break;
|
||||
}
|
||||
case PUBLIC_FEDERATED: {
|
||||
listCall = mastodonAPI.publicTimeline(null, fromId, uptoId, null);
|
||||
break;
|
||||
}
|
||||
case PUBLIC_LOCAL: {
|
||||
listCall = mastodonAPI.publicTimeline(true, fromId, uptoId, null);
|
||||
break;
|
||||
}
|
||||
case TAG: {
|
||||
listCall = mastodonAPI.hashtagTimeline(hashtagOrId, null, fromId, uptoId, null);
|
||||
break;
|
||||
}
|
||||
case USER: {
|
||||
listCall = mastodonAPI.accountStatuses(hashtagOrId, fromId, uptoId, null);
|
||||
break;
|
||||
}
|
||||
case FAVOURITES: {
|
||||
listCall = mastodonAPI.favourites(fromId, uptoId, null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
Call<List<Status>> listCall = getFetchCallByTimelineType(kind, hashtagOrId, fromId, uptoId);
|
||||
callList.add(listCall);
|
||||
listCall.enqueue(callback);
|
||||
}
|
||||
|
||||
private static boolean findStatus(List<Status> statuses, String id) {
|
||||
for (Status status : statuses) {
|
||||
if (status.id.equals(id)) {
|
||||
return true;
|
||||
public void onFetchTimelineSuccess(List<Status> statuses, String linkHeader,
|
||||
FetchEnd fetchEnd) {
|
||||
filterStatuses(statuses);
|
||||
List<HttpHeaderLink> links = HttpHeaderLink.parse(linkHeader);
|
||||
switch (fetchEnd) {
|
||||
case TOP: {
|
||||
HttpHeaderLink previous = HttpHeaderLink.findByRelationType(links, "prev");
|
||||
String uptoId = null;
|
||||
if (previous != null) {
|
||||
uptoId = previous.uri.getQueryParameter("since_id");
|
||||
}
|
||||
adapter.update(statuses, null, uptoId);
|
||||
break;
|
||||
}
|
||||
case BOTTOM: {
|
||||
HttpHeaderLink next = HttpHeaderLink.findByRelationType(links, "next");
|
||||
String fromId = null;
|
||||
if (next != null) {
|
||||
fromId = next.uri.getQueryParameter("max_id");
|
||||
}
|
||||
if (adapter.getItemCount() > 1) {
|
||||
adapter.addItems(statuses, fromId);
|
||||
} else {
|
||||
/* If this is the first fetch, also save the id from the "previous" link and
|
||||
* treat this operation as a refresh so the scroll position doesn't get pushed
|
||||
* down to the end. */
|
||||
HttpHeaderLink previous = HttpHeaderLink.findByRelationType(links, "prev");
|
||||
String uptoId = null;
|
||||
if (previous != null) {
|
||||
uptoId = previous.uri.getQueryParameter("since_id");
|
||||
}
|
||||
adapter.update(statuses, fromId, uptoId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
fulfillAnyQueuedFetches(fetchEnd);
|
||||
if (statuses.size() == 0 && adapter.getItemCount() == 1) {
|
||||
adapter.setFooterState(FooterViewHolder.State.EMPTY);
|
||||
} else {
|
||||
adapter.setFooterState(FooterViewHolder.State.END);
|
||||
}
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
}
|
||||
|
||||
public void onFetchTimelineFailure(Exception exception, FetchEnd fetchEnd) {
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
Log.e(TAG, "Fetch Failure: " + exception.getMessage());
|
||||
fulfillAnyQueuedFetches(fetchEnd);
|
||||
}
|
||||
|
||||
private void fulfillAnyQueuedFetches(FetchEnd fetchEnd) {
|
||||
switch (fetchEnd) {
|
||||
case BOTTOM: {
|
||||
bottomLoading = false;
|
||||
if (bottomFetches > 0) {
|
||||
bottomFetches--;
|
||||
onLoadMore(recyclerView);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TOP: {
|
||||
topLoading = false;
|
||||
if (topFetches > 0) {
|
||||
topFetches--;
|
||||
onRefresh();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected void filterStatuses(List<Status> statuses) {
|
||||
Iterator<Status> it = statuses.iterator();
|
||||
while (it.hasNext()) {
|
||||
Status status = it.next();
|
||||
if ((status.inReplyToId != null && filterRemoveReplies) || (status.reblog != null && filterRemoveReblogs)) {
|
||||
if ((status.inReplyToId != null && filterRemoveReplies)
|
||||
|| (status.reblog != null && filterRemoveReblogs)) {
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void setFiltersFromSettings() {
|
||||
boolean oldRemoveReplies = filterRemoveReplies;
|
||||
boolean oldRemoveReblogs = filterRemoveReblogs;
|
||||
filterRemoveReplies = (kind == Kind.HOME && !preferences.getBoolean("tabFilterHomeReplies", true));
|
||||
filterRemoveReblogs = (kind == Kind.HOME && !preferences.getBoolean("tabFilterHomeBoosts", true));
|
||||
|
||||
if (adapter.getItemCount() > 1 && (oldRemoveReblogs != filterRemoveReblogs || oldRemoveReplies != filterRemoveReplies)) {
|
||||
fullyRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
public void onFetchTimelineSuccess(List<Status> statuses, String fromId) {
|
||||
filterStatuses(statuses);
|
||||
if (fromId != null) {
|
||||
if (statuses.size() > 0 && !findStatus(statuses, fromId)) {
|
||||
adapter.addItems(statuses);
|
||||
}
|
||||
} else {
|
||||
adapter.update(statuses);
|
||||
}
|
||||
if (statuses.size() == 0 && adapter.getItemCount() == 1) {
|
||||
adapter.setFooterState(TimelineAdapter.FooterState.EMPTY);
|
||||
} else if(fromId != null) {
|
||||
adapter.setFooterState(TimelineAdapter.FooterState.END);
|
||||
}
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
}
|
||||
|
||||
public void onFetchTimelineFailure(Exception exception) {
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
Log.e(TAG, "Fetch Failure: " + exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,10 +35,8 @@ import android.view.ViewGroup;
|
|||
|
||||
|
||||
import com.keylesspalace.tusky.adapter.ThreadAdapter;
|
||||
import com.keylesspalace.tusky.BaseActivity;
|
||||
import com.keylesspalace.tusky.entity.Status;
|
||||
import com.keylesspalace.tusky.entity.StatusContext;
|
||||
import com.keylesspalace.tusky.network.MastodonApi;
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||
import com.keylesspalace.tusky.receiver.TimelineReceiver;
|
||||
|
@ -56,7 +54,6 @@ public class ViewThreadFragment extends SFragment implements
|
|||
private SwipeRefreshLayout swipeRefreshLayout;
|
||||
private RecyclerView recyclerView;
|
||||
private ThreadAdapter adapter;
|
||||
private MastodonApi mastodonApi;
|
||||
private String thisThreadsStatusId;
|
||||
private TimelineReceiver timelineReceiver;
|
||||
|
||||
|
@ -97,7 +94,6 @@ public class ViewThreadFragment extends SFragment implements
|
|||
adapter.setMediaPreviewEnabled(mediaPreviewEnabled);
|
||||
recyclerView.setAdapter(adapter);
|
||||
|
||||
mastodonApi = null;
|
||||
thisThreadsStatusId = null;
|
||||
|
||||
timelineReceiver = new TimelineReceiver(adapter, this);
|
||||
|
@ -117,77 +113,10 @@ public class ViewThreadFragment extends SFragment implements
|
|||
@Override
|
||||
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
/* BaseActivity's MastodonApi object isn't guaranteed to be valid until after its onCreate
|
||||
* is run, so all calls that need it can't be done until here. */
|
||||
mastodonApi = ((BaseActivity) getActivity()).mastodonApi;
|
||||
|
||||
thisThreadsStatusId = getArguments().getString("id");
|
||||
onRefresh();
|
||||
}
|
||||
|
||||
private void sendStatusRequest(final String id) {
|
||||
Call<Status> call = mastodonApi.status(id);
|
||||
call.enqueue(new Callback<Status>() {
|
||||
@Override
|
||||
public void onResponse(Call<Status> call, Response<Status> response) {
|
||||
if (response.isSuccessful()) {
|
||||
int position = adapter.setStatus(response.body());
|
||||
recyclerView.scrollToPosition(position);
|
||||
} else {
|
||||
onThreadRequestFailure(id);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<Status> call, Throwable t) {
|
||||
onThreadRequestFailure(id);
|
||||
}
|
||||
});
|
||||
callList.add(call);
|
||||
}
|
||||
|
||||
private void sendThreadRequest(final String id) {
|
||||
Call<StatusContext> call = mastodonApi.statusContext(id);
|
||||
call.enqueue(new Callback<StatusContext>() {
|
||||
@Override
|
||||
public void onResponse(Call<StatusContext> call, Response<StatusContext> response) {
|
||||
if (response.isSuccessful()) {
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
StatusContext context = response.body();
|
||||
|
||||
adapter.setContext(context.ancestors, context.descendants);
|
||||
} else {
|
||||
onThreadRequestFailure(id);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<StatusContext> call, Throwable t) {
|
||||
onThreadRequestFailure(id);
|
||||
}
|
||||
});
|
||||
callList.add(call);
|
||||
}
|
||||
|
||||
private void onThreadRequestFailure(final String id) {
|
||||
View view = getView();
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
if (view != null) {
|
||||
Snackbar.make(view, R.string.error_generic, Snackbar.LENGTH_LONG)
|
||||
.setAction(R.string.action_retry, new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
sendThreadRequest(id);
|
||||
sendStatusRequest(id);
|
||||
}
|
||||
})
|
||||
.show();
|
||||
} else {
|
||||
Log.e(TAG, "Couldn't display thread fetch error message");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
sendStatusRequest(thisThreadsStatusId);
|
||||
|
@ -229,6 +158,12 @@ public class ViewThreadFragment extends SFragment implements
|
|||
super.viewThread(status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOpenReblog(int position) {
|
||||
// there should be no reblogs in the thread but let's implement it to be sure
|
||||
super.openReblog(adapter.getItem(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewTag(String tag) {
|
||||
super.viewTag(tag);
|
||||
|
@ -238,4 +173,65 @@ public class ViewThreadFragment extends SFragment implements
|
|||
public void onViewAccount(String id) {
|
||||
super.viewAccount(id);
|
||||
}
|
||||
|
||||
private void sendStatusRequest(final String id) {
|
||||
Call<Status> call = mastodonApi.status(id);
|
||||
call.enqueue(new Callback<Status>() {
|
||||
@Override
|
||||
public void onResponse(Call<Status> call, Response<Status> response) {
|
||||
if (response.isSuccessful()) {
|
||||
int position = adapter.setStatus(response.body());
|
||||
recyclerView.scrollToPosition(position);
|
||||
} else {
|
||||
onThreadRequestFailure(id);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<Status> call, Throwable t) {
|
||||
onThreadRequestFailure(id);
|
||||
}
|
||||
});
|
||||
callList.add(call);
|
||||
}
|
||||
|
||||
private void sendThreadRequest(final String id) {
|
||||
Call<StatusContext> call = mastodonApi.statusContext(id);
|
||||
call.enqueue(new Callback<StatusContext>() {
|
||||
@Override
|
||||
public void onResponse(Call<StatusContext> call, Response<StatusContext> response) {
|
||||
if (response.isSuccessful()) {
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
StatusContext context = response.body();
|
||||
adapter.setContext(context.ancestors, context.descendants);
|
||||
} else {
|
||||
onThreadRequestFailure(id);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<StatusContext> call, Throwable t) {
|
||||
onThreadRequestFailure(id);
|
||||
}
|
||||
});
|
||||
callList.add(call);
|
||||
}
|
||||
|
||||
private void onThreadRequestFailure(final String id) {
|
||||
View view = getView();
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
if (view != null) {
|
||||
Snackbar.make(view, R.string.error_generic, Snackbar.LENGTH_LONG)
|
||||
.setAction(R.string.action_retry, new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
sendThreadRequest(id);
|
||||
sendStatusRequest(id);
|
||||
}
|
||||
})
|
||||
.show();
|
||||
} else {
|
||||
Log.e(TAG, "Couldn't display thread fetch error message");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,4 +26,5 @@ public interface StatusActionListener extends LinkListener {
|
|||
void onMore(View view, final int position);
|
||||
void onViewMedia(String[] urls, int index, Status.MediaAttachment.Type type);
|
||||
void onViewThread(int position);
|
||||
void onOpenReblog(int position);
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
package com.keylesspalace.tusky.json;
|
||||
|
||||
import android.text.Spanned;
|
||||
import android.text.SpannedString;
|
||||
|
||||
import com.emojione.Emojione;
|
||||
import com.google.gson.JsonDeserializationContext;
|
||||
|
@ -28,7 +29,13 @@ import java.lang.reflect.Type;
|
|||
|
||||
public class SpannedTypeAdapter implements JsonDeserializer<Spanned> {
|
||||
@Override
|
||||
public Spanned deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
return HtmlUtils.fromHtml(Emojione.shortnameToUnicode(json.getAsString(), false));
|
||||
public Spanned deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
|
||||
throws JsonParseException {
|
||||
String string = json.getAsString();
|
||||
if (string != null) {
|
||||
return HtmlUtils.fromHtml(Emojione.shortnameToUnicode(string, false));
|
||||
} else {
|
||||
return new SpannedString("");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -190,7 +190,10 @@ public interface MastodonApi {
|
|||
|
||||
@FormUrlEncoded
|
||||
@POST("api/v1/reports")
|
||||
Call<ResponseBody> report(@Field("account_id") String accountId, @Field("status_ids[]") List<String> statusIds, @Field("comment") String comment);
|
||||
Call<ResponseBody> report(
|
||||
@Field("account_id") String accountId,
|
||||
@Field("status_ids[]") List<String> statusIds,
|
||||
@Field("comment") String comment);
|
||||
|
||||
@GET("api/v1/search")
|
||||
Call<SearchResults> search(@Query("q") String q, @Query("resolve") Boolean resolve);
|
||||
|
|
|
@ -0,0 +1,148 @@
|
|||
/* Written in 2017 by Andrew Dawson
|
||||
*
|
||||
* To the extent possible under law, the author(s) have dedicated all copyright and related and
|
||||
* neighboring rights to this software to the public domain worldwide. This software is distributed
|
||||
* without any warranty.
|
||||
*
|
||||
* You should have received a copy of the CC0 Public Domain Dedication along with this software.
|
||||
* If not, see <http://creativecommons.org/publicdomain/zero/1.0/>. */
|
||||
|
||||
package com.keylesspalace.tusky.util;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class HttpHeaderLink {
|
||||
private static class Parameter {
|
||||
public String name;
|
||||
public String value;
|
||||
}
|
||||
|
||||
private List<Parameter> parameters;
|
||||
public Uri uri;
|
||||
|
||||
private HttpHeaderLink(String uri) {
|
||||
this.uri = Uri.parse(uri);
|
||||
this.parameters = new ArrayList<>();
|
||||
}
|
||||
|
||||
private static int findAny(String s, int fromIndex, char[] set) {
|
||||
for (int i = fromIndex; i < s.length(); i++) {
|
||||
char c = s.charAt(i);
|
||||
for (char member : set) {
|
||||
if (c == member) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static int findEndOfQuotedString(String line, int start) {
|
||||
for (int i = start; i < line.length(); i++) {
|
||||
char c = line.charAt(i);
|
||||
if (c == '\\') {
|
||||
i += 1;
|
||||
} else if (c == '"') {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static class ValueResult {
|
||||
String value;
|
||||
int end;
|
||||
|
||||
ValueResult() {
|
||||
end = -1;
|
||||
}
|
||||
|
||||
void setValue(String value) {
|
||||
value = value.trim();
|
||||
if (!value.isEmpty()) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ValueResult parseValue(String line, int start) {
|
||||
ValueResult result = new ValueResult();
|
||||
int foundIndex = findAny(line, start, new char[] {';', ',', '"'});
|
||||
if (foundIndex == -1) {
|
||||
result.setValue(line.substring(start));
|
||||
return result;
|
||||
}
|
||||
char c = line.charAt(foundIndex);
|
||||
if (c == ';' || c == ',') {
|
||||
result.end = foundIndex;
|
||||
result.setValue(line.substring(start, foundIndex));
|
||||
return result;
|
||||
} else {
|
||||
int quoteEnd = findEndOfQuotedString(line, foundIndex + 1);
|
||||
if (quoteEnd == -1) {
|
||||
quoteEnd = line.length();
|
||||
}
|
||||
result.end = quoteEnd;
|
||||
result.setValue(line.substring(foundIndex + 1, quoteEnd));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private static int parseParameters(String line, int start, HttpHeaderLink link) {
|
||||
for (int i = start; i < line.length(); i++) {
|
||||
int foundIndex = findAny(line, i, new char[] {'=', ','});
|
||||
if (foundIndex == -1) {
|
||||
return -1;
|
||||
} else if (line.charAt(foundIndex) == ',') {
|
||||
return foundIndex;
|
||||
}
|
||||
Parameter parameter = new Parameter();
|
||||
parameter.name = line.substring(line.indexOf(';', i) + 1, foundIndex).trim();
|
||||
link.parameters.add(parameter);
|
||||
ValueResult result = parseValue(line, foundIndex);
|
||||
parameter.value = result.value;
|
||||
if (result.end == -1) {
|
||||
return -1;
|
||||
} else {
|
||||
i = result.end;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public static List<HttpHeaderLink> parse(@Nullable String line) {
|
||||
List<HttpHeaderLink> linkList = new ArrayList<>();
|
||||
if (line != null) {
|
||||
for (int i = 0; i < line.length(); i++) {
|
||||
int uriEnd = line.indexOf('>', i);
|
||||
String uri = line.substring(line.indexOf('<', i) + 1, uriEnd);
|
||||
HttpHeaderLink link = new HttpHeaderLink(uri);
|
||||
linkList.add(link);
|
||||
int parseEnd = parseParameters(line, uriEnd, link);
|
||||
if (parseEnd == -1) {
|
||||
break;
|
||||
} else {
|
||||
i = parseEnd;
|
||||
}
|
||||
}
|
||||
}
|
||||
return linkList;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static HttpHeaderLink findByRelationType(List<HttpHeaderLink> links,
|
||||
String relationType) {
|
||||
for (HttpHeaderLink link : links) {
|
||||
for (Parameter parameter : link.parameters) {
|
||||
if (parameter.name.equals("rel") && parameter.value.equals(relationType)) {
|
||||
return link;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -47,8 +47,8 @@ public class LinkHelper {
|
|||
}
|
||||
|
||||
public static void setClickableText(TextView view, Spanned content,
|
||||
@Nullable Status.Mention[] mentions, boolean useCustomTabs,
|
||||
final LinkListener listener) {
|
||||
@Nullable Status.Mention[] mentions, boolean useCustomTabs,
|
||||
final LinkListener listener) {
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder(content);
|
||||
URLSpan[] urlSpans = content.getSpans(0, content.length(), URLSpan.class);
|
||||
for (URLSpan span : urlSpans) {
|
||||
|
|
|
@ -41,7 +41,7 @@ import com.squareup.picasso.Target;
|
|||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
|
||||
public class NotificationMaker {
|
||||
class NotificationMaker {
|
||||
|
||||
public static final String TAG = "NotificationMaker";
|
||||
|
||||
|
@ -89,10 +89,12 @@ public class NotificationMaker {
|
|||
TaskStackBuilder stackBuilder = TaskStackBuilder.create(context);
|
||||
stackBuilder.addParentStack(MainActivity.class);
|
||||
stackBuilder.addNextIntent(resultIntent);
|
||||
PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
Intent deleteIntent = new Intent(context, NotificationClearBroadcastReceiver.class);
|
||||
PendingIntent deletePendingIntent = PendingIntent.getBroadcast(context, 0, deleteIntent, PendingIntent.FLAG_CANCEL_CURRENT);
|
||||
PendingIntent deletePendingIntent = PendingIntent.getBroadcast(context, 0, deleteIntent,
|
||||
PendingIntent.FLAG_CANCEL_CURRENT);
|
||||
|
||||
final NotificationCompat.Builder builder = new NotificationCompat.Builder(context)
|
||||
.setSmallIcon(R.drawable.ic_notify)
|
||||
|
@ -104,15 +106,16 @@ public class NotificationMaker {
|
|||
builder.setContentTitle(titleForType(context, body))
|
||||
.setContentText(truncateWithEllipses(bodyForType(body), 40));
|
||||
|
||||
Target mTarget = new Target() {
|
||||
Target target = new Target() {
|
||||
@Override
|
||||
public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
|
||||
builder.setLargeIcon(bitmap);
|
||||
|
||||
setupPreferences(preferences, builder);
|
||||
|
||||
((NotificationManager) (context.getSystemService(Context.NOTIFICATION_SERVICE)))
|
||||
.notify(notifyId, builder.build());
|
||||
NotificationManager notificationManager = (NotificationManager)
|
||||
context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
notificationManager.notify(notifyId, builder.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -126,12 +129,15 @@ public class NotificationMaker {
|
|||
.load(body.account.avatar)
|
||||
.placeholder(R.drawable.avatar_default)
|
||||
.transform(new RoundedTransformation(7, 0))
|
||||
.into(mTarget);
|
||||
.into(target);
|
||||
} else {
|
||||
setupPreferences(preferences, builder);
|
||||
try {
|
||||
builder.setContentTitle(String.format(context.getString(R.string.notification_title_summary), currentNotifications.length()))
|
||||
.setContentText(truncateWithEllipses(joinNames(context, currentNotifications), 40));
|
||||
String format = context.getString(R.string.notification_title_summary);
|
||||
String title = String.format(format, currentNotifications.length());
|
||||
String text = truncateWithEllipses(joinNames(context, currentNotifications), 40);
|
||||
builder.setContentTitle(title)
|
||||
.setContentText(text);
|
||||
} catch (JSONException e) {
|
||||
Log.d(TAG, Log.getStackTraceString(e));
|
||||
}
|
||||
|
@ -142,26 +148,23 @@ public class NotificationMaker {
|
|||
builder.setCategory(android.app.Notification.CATEGORY_SOCIAL);
|
||||
}
|
||||
|
||||
((NotificationManager) (context.getSystemService(Context.NOTIFICATION_SERVICE)))
|
||||
.notify(notifyId, builder.build());
|
||||
NotificationManager notificationManager =
|
||||
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
notificationManager.notify(notifyId, builder.build());
|
||||
}
|
||||
|
||||
private static boolean filterNotification(SharedPreferences preferences,
|
||||
Notification notification) {
|
||||
Notification notification) {
|
||||
switch (notification.type) {
|
||||
default:
|
||||
case MENTION: {
|
||||
case MENTION:
|
||||
return preferences.getBoolean("notificationFilterMentions", true);
|
||||
}
|
||||
case FOLLOW: {
|
||||
case FOLLOW:
|
||||
return preferences.getBoolean("notificationFilterFollows", true);
|
||||
}
|
||||
case REBLOG: {
|
||||
case REBLOG:
|
||||
return preferences.getBoolean("notificationFilterReblogs", true);
|
||||
}
|
||||
case FAVOURITE: {
|
||||
case FAVOURITE:
|
||||
return preferences.getBoolean("notificationFilterFavourites", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -174,7 +177,7 @@ public class NotificationMaker {
|
|||
}
|
||||
|
||||
private static void setupPreferences(SharedPreferences preferences,
|
||||
NotificationCompat.Builder builder) {
|
||||
NotificationCompat.Builder builder) {
|
||||
if (preferences.getBoolean("notificationAlertSound", true)) {
|
||||
builder.setSound(Settings.System.DEFAULT_NOTIFICATION_URI);
|
||||
}
|
||||
|
@ -191,11 +194,14 @@ public class NotificationMaker {
|
|||
@Nullable
|
||||
private static String joinNames(Context context, JSONArray array) throws JSONException {
|
||||
if (array.length() > 3) {
|
||||
return String.format(context.getString(R.string.notification_summary_large), array.get(0), array.get(1), array.get(2), array.length() - 3);
|
||||
return String.format(context.getString(R.string.notification_summary_large),
|
||||
array.get(0), array.get(1), array.get(2), array.length() - 3);
|
||||
} else if (array.length() == 3) {
|
||||
return String.format(context.getString(R.string.notification_summary_medium), array.get(0), array.get(1), array.get(2));
|
||||
return String.format(context.getString(R.string.notification_summary_medium),
|
||||
array.get(0), array.get(1), array.get(2));
|
||||
} else if (array.length() == 2) {
|
||||
return String.format(context.getString(R.string.notification_summary_small), array.get(0), array.get(1));
|
||||
return String.format(context.getString(R.string.notification_summary_small),
|
||||
array.get(0), array.get(1));
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -205,13 +211,17 @@ public class NotificationMaker {
|
|||
private static String titleForType(Context context, Notification notification) {
|
||||
switch (notification.type) {
|
||||
case MENTION:
|
||||
return String.format(context.getString(R.string.notification_mention_format), notification.account.getDisplayName());
|
||||
return String.format(context.getString(R.string.notification_mention_format),
|
||||
notification.account.getDisplayName());
|
||||
case FOLLOW:
|
||||
return String.format(context.getString(R.string.notification_follow_format), notification.account.getDisplayName());
|
||||
return String.format(context.getString(R.string.notification_follow_format),
|
||||
notification.account.getDisplayName());
|
||||
case FAVOURITE:
|
||||
return String.format(context.getString(R.string.notification_favourite_format), notification.account.getDisplayName());
|
||||
return String.format(context.getString(R.string.notification_favourite_format),
|
||||
notification.account.getDisplayName());
|
||||
case REBLOG:
|
||||
return String.format(context.getString(R.string.notification_reblog_format), notification.account.getDisplayName());
|
||||
return String.format(context.getString(R.string.notification_reblog_format),
|
||||
notification.account.getDisplayName());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -226,7 +236,6 @@ public class NotificationMaker {
|
|||
case REBLOG:
|
||||
return notification.status.content.toString();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,17 @@ import android.text.Spannable;
|
|||
import android.text.Spanned;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class SpanUtils {
|
||||
private static final String TAG_REGEX = "(?:^|[^/)\\w])#([\\w_]*[\\p{Alpha}_][\\w_]*)";
|
||||
private static Pattern TAG_PATTERN = Pattern.compile(TAG_REGEX, Pattern.CASE_INSENSITIVE);
|
||||
private static final String MENTION_REGEX =
|
||||
"(?:^|[^/[:word:]])@([a-z0-9_]+(?:@[a-z0-9\\.\\-]+[a-z0-9]+)?)";
|
||||
private static Pattern MENTION_PATTERN =
|
||||
Pattern.compile(MENTION_REGEX, Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private static class FindCharsResult {
|
||||
int charIndex;
|
||||
int stringIndex;
|
||||
|
@ -63,35 +73,29 @@ public class SpanUtils {
|
|||
}
|
||||
|
||||
private static int findEndOfHashtag(String string, int fromIndex) {
|
||||
final int length = string.length();
|
||||
for (int i = fromIndex + 1; i < length;) {
|
||||
int codepoint = string.codePointAt(i);
|
||||
if (Character.isWhitespace(codepoint)) {
|
||||
return i;
|
||||
} else if (codepoint == '#') {
|
||||
return -1;
|
||||
}
|
||||
i += Character.charCount(codepoint);
|
||||
Matcher matcher = TAG_PATTERN.matcher(string);
|
||||
if (fromIndex >= 1) {
|
||||
fromIndex--;
|
||||
}
|
||||
boolean found = matcher.find(fromIndex);
|
||||
if (found) {
|
||||
return matcher.end();
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
return length;
|
||||
}
|
||||
|
||||
private static int findEndOfMention(String string, int fromIndex) {
|
||||
int atCount = 0;
|
||||
final int length = string.length();
|
||||
for (int i = fromIndex + 1; i < length;) {
|
||||
int codepoint = string.codePointAt(i);
|
||||
if (Character.isWhitespace(codepoint)) {
|
||||
return i;
|
||||
} else if (codepoint == '@') {
|
||||
atCount += 1;
|
||||
if (atCount >= 2) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
i += Character.charCount(codepoint);
|
||||
Matcher matcher = MENTION_PATTERN.matcher(string);
|
||||
if (fromIndex >= 1) {
|
||||
fromIndex--;
|
||||
}
|
||||
boolean found = matcher.find(fromIndex);
|
||||
if (found) {
|
||||
return matcher.end();
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
return length;
|
||||
}
|
||||
|
||||
public static void highlightSpans(Spannable text, int colour) {
|
||||
|
|
|
@ -1,20 +1,23 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
android:id="@+id/footer_container">
|
||||
|
||||
<LinearLayout
|
||||
<ProgressBar
|
||||
android:id="@+id/footer_progress_bar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_gravity="center">
|
||||
<ProgressBar
|
||||
android:id="@+id/footer_progress_bar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
</LinearLayout>
|
||||
android:layout_centerInParent="true"
|
||||
android:indeterminate="true" />
|
||||
|
||||
</LinearLayout>
|
||||
<TextView
|
||||
android:id="@+id/footer_end_message"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/footer_empty"
|
||||
android:textAlignment="center"
|
||||
android:layout_centerInParent="true"
|
||||
android:drawablePadding="16dp" />
|
||||
|
||||
</RelativeLayout>
|
|
@ -1,25 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="16dp"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:srcCompat="@drawable/elephant_friend" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/footer_empty"
|
||||
android:textAlignment="center" />
|
||||
|
||||
</LinearLayout>
|
|
@ -1,9 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
</LinearLayout>
|
|
@ -40,10 +40,7 @@
|
|||
<string name="status_content_warning_show_more">Pokaż więcej</string>
|
||||
<string name="status_content_warning_show_less">Ukryj</string>
|
||||
|
||||
<string name="footer_end_of_statuses">koniec statusów</string>
|
||||
<string name="footer_end_of_notifications">koniec powiadomień</string>
|
||||
<string name="footer_end_of_accounts">koniec listy kont</string>
|
||||
<string name="footer_empty">Brak wpisów! Pociągnij, aby odświeżyć.</string>
|
||||
<string name="footer_empty">Pusto! Pociągnij, aby odświeżyć.</string>
|
||||
|
||||
<string name="notification_reblog_format">%s podbił twój post</string>
|
||||
<string name="notification_favourite_format">%s dodał twój post do ulubionych</string>
|
||||
|
@ -91,7 +88,7 @@
|
|||
<string name="action_mute">Wycisz</string>
|
||||
<string name="action_unmute">Cofnij wyciszenie</string>
|
||||
<string name="action_mention">Wspomnij</string>
|
||||
<!--<string name="action_mark_sensitive">Mark media sensitive</string>-->
|
||||
<string name="action_hide_media">Ukryj zawartość multimedialną</string>
|
||||
<string name="action_compose_options">Opcje</string>
|
||||
<string name="action_open_drawer">Otwórz szufladę</string>
|
||||
<string name="action_clear">Wyczyść</string>
|
||||
|
@ -114,10 +111,10 @@
|
|||
|
||||
<string name="hint_domain">Jaka instancja?</string>
|
||||
<string name="hint_compose">Co ci chodzi po głowie?</string>
|
||||
<string name="hint_content_warning">Ostrzeenie o zawartości</string>
|
||||
<string name="hint_content_warning">Ostrzeżenie o zawartości</string>
|
||||
<string name="hint_display_name">Nazwa wyświetlana</string>
|
||||
<string name="hint_note">Biografia</string>
|
||||
<string name="hint_search">Szukaj kont i tagów…</string>
|
||||
<string name="hint_search">Szukaj…</string>
|
||||
|
||||
<string name="search_no_results">Brak wyników</string>
|
||||
|
||||
|
@ -168,6 +165,7 @@
|
|||
<string name="pref_title_status_tabs">Karty</string>
|
||||
<string name="pref_title_show_boosts">Pokazuj podbicia</string>
|
||||
<string name="pref_title_show_replies">Pokazuj odpowiedzi</string>
|
||||
<string name="pref_title_show_media_preview">Pokazuj podgląd zawartości multimedialnej</string>
|
||||
|
||||
<string name="notification_mention_format">%s wspomniał o tobie</string>
|
||||
<string name="notification_summary_large">%1$s, %2$s, %3$s i %4$d innych</string>
|
||||
|
@ -191,6 +189,8 @@
|
|||
|
||||
<string name="status_share_content">Udostępnij zawartość postu</string>
|
||||
<string name="status_share_link">Udostępnij link do postu</string>
|
||||
<string name="status_media_images">Obrazy</string>
|
||||
<string name="status_media_video">Wideo</string>
|
||||
|
||||
<string name="state_follow_requested">Wysłano prośbę o obserwację</string>
|
||||
</resources>
|
||||
|
|
|
@ -41,10 +41,7 @@
|
|||
<string name="status_content_warning_show_more">Show More</string>
|
||||
<string name="status_content_warning_show_less">Show Less</string>
|
||||
|
||||
<string name="footer_end_of_statuses">end of the statuses</string>
|
||||
<string name="footer_end_of_notifications">end of the notifications</string>
|
||||
<string name="footer_end_of_accounts">end of the accounts</string>
|
||||
<string name="footer_empty">There are no toots here so far. Pull down to refresh!</string>
|
||||
<string name="footer_empty">Nothing here. Pull down to refresh!</string>
|
||||
|
||||
<string name="notification_reblog_format">%s boosted your toot</string>
|
||||
<string name="notification_favourite_format">%s favourited your toot</string>
|
||||
|
@ -117,7 +114,7 @@
|
|||
<string name="hint_content_warning">Content warning</string>
|
||||
<string name="hint_display_name">Display name</string>
|
||||
<string name="hint_note">Bio</string>
|
||||
<string name="hint_search">Search accounts and tags…</string>
|
||||
<string name="hint_search">Search…</string>
|
||||
|
||||
<string name="search_no_results">No results</string>
|
||||
|
||||
|
|
Loading…
Reference in New Issue