mirror of
https://github.com/tuskyapp/Tusky
synced 2025-02-03 07:57:41 +01:00
Merge remote-tracking branch 'upstream/master' into feature_collapse_status
This commit is contained in:
commit
623cad203b
@ -1,4 +1,4 @@
|
||||
[![Translate - with Stringlate](https://img.shields.io/badge/translate%20with-stringlate-green.svg)](https://lonamiwebs.github.io/stringlate/translate?git=https%3A%2F%2Fgithub.com%2Ftuskyapp%2FTusky) [![Build Status](https://app.bitrise.io/app/55b2f0c77c4bba74/status.svg?token=elUl9fieM5K34iLRL0rpoA&branch=master)](https://app.bitrise.io/app/55b2f0c77c4bba74) [![CircleCI](https://circleci.com/gh/tuskyapp/Tusky.svg?style=svg)](https://circleci.com/gh/tuskyapp/Tusky)
|
||||
[![Translate - with Stringlate](https://img.shields.io/badge/translate%20with-stringlate-green.svg)](https://lonamiwebs.github.io/stringlate/translate?git=https%3A%2F%2Fgithub.com%2Ftuskyapp%2FTusky) [![Build Status](https://app.bitrise.io/app/a3e773c3c57a894c/status.svg?token=qLu_Ti4Gp2LWcYT4eo2INQ&branch=master)](https://app.bitrise.io/app/a3e773c3c57a894c#/builds) [![CircleCI](https://circleci.com/gh/tuskyapp/Tusky.svg?style=svg)](https://circleci.com/gh/tuskyapp/Tusky)
|
||||
# Tusky
|
||||
|
||||
![](/fastlane/metadata/android/en-US/images/icon.png)
|
||||
|
@ -1,7 +1,7 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
def getGitSha = { ->
|
||||
def stdout = new ByteArrayOutputStream()
|
||||
@ -19,8 +19,8 @@ android {
|
||||
applicationId "com.keylesspalace.tusky"
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 27
|
||||
versionCode 48
|
||||
versionName "3.0"
|
||||
versionCode 49
|
||||
versionName "3.1"
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables.useSupportLibrary true
|
||||
}
|
||||
@ -60,7 +60,7 @@ android {
|
||||
}
|
||||
|
||||
ext.supportLibraryVersion = '27.1.1'
|
||||
ext.daggerVersion = '2.16'
|
||||
ext.daggerVersion = '2.17'
|
||||
|
||||
|
||||
// if libraries are changed here, they should also be changed in LicenseActivity
|
||||
@ -82,13 +82,13 @@ dependencies {
|
||||
implementation 'com.squareup.okhttp3:logging-interceptor:3.11.0'
|
||||
implementation 'com.jakewharton.picasso:picasso2-okhttp3-downloader:1.1.0'
|
||||
implementation 'com.github.connyduck:sparkbutton:1.0.1'
|
||||
implementation 'com.github.chrisbanes:PhotoView:2.1.3'
|
||||
implementation 'com.github.chrisbanes:PhotoView:2.1.4'
|
||||
implementation 'com.mikepenz:google-material-typeface:3.0.1.2.original@aar'
|
||||
implementation('com.theartofdev.edmodo:android-image-cropper:2.7.0') {
|
||||
exclude group: 'com.android.support'
|
||||
}
|
||||
implementation 'com.evernote:android-job:1.2.6'
|
||||
implementation 'com.android.support.constraint:constraint-layout:1.1.2'
|
||||
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
|
||||
// EmojiCompat
|
||||
implementation "com.android.support:support-emoji:$supportLibraryVersion"
|
||||
implementation "com.android.support:support-emoji-appcompat:$supportLibraryVersion"
|
||||
@ -111,8 +111,8 @@ dependencies {
|
||||
exclude group: 'com.android.support', module: 'support-annotations'
|
||||
})
|
||||
debugImplementation 'im.dino:dbinspector:3.4.1@aar'
|
||||
implementation 'io.reactivex.rxjava2:rxjava:2.2.0'
|
||||
implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
|
||||
implementation 'com.uber.autodispose:autodispose-android-archcomponents:0.8.0'
|
||||
implementation 'com.uber.autodispose:autodispose-kotlin:0.8.0'
|
||||
implementation 'io.reactivex.rxjava2:rxjava:2.2.1'
|
||||
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
|
||||
implementation 'com.uber.autodispose:autodispose-android-archcomponents:1.0.0-RC2'
|
||||
implementation 'com.uber.autodispose:autodispose-ktx:1.0.0-RC2'
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ package com.keylesspalace.tusky;
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.ProgressDialog;
|
||||
import android.arch.lifecycle.Lifecycle;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
@ -135,12 +136,20 @@ import java.util.Locale;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import at.connyduck.sparkbutton.helpers.Utils;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.SingleObserver;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.MultipartBody;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
import static com.uber.autodispose.AutoDispose.autoDisposable;
|
||||
import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from;
|
||||
|
||||
public final class ComposeActivity
|
||||
extends BaseActivity
|
||||
implements ComposeOptionsListener,
|
||||
@ -1121,11 +1130,24 @@ public final class ComposeActivity
|
||||
DisplayMetrics displayMetrics = new DisplayMetrics();
|
||||
getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
|
||||
|
||||
Picasso.with(this)
|
||||
.load(item.uri)
|
||||
.resize(displayMetrics.widthPixels, displayMetrics.heightPixels)
|
||||
.onlyScaleDown()
|
||||
.into(imageView);
|
||||
Single.fromCallable(() ->
|
||||
MediaUtils.getSampledBitmap(getContentResolver(), item.uri, displayMetrics.widthPixels, displayMetrics.heightPixels))
|
||||
.subscribeOn(Schedulers.computation())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
|
||||
.subscribe(new SingleObserver<Bitmap>() {
|
||||
@Override
|
||||
public void onSubscribe(Disposable d) {}
|
||||
|
||||
@Override
|
||||
public void onSuccess(Bitmap bitmap) {
|
||||
imageView.setImageBitmap(bitmap);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable e) { }
|
||||
});
|
||||
|
||||
|
||||
int margin = Utils.dpToPx(this, 4);
|
||||
dialogLayout.addView(imageView);
|
||||
|
@ -420,7 +420,7 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
|
||||
|
||||
Intent intent = new Intent(this, MainActivity.class);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
startActivityWithSlideInAnimation(intent);
|
||||
startActivity(intent);
|
||||
finishWithoutSlideOutAnimation();
|
||||
|
||||
overridePendingTransition(R.anim.explode, R.anim.explode);
|
||||
@ -510,21 +510,15 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
|
||||
|
||||
List<AccountEntity> allAccounts = accountManager.getAllAccountsOrderedByActive();
|
||||
|
||||
// reuse the already existing "add account" item
|
||||
List<IProfile> profiles = new ArrayList<>(allAccounts.size()+1);
|
||||
for (IProfile profile: headerResult.getProfiles()) {
|
||||
if (profile.getIdentifier() == DRAWER_ITEM_ADD_ACCOUNT) {
|
||||
profiles.add(profile);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (AccountEntity acc : allAccounts) {
|
||||
CharSequence emojifiedName = CustomEmojiHelper.emojifyString(acc.getDisplayName(), acc.getEmojis(), headerResult.getView());
|
||||
emojifiedName = EmojiCompat.get().process(emojifiedName);
|
||||
|
||||
profiles.add(0,
|
||||
profiles.add(
|
||||
new ProfileDrawerItem()
|
||||
.withSetSelected(acc.isActive())
|
||||
.withName(emojifiedName)
|
||||
.withIcon(acc.getProfilePictureUrl())
|
||||
.withNameShown(true)
|
||||
@ -532,6 +526,15 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
|
||||
.withEmail(acc.getFullName()));
|
||||
|
||||
}
|
||||
|
||||
// reuse the already existing "add account" item
|
||||
for (IProfile profile: headerResult.getProfiles()) {
|
||||
if (profile.getIdentifier() == DRAWER_ITEM_ADD_ACCOUNT) {
|
||||
profiles.add(profile);
|
||||
break;
|
||||
}
|
||||
}
|
||||
headerResult.clear();
|
||||
headerResult.setProfiles(profiles);
|
||||
|
||||
}
|
||||
|
@ -15,6 +15,7 @@
|
||||
|
||||
package com.keylesspalace.tusky.adapter;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
|
||||
@ -26,55 +27,40 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class AccountAdapter extends RecyclerView.Adapter {
|
||||
static final int VIEW_TYPE_ACCOUNT = 0;
|
||||
static final int VIEW_TYPE_FOOTER = 1;
|
||||
|
||||
|
||||
List<Account> accountList;
|
||||
AccountActionListener accountActionListener;
|
||||
FooterViewHolder.State footerState;
|
||||
|
||||
private String topId;
|
||||
private String bottomId;
|
||||
private boolean bottomLoading;
|
||||
|
||||
AccountAdapter(AccountActionListener accountActionListener) {
|
||||
super();
|
||||
accountList = new ArrayList<>();
|
||||
this.accountList = new ArrayList<>();
|
||||
this.accountActionListener = accountActionListener;
|
||||
footerState = FooterViewHolder.State.END;
|
||||
bottomLoading = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return accountList.size() + 1;
|
||||
return accountList.size() + (bottomLoading ? 1 : 0);
|
||||
}
|
||||
|
||||
public void update(@Nullable List<Account> newAccounts, @Nullable String fromId,
|
||||
@Nullable String uptoId) {
|
||||
if (newAccounts == null || newAccounts.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
bottomId = fromId;
|
||||
topId = uptoId;
|
||||
|
||||
if (accountList.isEmpty()) {
|
||||
accountList = ListUtils.removeDuplicates(newAccounts);
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
if (position == accountList.size() && bottomLoading) {
|
||||
return VIEW_TYPE_FOOTER;
|
||||
} else {
|
||||
int index = accountList.indexOf(newAccounts.get(newAccounts.size() - 1));
|
||||
for (int i = 0; i < index; i++) {
|
||||
accountList.remove(0);
|
||||
}
|
||||
int newIndex = newAccounts.indexOf(accountList.get(0));
|
||||
if (newIndex == -1) {
|
||||
accountList.addAll(0, newAccounts);
|
||||
} else {
|
||||
accountList.addAll(0, newAccounts.subList(0, newIndex));
|
||||
}
|
||||
return VIEW_TYPE_ACCOUNT;
|
||||
}
|
||||
}
|
||||
|
||||
public void update(@NonNull List<Account> newAccounts) {
|
||||
accountList = ListUtils.removeDuplicates(newAccounts);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void addItems(List<Account> newAccounts, @Nullable String fromId) {
|
||||
if (fromId != null) {
|
||||
bottomId = fromId;
|
||||
}
|
||||
public void addItems(List<Account> newAccounts) {
|
||||
int end = accountList.size();
|
||||
Account last = accountList.get(end - 1);
|
||||
if (last != null && !findAccount(newAccounts, last.getId())) {
|
||||
@ -83,6 +69,19 @@ public abstract class AccountAdapter extends RecyclerView.Adapter {
|
||||
}
|
||||
}
|
||||
|
||||
public void setBottomLoading(boolean loading) {
|
||||
boolean wasLoading = bottomLoading;
|
||||
if(wasLoading == loading) {
|
||||
return;
|
||||
}
|
||||
bottomLoading = loading;
|
||||
if(loading) {
|
||||
notifyItemInserted(accountList.size());
|
||||
} else {
|
||||
notifyItemRemoved(accountList.size());
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean findAccount(List<Account> accounts, String id) {
|
||||
for (Account account : accounts) {
|
||||
if (account.getId().equals(id)) {
|
||||
@ -110,25 +109,5 @@ 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;
|
||||
}
|
||||
}
|
||||
|
@ -31,8 +31,6 @@ import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
||||
import com.squareup.picasso.Picasso;
|
||||
|
||||
public class BlocksAdapter extends AccountAdapter {
|
||||
private static final int VIEW_TYPE_BLOCKED_USER = 0;
|
||||
private static final int VIEW_TYPE_FOOTER = 1;
|
||||
|
||||
public BlocksAdapter(AccountActionListener accountActionListener) {
|
||||
super(accountActionListener);
|
||||
@ -43,7 +41,7 @@ public class BlocksAdapter extends AccountAdapter {
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
switch (viewType) {
|
||||
default:
|
||||
case VIEW_TYPE_BLOCKED_USER: {
|
||||
case VIEW_TYPE_ACCOUNT: {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_blocked_user, parent, false);
|
||||
return new BlockedUserViewHolder(view);
|
||||
@ -51,29 +49,17 @@ public class BlocksAdapter extends AccountAdapter {
|
||||
case VIEW_TYPE_FOOTER: {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_footer, parent, false);
|
||||
return new FooterViewHolder(view);
|
||||
return new LoadingFooterViewHolder(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
|
||||
if (position < accountList.size()) {
|
||||
if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) {
|
||||
BlockedUserViewHolder holder = (BlockedUserViewHolder) viewHolder;
|
||||
holder.setupWithAccount(accountList.get(position));
|
||||
holder.setupActionListener(accountActionListener);
|
||||
} else {
|
||||
FooterViewHolder holder = (FooterViewHolder) viewHolder;
|
||||
holder.setState(footerState);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
if (position == accountList.size()) {
|
||||
return VIEW_TYPE_FOOTER;
|
||||
} else {
|
||||
return VIEW_TYPE_BLOCKED_USER;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -26,8 +26,6 @@ import com.keylesspalace.tusky.interfaces.AccountActionListener;
|
||||
|
||||
/** Both for follows and following lists. */
|
||||
public class FollowAdapter extends AccountAdapter {
|
||||
private static final int VIEW_TYPE_ACCOUNT = 0;
|
||||
private static final int VIEW_TYPE_FOOTER = 1;
|
||||
|
||||
public FollowAdapter(AccountActionListener accountActionListener) {
|
||||
super(accountActionListener);
|
||||
@ -46,29 +44,18 @@ public class FollowAdapter extends AccountAdapter {
|
||||
case VIEW_TYPE_FOOTER: {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_footer, parent, false);
|
||||
return new FooterViewHolder(view);
|
||||
return new LoadingFooterViewHolder(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
|
||||
if (position < accountList.size()) {
|
||||
if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) {
|
||||
AccountViewHolder holder = (AccountViewHolder) viewHolder;
|
||||
holder.setupWithAccount(accountList.get(position));
|
||||
holder.setupActionListener(accountActionListener);
|
||||
} else {
|
||||
FooterViewHolder holder = (FooterViewHolder) viewHolder;
|
||||
holder.setState(footerState);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
if (position == accountList.size()) {
|
||||
return VIEW_TYPE_FOOTER;
|
||||
} else {
|
||||
return VIEW_TYPE_ACCOUNT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -31,8 +31,6 @@ import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
||||
import com.squareup.picasso.Picasso;
|
||||
|
||||
public class FollowRequestsAdapter extends AccountAdapter {
|
||||
private static final int VIEW_TYPE_FOLLOW_REQUEST = 0;
|
||||
private static final int VIEW_TYPE_FOOTER = 1;
|
||||
|
||||
public FollowRequestsAdapter(AccountActionListener accountActionListener) {
|
||||
super(accountActionListener);
|
||||
@ -43,7 +41,7 @@ public class FollowRequestsAdapter extends AccountAdapter {
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
switch (viewType) {
|
||||
default:
|
||||
case VIEW_TYPE_FOLLOW_REQUEST: {
|
||||
case VIEW_TYPE_ACCOUNT: {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_follow_request, parent, false);
|
||||
return new FollowRequestViewHolder(view);
|
||||
@ -51,29 +49,17 @@ public class FollowRequestsAdapter extends AccountAdapter {
|
||||
case VIEW_TYPE_FOOTER: {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_footer, parent, false);
|
||||
return new FooterViewHolder(view);
|
||||
return new LoadingFooterViewHolder(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
|
||||
if (position < accountList.size()) {
|
||||
if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) {
|
||||
FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder;
|
||||
holder.setupWithAccount(accountList.get(position));
|
||||
holder.setupActionListener(accountActionListener);
|
||||
} else {
|
||||
FooterViewHolder holder = (FooterViewHolder) viewHolder;
|
||||
holder.setState(footerState);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
if (position == accountList.size()) {
|
||||
return VIEW_TYPE_FOOTER;
|
||||
} else {
|
||||
return VIEW_TYPE_FOLLOW_REQUEST;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,80 +0,0 @@
|
||||
/* Copyright 2017 Andrew Dawson
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.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;
|
||||
|
||||
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);
|
||||
container = itemView.findViewById(R.id.footer_container);
|
||||
progressBar = itemView.findViewById(R.id.footer_progress_bar);
|
||||
endMessage = itemView.findViewById(R.id.footer_end_message);
|
||||
Drawable top = AppCompatResources.getDrawable(itemView.getContext(),
|
||||
R.drawable.elephant_friend_empty);
|
||||
endMessage.setCompoundDrawablesWithIntrinsicBounds(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
/* Copyright 2018 Conny Duck
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.adapter
|
||||
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.view.View
|
||||
|
||||
class LoadingFooterViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
|
@ -16,8 +16,6 @@ import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
||||
import com.squareup.picasso.Picasso;
|
||||
|
||||
public class MutesAdapter extends AccountAdapter {
|
||||
private static final int VIEW_TYPE_MUTED_USER = 0;
|
||||
private static final int VIEW_TYPE_FOOTER = 1;
|
||||
|
||||
public MutesAdapter(AccountActionListener accountActionListener) {
|
||||
super(accountActionListener);
|
||||
@ -28,7 +26,7 @@ public class MutesAdapter extends AccountAdapter {
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
switch (viewType) {
|
||||
default:
|
||||
case VIEW_TYPE_MUTED_USER: {
|
||||
case VIEW_TYPE_ACCOUNT: {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_muted_user, parent, false);
|
||||
return new MutesAdapter.MutedUserViewHolder(view);
|
||||
@ -36,31 +34,20 @@ public class MutesAdapter extends AccountAdapter {
|
||||
case VIEW_TYPE_FOOTER: {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_footer, parent, false);
|
||||
return new FooterViewHolder(view);
|
||||
return new LoadingFooterViewHolder(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
|
||||
if (position < accountList.size()) {
|
||||
if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) {
|
||||
MutedUserViewHolder holder = (MutedUserViewHolder) viewHolder;
|
||||
holder.setupWithAccount(accountList.get(position));
|
||||
holder.setupActionListener(accountActionListener);
|
||||
} else {
|
||||
FooterViewHolder holder = (FooterViewHolder) viewHolder;
|
||||
holder.setState(footerState);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
if (position == accountList.size()) {
|
||||
return VIEW_TYPE_FOOTER;
|
||||
} else {
|
||||
return VIEW_TYPE_MUTED_USER;
|
||||
}
|
||||
}
|
||||
|
||||
static class MutedUserViewHolder extends RecyclerView.ViewHolder {
|
||||
private ImageView avatar;
|
||||
|
@ -58,10 +58,9 @@ import java.util.List;
|
||||
|
||||
public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||
private static final int VIEW_TYPE_MENTION = 0;
|
||||
private static final int VIEW_TYPE_FOOTER = 1;
|
||||
private static final int VIEW_TYPE_STATUS_NOTIFICATION = 2;
|
||||
private static final int VIEW_TYPE_FOLLOW = 3;
|
||||
private static final int VIEW_TYPE_PLACEHOLDER = 4;
|
||||
private static final int VIEW_TYPE_STATUS_NOTIFICATION = 1;
|
||||
private static final int VIEW_TYPE_FOLLOW = 2;
|
||||
private static final int VIEW_TYPE_PLACEHOLDER = 3;
|
||||
|
||||
private List<NotificationViewData> notifications;
|
||||
private StatusActionListener statusListener;
|
||||
@ -89,11 +88,6 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||
.inflate(R.layout.item_status, parent, false);
|
||||
return new StatusViewHolder(view);
|
||||
}
|
||||
case VIEW_TYPE_FOOTER: {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_footer, parent, false);
|
||||
return new FooterViewHolder(view);
|
||||
}
|
||||
case VIEW_TYPE_STATUS_NOTIFICATION: {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_status_notification, parent, false);
|
||||
@ -174,31 +168,28 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
if (position == notifications.size()) {
|
||||
return VIEW_TYPE_FOOTER;
|
||||
} else {
|
||||
NotificationViewData notification = notifications.get(position);
|
||||
if (notification instanceof NotificationViewData.Concrete) {
|
||||
NotificationViewData.Concrete concrete = ((NotificationViewData.Concrete) notification);
|
||||
switch (concrete.getType()) {
|
||||
default:
|
||||
case MENTION: {
|
||||
return VIEW_TYPE_MENTION;
|
||||
}
|
||||
case FAVOURITE:
|
||||
case REBLOG: {
|
||||
return VIEW_TYPE_STATUS_NOTIFICATION;
|
||||
}
|
||||
case FOLLOW: {
|
||||
return VIEW_TYPE_FOLLOW;
|
||||
}
|
||||
NotificationViewData notification = notifications.get(position);
|
||||
if (notification instanceof NotificationViewData.Concrete) {
|
||||
NotificationViewData.Concrete concrete = ((NotificationViewData.Concrete) notification);
|
||||
switch (concrete.getType()) {
|
||||
default:
|
||||
case MENTION: {
|
||||
return VIEW_TYPE_MENTION;
|
||||
}
|
||||
case FAVOURITE:
|
||||
case REBLOG: {
|
||||
return VIEW_TYPE_STATUS_NOTIFICATION;
|
||||
}
|
||||
case FOLLOW: {
|
||||
return VIEW_TYPE_FOLLOW;
|
||||
}
|
||||
} else if (notification instanceof NotificationViewData.Placeholder) {
|
||||
return VIEW_TYPE_PLACEHOLDER;
|
||||
} else {
|
||||
throw new AssertionError("Unknown notification type");
|
||||
}
|
||||
} else if (notification instanceof NotificationViewData.Placeholder) {
|
||||
return VIEW_TYPE_PLACEHOLDER;
|
||||
} else {
|
||||
throw new AssertionError("Unknown notification type");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void update(@Nullable List<NotificationViewData> newNotifications) {
|
||||
@ -375,8 +366,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||
private void setCreatedAt(@Nullable Date createdAt) {
|
||||
// This is the visible timestampInfo.
|
||||
String readout;
|
||||
/* This one is for screen-readers. Frequently, they would mispronounce timestamps like "17m"
|
||||
* as 17 meters instead of minutes. */
|
||||
/* This one is for screen-readers. Frequently, they would mispronounce timestamps like "17m"
|
||||
* as 17 meters instead of minutes. */
|
||||
CharSequence readoutAloud;
|
||||
if (createdAt != null) {
|
||||
long then = createdAt.getTime();
|
||||
|
@ -18,7 +18,7 @@ package com.keylesspalace.tusky.di
|
||||
import com.keylesspalace.tusky.TuskyApplication
|
||||
import dagger.BindsInstance
|
||||
import dagger.Component
|
||||
import dagger.android.AndroidInjectionModule
|
||||
import dagger.android.support.AndroidSupportInjectionModule
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
||||
@ -30,7 +30,7 @@ import javax.inject.Singleton
|
||||
@Component(modules = [
|
||||
AppModule::class,
|
||||
NetworkModule::class,
|
||||
AndroidInjectionModule::class,
|
||||
AndroidSupportInjectionModule::class,
|
||||
ActivitiesModule::class,
|
||||
ServicesModule::class,
|
||||
BroadcastReceiverModule::class,
|
||||
|
@ -36,7 +36,6 @@ 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.di.Injectable;
|
||||
import com.keylesspalace.tusky.entity.Account;
|
||||
@ -79,10 +78,8 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
|
||||
private RecyclerView recyclerView;
|
||||
private EndlessOnScrollListener scrollListener;
|
||||
private AccountAdapter adapter;
|
||||
private boolean bottomLoading;
|
||||
private int bottomFetches;
|
||||
private boolean topLoading;
|
||||
private int topFetches;
|
||||
private boolean fetching = false;
|
||||
private String bottomId;
|
||||
|
||||
public static AccountListFragment newInstance(Type type) {
|
||||
Bundle arguments = new Bundle();
|
||||
@ -140,10 +137,6 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
|
||||
}
|
||||
recyclerView.setAdapter(adapter);
|
||||
|
||||
bottomLoading = false;
|
||||
bottomFetches = 0;
|
||||
topLoading = false;
|
||||
topFetches = 0;
|
||||
|
||||
return rootView;
|
||||
}
|
||||
@ -155,13 +148,13 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
|
||||
scrollListener = new EndlessOnScrollListener(layoutManager) {
|
||||
@Override
|
||||
public void onLoadMore(int totalItemsCount, RecyclerView view) {
|
||||
AccountListFragment.this.onLoadMore(view);
|
||||
AccountListFragment.this.onLoadMore();
|
||||
}
|
||||
};
|
||||
|
||||
recyclerView.addOnScrollListener(scrollListener);
|
||||
|
||||
fetchAccounts(null, null, FetchEnd.BOTTOM);
|
||||
fetchAccounts(null);
|
||||
|
||||
}
|
||||
|
||||
@ -176,14 +169,6 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
|
||||
|
||||
@Override
|
||||
public void onMute(final boolean mute, final String id, final int position) {
|
||||
if (api == null) {
|
||||
/* If somehow an unmute button is clicked after onCreateView but before
|
||||
* onActivityCreated, then this would get called with a null api object, so this eats
|
||||
* that input. */
|
||||
Log.d(TAG, "MastodonApi isn't initialised so this mute can't occur.");
|
||||
return;
|
||||
}
|
||||
|
||||
Callback<Relationship> callback = new Callback<Relationship>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<Relationship> call, @NonNull Response<Relationship> response) {
|
||||
@ -237,14 +222,6 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
|
||||
|
||||
@Override
|
||||
public void onBlock(final boolean block, final String id, final int position) {
|
||||
if (api == null) {
|
||||
/* If somehow an unblock button is clicked after onCreateView but before
|
||||
* onActivityCreated, then this would get called with a null api object, so this eats
|
||||
* that input. */
|
||||
Log.d(TAG, "MastodonApi isn't initialised so this block can't occur.");
|
||||
return;
|
||||
}
|
||||
|
||||
Callback<Relationship> cb = new Callback<Relationship>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<Relationship> call, @NonNull Response<Relationship> response) {
|
||||
@ -299,13 +276,6 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
|
||||
@Override
|
||||
public void onRespondToFollowRequest(final boolean accept, final String accountId,
|
||||
final int position) {
|
||||
if (api == null) {
|
||||
/* If somehow an response button is clicked after onCreateView but before
|
||||
* onActivityCreated, then this would get called with a null api object, so this eats
|
||||
* that input. */
|
||||
Log.d(TAG, "MastodonApi isn't initialised, so follow requests can't be responded to.");
|
||||
return;
|
||||
}
|
||||
|
||||
Callback<Relationship> callback = new Callback<Relationship>() {
|
||||
@Override
|
||||
@ -349,44 +319,30 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
|
||||
Log.e(TAG, message);
|
||||
}
|
||||
|
||||
private enum FetchEnd {
|
||||
TOP,
|
||||
BOTTOM
|
||||
}
|
||||
|
||||
private Call<List<Account>> getFetchCallByListType(Type type, String fromId, String uptoId) {
|
||||
private Call<List<Account>> getFetchCallByListType(Type type, String fromId) {
|
||||
switch (type) {
|
||||
default:
|
||||
case FOLLOWS:
|
||||
return api.accountFollowing(accountId, fromId, uptoId, null);
|
||||
return api.accountFollowing(accountId, fromId, null, null);
|
||||
case FOLLOWERS:
|
||||
return api.accountFollowers(accountId, fromId, uptoId, null);
|
||||
return api.accountFollowers(accountId, fromId, null, null);
|
||||
case BLOCKS:
|
||||
return api.blocks(fromId, uptoId, null);
|
||||
return api.blocks(fromId, null, null);
|
||||
case MUTES:
|
||||
return api.mutes(fromId, uptoId, null);
|
||||
return api.mutes(fromId, null, null);
|
||||
case FOLLOW_REQUESTS:
|
||||
return api.followRequests(fromId, uptoId, null);
|
||||
return api.followRequests(fromId, null, 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++;
|
||||
private void fetchAccounts(String id) {
|
||||
if (fetching) {
|
||||
return;
|
||||
}
|
||||
fetching = true;
|
||||
|
||||
if (fromId != null || adapter.getItemCount() <= 1) {
|
||||
/* When this is called by the EndlessScrollListener it cannot refresh the footer state
|
||||
* using adapter.notifyItemChanged. So its necessary to postpone doing so until a
|
||||
* convenient time for the UI thread using a Runnable. */
|
||||
recyclerView.post(() -> adapter.setFooterState(FooterViewHolder.State.LOADING));
|
||||
if (id != null) {
|
||||
recyclerView.post(() -> adapter.setBottomLoading(true));
|
||||
}
|
||||
|
||||
Callback<List<Account>> cb = new Callback<List<Account>>() {
|
||||
@ -394,99 +350,55 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
|
||||
public void onResponse(@NonNull Call<List<Account>> call, @NonNull Response<List<Account>> response) {
|
||||
if (response.isSuccessful()) {
|
||||
String linkHeader = response.headers().get("Link");
|
||||
onFetchAccountsSuccess(response.body(), linkHeader, fetchEnd);
|
||||
onFetchAccountsSuccess(response.body(), linkHeader);
|
||||
} else {
|
||||
onFetchAccountsFailure(new Exception(response.message()), fetchEnd);
|
||||
onFetchAccountsFailure(new Exception(response.message()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<List<Account>> call, @NonNull Throwable t) {
|
||||
onFetchAccountsFailure((Exception) t, fetchEnd);
|
||||
onFetchAccountsFailure((Exception) t);
|
||||
}
|
||||
};
|
||||
Call<List<Account>> listCall = getFetchCallByListType(type, fromId, uptoId);
|
||||
Call<List<Account>> listCall = getFetchCallByListType(type, id);
|
||||
callList.add(listCall);
|
||||
listCall.enqueue(cb);
|
||||
}
|
||||
|
||||
private void onFetchAccountsSuccess(List<Account> accounts, String linkHeader,
|
||||
FetchEnd fetchEnd) {
|
||||
private void onFetchAccountsSuccess(List<Account> accounts, String linkHeader) {
|
||||
adapter.setBottomLoading(false);
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
HttpHeaderLink next = HttpHeaderLink.findByRelationType(links, "next");
|
||||
String fromId = null;
|
||||
if (next != null) {
|
||||
fromId = next.uri.getQueryParameter("max_id");
|
||||
}
|
||||
fulfillAnyQueuedFetches(fetchEnd);
|
||||
if (accounts.size() == 0 && adapter.getItemCount() == 1) {
|
||||
adapter.setFooterState(FooterViewHolder.State.EMPTY);
|
||||
if (adapter.getItemCount() > 1) {
|
||||
adapter.addItems(accounts);
|
||||
} else {
|
||||
adapter.setFooterState(FooterViewHolder.State.END);
|
||||
adapter.update(accounts);
|
||||
}
|
||||
|
||||
bottomId = fromId;
|
||||
|
||||
fetching = false;
|
||||
|
||||
adapter.setBottomLoading(false);
|
||||
}
|
||||
|
||||
private void onFetchAccountsFailure(Exception exception, FetchEnd fetchEnd) {
|
||||
private void onFetchAccountsFailure(Exception exception) {
|
||||
fetching = false;
|
||||
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();
|
||||
//if we do not have a bottom id, we know we do not need to load more
|
||||
if (adapter.getBottomId() == null) return;
|
||||
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;
|
||||
}
|
||||
private void onLoadMore() {
|
||||
if(bottomId == null) {
|
||||
return;
|
||||
}
|
||||
fetchAccounts(bottomId);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -753,19 +753,23 @@ public class NotificationsFragment extends SFragment implements
|
||||
private void saveNewestNotificationId(List<Notification> notifications) {
|
||||
|
||||
AccountEntity account = accountManager.getActiveAccount();
|
||||
BigInteger lastNoti = new BigInteger(account.getLastNotificationId());
|
||||
if(account != null) {
|
||||
BigInteger lastNoti = new BigInteger(account.getLastNotificationId());
|
||||
|
||||
for (Notification noti : notifications) {
|
||||
BigInteger a = new BigInteger(noti.getId());
|
||||
if (isBiggerThan(a, lastNoti)) {
|
||||
lastNoti = a;
|
||||
for (Notification noti : notifications) {
|
||||
BigInteger a = new BigInteger(noti.getId());
|
||||
if (isBiggerThan(a, lastNoti)) {
|
||||
lastNoti = a;
|
||||
}
|
||||
}
|
||||
|
||||
String lastNotificationId = lastNoti.toString();
|
||||
if(!account.getLastNotificationId().equals(lastNotificationId)) {
|
||||
Log.d(TAG, "saving newest noti id: " + lastNotificationId);
|
||||
account.setLastNotificationId(lastNotificationId);
|
||||
accountManager.saveAccount(account);
|
||||
}
|
||||
}
|
||||
|
||||
Log.d(TAG, "saving newest noti id: " + lastNoti);
|
||||
|
||||
account.setLastNotificationId(lastNoti.toString());
|
||||
accountManager.saveAccount(account);
|
||||
}
|
||||
|
||||
private boolean isBiggerThan(BigInteger newId, BigInteger lastShownNotificationId) {
|
||||
|
@ -306,6 +306,9 @@ public class TimelineFragment extends SFragment implements
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(statuses.size() == 0) {
|
||||
nothingMessageView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -858,6 +861,8 @@ public class TimelineFragment extends SFragment implements
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
if (this.statuses.size() == 0) {
|
||||
nothingMessageView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
nothingMessageView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -18,16 +18,11 @@ package com.keylesspalace.tusky.util;
|
||||
import android.content.ContentResolver;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Matrix;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.media.ExifInterface;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@ -37,7 +32,6 @@ import java.util.List;
|
||||
* aspect ratio and orientation.
|
||||
*/
|
||||
public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> {
|
||||
private static final String TAG = "DownsizeImageTask";
|
||||
private int sizeLimit;
|
||||
private ContentResolver contentResolver;
|
||||
private Listener listener;
|
||||
@ -54,83 +48,6 @@ public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static Bitmap reorientBitmap(Bitmap bitmap, int orientation) {
|
||||
Matrix matrix = new Matrix();
|
||||
switch (orientation) {
|
||||
default:
|
||||
case ExifInterface.ORIENTATION_NORMAL: {
|
||||
return bitmap;
|
||||
}
|
||||
case ExifInterface.ORIENTATION_FLIP_HORIZONTAL: {
|
||||
matrix.setScale(-1, 1);
|
||||
break;
|
||||
}
|
||||
case ExifInterface.ORIENTATION_ROTATE_180: {
|
||||
matrix.setRotate(180);
|
||||
break;
|
||||
}
|
||||
case ExifInterface.ORIENTATION_FLIP_VERTICAL: {
|
||||
matrix.setRotate(180);
|
||||
matrix.postScale(-1, 1);
|
||||
break;
|
||||
}
|
||||
case ExifInterface.ORIENTATION_TRANSPOSE: {
|
||||
matrix.setRotate(90);
|
||||
matrix.postScale(-1, 1);
|
||||
break;
|
||||
}
|
||||
case ExifInterface.ORIENTATION_ROTATE_90: {
|
||||
matrix.setRotate(90);
|
||||
break;
|
||||
}
|
||||
case ExifInterface.ORIENTATION_TRANSVERSE: {
|
||||
matrix.setRotate(-90);
|
||||
matrix.postScale(-1, 1);
|
||||
break;
|
||||
}
|
||||
case ExifInterface.ORIENTATION_ROTATE_270: {
|
||||
matrix.setRotate(-90);
|
||||
break;
|
||||
}
|
||||
}
|
||||
try {
|
||||
Bitmap result = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),
|
||||
bitmap.getHeight(), matrix, true);
|
||||
if (!bitmap.sameAs(result)) {
|
||||
bitmap.recycle();
|
||||
}
|
||||
return result;
|
||||
} catch (OutOfMemoryError e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static int getOrientation(Uri uri, ContentResolver contentResolver) {
|
||||
InputStream inputStream;
|
||||
try {
|
||||
inputStream = contentResolver.openInputStream(uri);
|
||||
} catch (FileNotFoundException e) {
|
||||
Log.d(TAG, Log.getStackTraceString(e));
|
||||
return ExifInterface.ORIENTATION_UNDEFINED;
|
||||
}
|
||||
if (inputStream == null) {
|
||||
return ExifInterface.ORIENTATION_UNDEFINED;
|
||||
}
|
||||
ExifInterface exifInterface;
|
||||
try {
|
||||
exifInterface = new ExifInterface(inputStream);
|
||||
} catch (IOException e) {
|
||||
Log.d(TAG, Log.getStackTraceString(e));
|
||||
IOUtils.closeQuietly(inputStream);
|
||||
return ExifInterface.ORIENTATION_UNDEFINED;
|
||||
}
|
||||
int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION,
|
||||
ExifInterface.ORIENTATION_NORMAL);
|
||||
IOUtils.closeQuietly(inputStream);
|
||||
return orientation;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(Uri... uris) {
|
||||
resultList = new ArrayList<>();
|
||||
@ -147,7 +64,7 @@ public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> {
|
||||
BitmapFactory.decodeStream(inputStream, null, options);
|
||||
IOUtils.closeQuietly(inputStream);
|
||||
// Get EXIF data, for orientation info.
|
||||
int orientation = getOrientation(uri, contentResolver);
|
||||
int orientation = MediaUtils.getImageOrientation(uri, contentResolver);
|
||||
// Then use that information to determine how much to compress.
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
/* Unfortunately, there isn't a determined worst case compression ratio for image
|
||||
@ -176,7 +93,7 @@ public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> {
|
||||
if (scaledBitmap == null) {
|
||||
return false;
|
||||
}
|
||||
Bitmap reorientedBitmap = reorientBitmap(scaledBitmap, orientation);
|
||||
Bitmap reorientedBitmap = MediaUtils.reorientBitmap(scaledBitmap, orientation);
|
||||
if (reorientedBitmap == null) {
|
||||
scaledBitmap.recycle();
|
||||
return false;
|
||||
|
@ -126,7 +126,7 @@ public class LinkHelper {
|
||||
* @param context context
|
||||
*/
|
||||
public static void openLink(String url, Context context) {
|
||||
Uri uri = Uri.parse(url);
|
||||
Uri uri = Uri.parse(url).normalizeScheme();
|
||||
|
||||
boolean useCustomTabs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getBoolean("customTabs", false);
|
||||
|
@ -20,6 +20,7 @@ import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Matrix;
|
||||
import android.media.MediaMetadataRetriever;
|
||||
import android.media.ThumbnailUtils;
|
||||
import android.net.Uri;
|
||||
@ -27,6 +28,7 @@ import android.provider.OpenableColumns;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.Px;
|
||||
import android.support.media.ExifInterface;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
@ -111,7 +113,9 @@ public class MediaUtils {
|
||||
options.inJustDecodeBounds = false;
|
||||
try {
|
||||
stream = contentResolver.openInputStream(uri);
|
||||
return BitmapFactory.decodeStream(stream, null, options);
|
||||
Bitmap bitmap = BitmapFactory.decodeStream(stream, null, options);
|
||||
int orientation = getImageOrientation(uri, contentResolver);
|
||||
return reorientBitmap(bitmap, orientation);
|
||||
} catch (FileNotFoundException e) {
|
||||
Log.w(TAG, e);
|
||||
return null;
|
||||
@ -176,4 +180,81 @@ public class MediaUtils {
|
||||
|
||||
return inSampleSize;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Bitmap reorientBitmap(Bitmap bitmap, int orientation) {
|
||||
Matrix matrix = new Matrix();
|
||||
switch (orientation) {
|
||||
default:
|
||||
case ExifInterface.ORIENTATION_NORMAL: {
|
||||
return bitmap;
|
||||
}
|
||||
case ExifInterface.ORIENTATION_FLIP_HORIZONTAL: {
|
||||
matrix.setScale(-1, 1);
|
||||
break;
|
||||
}
|
||||
case ExifInterface.ORIENTATION_ROTATE_180: {
|
||||
matrix.setRotate(180);
|
||||
break;
|
||||
}
|
||||
case ExifInterface.ORIENTATION_FLIP_VERTICAL: {
|
||||
matrix.setRotate(180);
|
||||
matrix.postScale(-1, 1);
|
||||
break;
|
||||
}
|
||||
case ExifInterface.ORIENTATION_TRANSPOSE: {
|
||||
matrix.setRotate(90);
|
||||
matrix.postScale(-1, 1);
|
||||
break;
|
||||
}
|
||||
case ExifInterface.ORIENTATION_ROTATE_90: {
|
||||
matrix.setRotate(90);
|
||||
break;
|
||||
}
|
||||
case ExifInterface.ORIENTATION_TRANSVERSE: {
|
||||
matrix.setRotate(-90);
|
||||
matrix.postScale(-1, 1);
|
||||
break;
|
||||
}
|
||||
case ExifInterface.ORIENTATION_ROTATE_270: {
|
||||
matrix.setRotate(-90);
|
||||
break;
|
||||
}
|
||||
}
|
||||
try {
|
||||
Bitmap result = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),
|
||||
bitmap.getHeight(), matrix, true);
|
||||
if (!bitmap.sameAs(result)) {
|
||||
bitmap.recycle();
|
||||
}
|
||||
return result;
|
||||
} catch (OutOfMemoryError e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static int getImageOrientation(Uri uri, ContentResolver contentResolver) {
|
||||
InputStream inputStream;
|
||||
try {
|
||||
inputStream = contentResolver.openInputStream(uri);
|
||||
} catch (FileNotFoundException e) {
|
||||
Log.w(TAG, e);
|
||||
return ExifInterface.ORIENTATION_UNDEFINED;
|
||||
}
|
||||
if (inputStream == null) {
|
||||
return ExifInterface.ORIENTATION_UNDEFINED;
|
||||
}
|
||||
ExifInterface exifInterface;
|
||||
try {
|
||||
exifInterface = new ExifInterface(inputStream);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
IOUtils.closeQuietly(inputStream);
|
||||
return ExifInterface.ORIENTATION_UNDEFINED;
|
||||
}
|
||||
int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION,
|
||||
ExifInterface.ORIENTATION_NORMAL);
|
||||
IOUtils.closeQuietly(inputStream);
|
||||
return orientation;
|
||||
}
|
||||
}
|
||||
|
5
app/src/main/res/drawable/tab_page_margin_black.xml
Normal file
5
app/src/main/res/drawable/tab_page_margin_black.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/tab_page_margin_black" />
|
||||
</shape>
|
@ -91,6 +91,26 @@
|
||||
license:link="https://google.github.io/dagger/"
|
||||
license:name="Dagger 2" />
|
||||
|
||||
<com.keylesspalace.tusky.view.LicenseCard
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="12dp"
|
||||
license:license="@string/license_apache_2"
|
||||
license:link="https://github.com/ReactiveX/RxJava"
|
||||
license:name="RxJava" />
|
||||
|
||||
<com.keylesspalace.tusky.view.LicenseCard
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="12dp"
|
||||
license:license="@string/license_apache_2"
|
||||
license:link="https://github.com/uber/AutoDispose"
|
||||
license:name="AutoDispose" />
|
||||
|
||||
<com.keylesspalace.tusky.view.LicenseCard
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -1,24 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/footer_container"
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="72dp">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/footer_progress_bar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:layout_gravity="center"
|
||||
android:indeterminate="true" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/footer_end_message"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:drawablePadding="32dp"
|
||||
android:text="@string/footer_empty"
|
||||
android:textAlignment="center"
|
||||
android:textSize="?attr/status_text_medium" />
|
||||
|
||||
</RelativeLayout>
|
||||
</FrameLayout>
|
@ -285,7 +285,7 @@
|
||||
<string name="abbreviated_seconds_ago">%ds</string>
|
||||
|
||||
<string name="follows_you">Följer dig</string>
|
||||
<string name="pref_title_alway_show_sensitive_media">Visa alltid allt innehåll (nsfw)</string>
|
||||
<string name="pref_title_alway_show_sensitive_media">Visa alltid allt innehåll (inkl. känsligt)</string>
|
||||
<string name="title_media">Media</string>
|
||||
<string name="replying_to">Svarar till @%s</string>
|
||||
<string name="load_more_placeholder_text">ladda mer</string>
|
||||
|
@ -49,6 +49,7 @@
|
||||
<color name="text_color_primary_black">#FFFFFF</color>
|
||||
<color name="toolbar_background_black">#111111</color>
|
||||
<color name="dialog_background_black">#111111</color>
|
||||
<color name="tab_page_margin_black">#000000</color>
|
||||
<!--Light Theme Colors-->
|
||||
<color name="color_primary_light">#dfdfdf</color>
|
||||
<color name="color_primary_dark_light">#8f8f8f</color>
|
||||
|
@ -172,6 +172,7 @@
|
||||
<item name="material_drawer_selected">@color/color_primary_black</item>
|
||||
<item name="material_drawer_selected_text">@color/text_color_primary_black</item>
|
||||
<item name="material_drawer_header_selection_text">@color/text_color_primary_black</item>
|
||||
<item name="tab_page_margin_drawable">@drawable/tab_page_margin_black</item>
|
||||
</style>
|
||||
<style name="TuskyBlackTheme" parent="TuskyBlackThemeBase"/>
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user