From 51c547ffb0a2cc344e42bf7556aa40b1e2e63a93 Mon Sep 17 00:00:00 2001 From: Conny Duck Date: Sat, 4 Nov 2017 13:25:37 +0100 Subject: [PATCH 1/3] fix an error where opening notifications would lead to the wrong status --- .../com/keylesspalace/tusky/fragment/NotificationsFragment.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java index 5c3254c9b..018bf3727 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java @@ -532,7 +532,7 @@ public class NotificationsFragment extends SFragment implements notifications.addAll(newNotifications); List newViewDatas = notifications.getPairedCopy() .subList(notifications.size() - newNotifications.size(), - notifications.size() - 1); + notifications.size()); adapter.addItems(newViewDatas); } } From a9345820254b377aebb960b496ab0a176f7f6c4b Mon Sep 17 00:00:00 2001 From: Kazuki KANDA Date: Sun, 5 Nov 2017 01:56:19 +0900 Subject: [PATCH 2/3] Improves and adds Japanese translations. (#434) --- app/src/main/res/values-ja/array.xml | 13 ++++++ app/src/main/res/values-ja/strings.xml | 55 ++++++++++++++++++++------ 2 files changed, 56 insertions(+), 12 deletions(-) create mode 100644 app/src/main/res/values-ja/array.xml diff --git a/app/src/main/res/values-ja/array.xml b/app/src/main/res/values-ja/array.xml new file mode 100644 index 000000000..1f10c2b5c --- /dev/null +++ b/app/src/main/res/values-ja/array.xml @@ -0,0 +1,13 @@ + + + + 15分 + 20分 + 25分 + 30分 + 45分 + 1時間 + 2時間 + + + diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 7faa27f75..251fa7a0c 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -100,9 +100,11 @@ 拒否 検索 下書き - リンクをコピー + %1$s をダウンロードしています + リンクをコピー + トゥートのURLを共有… トゥートを共有… @@ -127,14 +129,16 @@ 接続中… - mastodon.social, mstdn.jp, pawoo.net や - その他 - のような、あらゆるインスタンスのアドレスやドメインを入力できます。 - \n\nまだアカウントをお持ちでない場合は、参加したいインスタンスの名前を入力することで - そのインスタンスにアカウントを作成できます。\n\nインスタンスはあなたのアカウントが提供される単独の場所ですが、 - 他のインスタンスのユーザーとあたかも同じ場所にいるように簡単にコミュニケーションをとったりフォローしたりできます。 + mastodon.social, mstdn.jp, pawoo.netやその他のような、あらゆるインスタンスのアドレスやドメインを入力できます。 + \n\nまだアカウントをお持ちでない場合は、参加したいインスタンスの名前を入力することでそのインスタンスにアカウントを作成できます。 + \n\nインスタンスはあなたのアカウントが提供される単独の場所ですが、他のインスタンスのユーザーとあたかも同じ場所にいるように簡単にコミュニケーションをとったりフォローしたりできます。 \n\nさらに詳しい情報はjoinmastodon.orgでご覧いただけます。 + メディアをアップロードしています アップロード中… ダウンロード @@ -170,6 +174,15 @@ 返信を表示 メディアのプレビューを表示する + 新しい返信 + 新しい返信の通知 + 新しいフォロワー + 新しいフォロワーの通知 + ブースト + あなたのトゥートがブーストされたときの通知 + お気に入り + あなたのトゥートがお気に入りに登録されたときの通知 + %sさんが返信しました %1$sさん、%2$sさん、%3$sさんと他%4$d人 %1$sさん、%2$sさん、%3$sさん @@ -180,12 +193,17 @@ このアプリについて アプリのバージョン:%s + Tusky %s + Tuskyは無料のオープンソースソフトウェアです。GNU General Public License Version 3 の下で使用許諾されています。ライセンスはここからご覧いただけます: https://www.gnu.org/licenses/gpl-3.0.ja.html + - プロジェクトのWebサイト:\n + プロジェクトのWebサイト(英語):\n https://tusky.keylesspalace.com - --> + - バグ報告 & 機能リクエスト:\n + バグ報告 & 機能リクエスト(英語):\n https://github.com/Vavassor/Tusky/issues Tusky公式アカウント @@ -199,8 +217,21 @@ 下書きはありません 下書きに保存しました! - + + + %d年後 + %d日後 + %d時間後 + %d分後 + %d秒後 + %d年前 + %d日前 + %d時間前 + %d分前 + %d秒前 + あなたをフォロー中 - 常にすべての閲覧注意なコンテンツを表示する + 閲覧注意のコンテンツを常に表示 + @%sに返信 From dc1a60cc1207e50b2614f168c1d9180622e6c57d Mon Sep 17 00:00:00 2001 From: Ivan Kupalov Date: Sun, 5 Nov 2017 23:32:36 +0200 Subject: [PATCH 3/3] Implement media tab (#430) --- app/build.gradle | 6 + .../keylesspalace/tusky/AccountActivity.java | 111 +++++-- .../tusky/AccountListActivity.java | 60 +++- .../keylesspalace/tusky/ReportActivity.java | 2 +- .../tusky/fragment/AccountListFragment.java | 107 ++----- .../tusky/fragment/AccountMediaFragment.kt | 288 ++++++++++++++++++ .../tusky/fragment/TimelineFragment.java | 15 +- .../tusky/network/MastodonApi.java | 17 +- .../tusky/pager/AccountPagerAdapter.java | 28 +- .../tusky/view/SquareImageView.kt | 24 ++ .../main/res/color/account_tab_font_color.xml | 4 +- app/src/main/res/layout/activity_account.xml | 81 +++-- app/src/main/res/layout/tab_account.xml | 28 +- app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values-small/integer.xml | 4 + app/src/main/res/values/integers.xml | 4 + app/src/main/res/values/strings.xml | 4 + build.gradle | 2 + 18 files changed, 597 insertions(+), 189 deletions(-) create mode 100644 app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/view/SquareImageView.kt create mode 100644 app/src/main/res/values-small/integer.xml create mode 100644 app/src/main/res/values/integers.xml diff --git a/app/build.gradle b/app/build.gradle index e0a8d68a4..729801e06 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,4 +1,5 @@ apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' android { compileSdkVersion 26 @@ -58,6 +59,7 @@ dependencies { compile "com.mikepenz:google-material-typeface:3.0.1.0.original@aar" compile "com.theartofdev.edmodo:android-image-cropper:2.5.1" compile 'com.evernote:android-job:1.2.0' + implementation 'com.android.support.constraint:constraint-layout:1.0.2' //room compile "android.arch.persistence.room:runtime:1.0.0-rc1" @@ -67,5 +69,9 @@ dependencies { androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { exclude group: 'com.android.support', module: 'support-annotations' }) + compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" +} +repositories { + mavenCentral() } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.java b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.java index be0257ea0..e815fcd66 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.java @@ -41,6 +41,7 @@ import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; +import android.view.ViewGroup; import android.widget.Button; import android.widget.ImageView; import android.widget.TextView; @@ -51,8 +52,8 @@ import com.keylesspalace.tusky.interfaces.ActionButtonActivity; import com.keylesspalace.tusky.interfaces.LinkListener; import com.keylesspalace.tusky.pager.AccountPagerAdapter; import com.keylesspalace.tusky.receiver.TimelineReceiver; -import com.keylesspalace.tusky.util.LinkHelper; import com.keylesspalace.tusky.util.Assert; +import com.keylesspalace.tusky.util.LinkHelper; import com.keylesspalace.tusky.util.ThemeUtils; import com.pkmmte.view.CircularImageView; import com.squareup.picasso.Picasso; @@ -66,7 +67,7 @@ import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; -public class AccountActivity extends BaseActivity implements ActionButtonActivity { +public final class AccountActivity extends BaseActivity implements ActionButtonActivity { private static final String TAG = "AccountActivity"; // logging tag private enum FollowState { @@ -81,6 +82,7 @@ public class AccountActivity extends BaseActivity implements ActionButtonActivit private boolean muting; private boolean isSelf; private Account loadedAccount; + private CircularImageView avatar; private ImageView header; private FloatingActionButton floatingBtn; @@ -89,6 +91,10 @@ public class AccountActivity extends BaseActivity implements ActionButtonActivit private TabLayout tabLayout; private ImageView accountLockedView; private View container; + private TextView followersTextView; + private TextView followingTextView; + private TextView statusesTextView; + private boolean hideFab; private int oldOffset; @@ -105,6 +111,9 @@ public class AccountActivity extends BaseActivity implements ActionButtonActivit tabLayout = findViewById(R.id.tab_layout); accountLockedView = findViewById(R.id.account_locked); container = findViewById(R.id.activity_account); + followersTextView = findViewById(R.id.followers_tv); + followingTextView = findViewById(R.id.following_tv); + statusesTextView = findViewById(R.id.statuses_btn); if (savedInstanceState != null) { accountId = savedInstanceState.getString("accountId"); @@ -139,7 +148,8 @@ public class AccountActivity extends BaseActivity implements ActionButtonActivit AppBarLayout appBarLayout = findViewById(R.id.account_app_bar_layout); final CollapsingToolbarLayout collapsingToolbar = findViewById(R.id.collapsing_toolbar); appBarLayout.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() { - @AttrRes int priorAttribute = R.attr.account_toolbar_icon_tint_uncollapsed; + @AttrRes + int priorAttribute = R.attr.account_toolbar_icon_tint_uncollapsed; @Override public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) { @@ -147,10 +157,10 @@ public class AccountActivity extends BaseActivity implements ActionButtonActivit if (collapsingToolbar.getHeight() + verticalOffset < 2 * ViewCompat.getMinimumHeight(collapsingToolbar)) { - toolbar.setTitleTextColor(ThemeUtils.getColor(AccountActivity.this, - android.R.attr.textColorPrimary)); - toolbar.setSubtitleTextColor(ThemeUtils.getColor(AccountActivity.this, - android.R.attr.textColorSecondary)); + toolbar.setTitleTextColor(ThemeUtils.getColor(AccountActivity.this, + android.R.attr.textColorPrimary)); + toolbar.setSubtitleTextColor(ThemeUtils.getColor(AccountActivity.this, + android.R.attr.textColorSecondary)); attribute = R.attr.account_toolbar_icon_tint_collapsed; } else { @@ -166,7 +176,7 @@ public class AccountActivity extends BaseActivity implements ActionButtonActivit ThemeUtils.setDrawableTint(context, toolbar.getOverflowIcon(), attribute); } - if(floatingBtn != null && hideFab && !isSelf && !blocking) { + if (floatingBtn != null && hideFab && !isSelf && !blocking) { if (verticalOffset > oldOffset) { floatingBtn.show(); } @@ -196,28 +206,62 @@ public class AccountActivity extends BaseActivity implements ActionButtonActivit } // Setup the tabs and timeline pager. - AccountPagerAdapter adapter = new AccountPagerAdapter(getSupportFragmentManager(), this, + AccountPagerAdapter adapter = new AccountPagerAdapter(getSupportFragmentManager(), accountId); String[] pageTitles = { - getString(R.string.title_statuses), - getString(R.string.title_follows), - getString(R.string.title_followers) + getString(R.string.title_statuses), + getString(R.string.title_media) }; adapter.setPageTitles(pageTitles); - ViewPager viewPager = findViewById(R.id.pager); + final ViewPager viewPager = findViewById(R.id.pager); int pageMargin = getResources().getDimensionPixelSize(R.dimen.tab_page_margin); viewPager.setPageMargin(pageMargin); Drawable pageMarginDrawable = ThemeUtils.getDrawable(this, R.attr.tab_page_margin_drawable, R.drawable.tab_page_margin_dark); viewPager.setPageMarginDrawable(pageMarginDrawable); viewPager.setAdapter(adapter); + viewPager.setOffscreenPageLimit(0); tabLayout.setupWithViewPager(viewPager); - for (int i = 0; i < tabLayout.getTabCount(); i++) { - TabLayout.Tab tab = tabLayout.getTabAt(i); - if (tab != null) { - tab.setCustomView(adapter.getTabView(i, tabLayout)); + + View.OnClickListener accountListClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + AccountListActivity.Type type; + switch (v.getId()) { + case R.id.followers_tv: + type = AccountListActivity.Type.FOLLOWERS; + break; + case R.id.following_tv: + type = AccountListActivity.Type.FOLLOWING; + break; + default: + throw new AssertionError(); + } + Intent intent = AccountListActivity.newIntent(AccountActivity.this, type, + accountId); + startActivity(intent); } - } + }; + followersTextView.setOnClickListener(accountListClickListener); + followingTextView.setOnClickListener(accountListClickListener); + + statusesTextView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + // Make nice ripple effect on tab + + //noinspection ConstantConditions + tabLayout.getTabAt(0).select(); + final View poorTabView = ((ViewGroup) tabLayout.getChildAt(0)).getChildAt(0); + poorTabView.setPressed(true); + tabLayout.postDelayed(new Runnable() { + @Override + public void run() { + poorTabView.setPressed(false); + } + }, 300); + } + }); } @Override @@ -306,11 +350,17 @@ public class AccountActivity extends BaseActivity implements ActionButtonActivit // Add counts to the tabs in the TabLayout. String[] counts = { - nf.format(Integer.parseInt(account.statusesCount)), - nf.format(Integer.parseInt(account.followingCount)), - nf.format(Integer.parseInt(account.followersCount)), + nf.format(Integer.parseInt(account.statusesCount)), + "" }; + long followersCount = Long.parseLong(account.followersCount); + long followingCount = Long.parseLong(account.followingCount); + long statusesCount = Long.parseLong(account.statusesCount); + followersTextView.setText(getString(R.string.title_x_followers, followersCount)); + followingTextView.setText(getString(R.string.title_x_following, followingCount)); + statusesTextView.setText(getString(R.string.title_x_statuses, statusesCount)); + for (int i = 0; i < tabLayout.getTabCount(); i++) { TabLayout.Tab tab = tabLayout.getTabAt(i); if (tab != null) { @@ -397,7 +447,7 @@ public class AccountActivity extends BaseActivity implements ActionButtonActivit private void updateButtons() { invalidateOptionsMenu(); - if(!isSelf && !blocking) { + if (!isSelf && !blocking) { floatingBtn.show(); followBtn.setVisibility(View.VISIBLE); @@ -449,9 +499,11 @@ public class AccountActivity extends BaseActivity implements ActionButtonActivit private String getFollowAction() { switch (followState) { default: - case NOT_FOLLOWING: return getString(R.string.action_follow); + case NOT_FOLLOWING: + return getString(R.string.action_follow); case REQUESTED: - case FOLLOWING: return getString(R.string.action_unfollow); + case FOLLOWING: + return getString(R.string.action_unfollow); } } @@ -517,8 +569,14 @@ public class AccountActivity extends BaseActivity implements ActionButtonActivit Assert.expect(followState != FollowState.REQUESTED); switch (followState) { - case NOT_FOLLOWING: { mastodonApi.followAccount(id).enqueue(cb); break; } - case FOLLOWING: { mastodonApi.unfollowAccount(id).enqueue(cb); break; } + case NOT_FOLLOWING: { + mastodonApi.followAccount(id).enqueue(cb); + break; + } + case FOLLOWING: { + mastodonApi.unfollowAccount(id).enqueue(cb); + break; + } } } @@ -655,7 +713,6 @@ public class AccountActivity extends BaseActivity implements ActionButtonActivit .mentionedUsernames(Collections.singleton(loadedAccount.username)) .build(this); startActivity(intent); - startActivity(intent); return true; } diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountListActivity.java b/app/src/main/java/com/keylesspalace/tusky/AccountListActivity.java index 735c16126..c9ad79d4e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountListActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/AccountListActivity.java @@ -15,8 +15,10 @@ package com.keylesspalace.tusky; +import android.content.Context; import android.content.Intent; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentTransaction; @@ -26,11 +28,27 @@ import android.view.MenuItem; import com.keylesspalace.tusky.fragment.AccountListFragment; -public class AccountListActivity extends BaseActivity { +public final class AccountListActivity extends BaseActivity { + + private static final String TYPE_EXTRA = "type"; + private static final String ARG_EXTRA = "arg"; + + public static Intent newIntent(@NonNull Context context, @NonNull Type type, + @Nullable String argument) { + Intent intent = new Intent(context, AccountListActivity.class); + intent.putExtra(TYPE_EXTRA, type); + if (argument != null) { + intent.putExtra(ARG_EXTRA, argument); + } + return intent; + } + enum Type { BLOCKS, MUTES, FOLLOW_REQUESTS, + FOLLOWERS, + FOLLOWING, } @Override @@ -51,29 +69,55 @@ public class AccountListActivity extends BaseActivity { ActionBar bar = getSupportActionBar(); if (bar != null) { switch (type) { - case BLOCKS: { bar.setTitle(getString(R.string.title_blocks)); break; } - case MUTES: { bar.setTitle(getString(R.string.title_mutes)); break; } + case BLOCKS: { + bar.setTitle(getString(R.string.title_blocks)); + break; + } + case MUTES: { + bar.setTitle(getString(R.string.title_mutes)); + break; + } case FOLLOW_REQUESTS: { bar.setTitle(getString(R.string.title_follow_requests)); break; } + case FOLLOWERS: + bar.setTitle(getString(R.string.title_followers)); + break; + case FOLLOWING: + bar.setTitle(getString(R.string.title_follows)); } bar.setDisplayHomeAsUpEnabled(true); bar.setDisplayShowHomeEnabled(true); } FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); - AccountListFragment.Type fragmentType; + AccountListFragment fragment; switch (type) { default: - case BLOCKS: { fragmentType = AccountListFragment.Type.BLOCKS; break; } - case MUTES: { fragmentType = AccountListFragment.Type.MUTES; break; } + case BLOCKS: { + fragment = AccountListFragment.newInstance(AccountListFragment.Type.BLOCKS); + break; + } + case MUTES: { + fragment = AccountListFragment.newInstance(AccountListFragment.Type.MUTES); + break; + } + case FOLLOWERS: { + String argument = intent.getStringExtra(ARG_EXTRA); + fragment = AccountListFragment.newInstance(AccountListFragment.Type.FOLLOWERS, argument); + break; + } + case FOLLOWING: { + String argument = intent.getStringExtra(ARG_EXTRA); + fragment = AccountListFragment.newInstance(AccountListFragment.Type.FOLLOWS, argument); + break; + } case FOLLOW_REQUESTS: { - fragmentType = AccountListFragment.Type.FOLLOW_REQUESTS; + fragment = AccountListFragment.newInstance(AccountListFragment.Type.FOLLOW_REQUESTS); break; } } - Fragment fragment = AccountListFragment.newInstance(fragmentType); fragmentTransaction.replace(R.id.fragment_container, fragment); fragmentTransaction.commit(); } diff --git a/app/src/main/java/com/keylesspalace/tusky/ReportActivity.java b/app/src/main/java/com/keylesspalace/tusky/ReportActivity.java index b6332ea15..cecfae754 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ReportActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/ReportActivity.java @@ -182,7 +182,7 @@ public class ReportActivity extends BaseActivity { onFetchStatusesFailure((Exception) t); } }; - mastodonApi.accountStatuses(accountId, null, null, null) + mastodonApi.accountStatuses(accountId, null, null, null, null) .enqueue(callback); } diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.java index a428e028d..9bd5aa3c6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.java @@ -61,6 +61,9 @@ import retrofit2.Response; public class AccountListFragment extends BaseFragment implements AccountActionListener { private static final String TAG = "AccountList"; // logging tag + public AccountListFragment() { + } + public enum Type { FOLLOWS, FOLLOWERS, @@ -75,13 +78,11 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi private RecyclerView recyclerView; private EndlessOnScrollListener scrollListener; private AccountAdapter adapter; - private TabLayout.OnTabSelectedListener onTabSelectedListener; private MastodonApi api; private boolean bottomLoading; private int bottomFetches; private boolean topLoading; private int topFetches; - private boolean hideFab; public static AccountListFragment newInstance(Type type) { Bundle arguments = new Bundle(); @@ -112,7 +113,7 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { + @Nullable Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_account_list, container, false); @@ -152,86 +153,21 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi super.onActivityCreated(savedInstanceState); BaseActivity activity = (BaseActivity) getActivity(); - if (jumpToTopAllowed()) { - TabLayout layout = activity.findViewById(R.id.tab_layout); - onTabSelectedListener = new TabLayout.OnTabSelectedListener() { - @Override - public void onTabSelected(TabLayout.Tab tab) {} - - @Override - public void onTabUnselected(TabLayout.Tab tab) {} - - @Override - public void onTabReselected(TabLayout.Tab tab) { - jumpToTop(); - } - }; - layout.addOnTabSelectedListener(onTabSelectedListener); - } - /* MastodonApi on the base activity is only guaranteed to be initialised after the parent * activity is created, so everything needing to access the api object has to be delayed * until here. */ api = activity.mastodonApi; - - - if (actionButtonPresent()) { - /* Use a modified scroll listener that both loads more statuses as it goes, and hides - * the follow button on down-scroll. */ - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getContext()); - hideFab = preferences.getBoolean("fabHide", false); - scrollListener = new EndlessOnScrollListener(layoutManager) { - @Override - public void onScrolled(RecyclerView view, int dx, int dy) { - super.onScrolled(view, dx, dy); - - ActionButtonActivity actionButtonActivity = (ActionButtonActivity) getActivity(); - FloatingActionButton composeButton = actionButtonActivity.getActionButton(); - - if (composeButton != null) { - if (hideFab) { - if (dy > 0 && composeButton.isShown()) { - composeButton.hide(); // hides the button if we're scrolling down - } else if (dy < 0 && !composeButton.isShown()) { - composeButton.show(); // shows it if we are scrolling up - } - } else if (!composeButton.isShown()) { - composeButton.show(); - } - } - } - - @Override - public void onLoadMore(int page, int totalItemsCount, RecyclerView view) { - AccountListFragment.this.onLoadMore(view); - } - }; - } else { - // Just use the basic scroll listener to load more accounts. - scrollListener = new EndlessOnScrollListener(layoutManager) { - @Override - public void onLoadMore(int page, int totalItemsCount, RecyclerView view) { - AccountListFragment.this.onLoadMore(view); - } - }; - } + // Just use the basic scroll listener to load more accounts. + scrollListener = new EndlessOnScrollListener(layoutManager) { + @Override + public void onLoadMore(int page, int totalItemsCount, RecyclerView view) { + AccountListFragment.this.onLoadMore(view); + } + }; recyclerView.addOnScrollListener(scrollListener); } - private boolean actionButtonPresent() { - return type == Type.FOLLOWS || type == Type.FOLLOWERS; - } - - @Override - public void onDestroyView() { - if (jumpToTopAllowed()) { - TabLayout tabLayout = getActivity().findViewById(R.id.tab_layout); - tabLayout.removeOnTabSelectedListener(onTabSelectedListener); - } - super.onDestroyView(); - } - @Override public void onViewAccount(String id) { Intent intent = new Intent(getContext(), AccountActivity.class); @@ -420,10 +356,6 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi Log.e(TAG, message); } - private boolean jumpToTopAllowed() { - return type == Type.FOLLOWS || type == Type.FOLLOWERS; - } - private void jumpToTop() { layoutManager.scrollToPositionWithOffset(0, 0); scrollListener.reset(); @@ -437,11 +369,16 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi private Call> 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); + 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); } } @@ -491,7 +428,7 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi } private void onFetchAccountsSuccess(List accounts, String linkHeader, - FetchEnd fetchEnd) { + FetchEnd fetchEnd) { List links = HttpHeaderLink.parse(linkHeader); switch (fetchEnd) { case TOP: { diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt new file mode 100644 index 000000000..ebcdab985 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt @@ -0,0 +1,288 @@ +/* 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 . */ + +package com.keylesspalace.tusky.fragment + +import android.content.Context +import android.content.Intent +import android.graphics.Color +import android.os.Bundle +import android.preference.PreferenceManager +import android.support.v4.app.ActivityOptionsCompat +import android.support.v4.content.ContextCompat +import android.support.v4.view.ViewCompat +import android.support.v4.widget.SwipeRefreshLayout +import android.support.v7.widget.GridLayoutManager +import android.support.v7.widget.RecyclerView +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import com.keylesspalace.tusky.BaseActivity +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.ViewMediaActivity +import com.keylesspalace.tusky.ViewVideoActivity +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.view.SquareImageView +import com.squareup.picasso.Picasso +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import java.util.* + +/** + * Created by charlag on 26/10/2017. + * + * Fragment with multiple columns of media previews for the specified account. + */ + +class AccountMediaFragment : BaseFragment() { + + companion object { + @JvmStatic + fun newInstance(accountId: String): AccountMediaFragment { + val fragment = AccountMediaFragment() + fragment.arguments = Bundle() + fragment.arguments.putString(ACCOUNT_ID_ARG, accountId) + return fragment + } + + private const val ACCOUNT_ID_ARG = "account_id" + private const val TAG = "AccountMediaFragment" + } + + private val adapter = MediaGridAdapter() + private var currentCall: Call>? = null + private lateinit var api: MastodonApi + private val statuses = mutableListOf() + private var fetchingStatus = FetchingStatus.NOT_FETCHING + lateinit private var swipeLayout: SwipeRefreshLayout + + private val callback = object : Callback> { + override fun onFailure(call: Call>?, t: Throwable?) { + fetchingStatus = FetchingStatus.NOT_FETCHING + swipeLayout.isRefreshing = false + Log.d(TAG, "Failed to fetch account media", t) + } + + override fun onResponse(call: Call>, response: Response>) { + fetchingStatus = FetchingStatus.NOT_FETCHING + swipeLayout.isRefreshing = false + val body = response.body() + body?.let { fetched -> + statuses.addAll(0, fetched) + // flatMap requires iterable but I don't want to box each array into list + val result = mutableListOf() + for (status in fetched) { + result.addAll(status.attachments) + } + adapter.addTop(result) + } + } + } + + private val bottomCallback = object : Callback> { + override fun onFailure(call: Call>?, t: Throwable?) { + fetchingStatus = FetchingStatus.NOT_FETCHING + Log.d(TAG, "Failed to fetch account media", t) + } + + override fun onResponse(call: Call>, response: Response>) { + fetchingStatus = FetchingStatus.NOT_FETCHING + val body = response.body() + body?.let { fetched -> + Log.d(TAG, "fetched ${fetched.size} statuses") + if (fetched.isNotEmpty()) Log.d(TAG, "first: ${fetched.first().id}, last: ${fetched.last().id}") + statuses.addAll(fetched) + Log.d(TAG, "now there are ${statuses.size} statuses") + // flatMap requires iterable but I don't want to box each array into list + val result = mutableListOf() + for (status in fetched) { + result.addAll(status.attachments) + } + adapter.addBottom(result) + } + } + + } + + override fun onAttach(context: Context) { + super.onAttach(context) + // we should get rid of this + api = (context as BaseActivity).mastodonApi + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + val view = inflater.inflate(R.layout.fragment_timeline, container, false) + val recyclerView = view.findViewById(R.id.recycler_view) + val columnCount = context.resources.getInteger(R.integer.profile_media_column_count) + val layoutManager = GridLayoutManager(context, columnCount) + + val lightThemeEnabled = PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean("lightTheme", false) + val bgRes = if (lightThemeEnabled) R.color.window_background_light + else R.color.window_background_dark + adapter.baseItemColor = ContextCompat.getColor(recyclerView.context, bgRes) + + recyclerView.layoutManager = layoutManager + recyclerView.adapter = adapter + + val accountId = arguments.getString(ACCOUNT_ID_ARG) + + swipeLayout = view.findViewById(R.id.swipe_refresh_layout) + swipeLayout.setOnRefreshListener { + if (fetchingStatus != FetchingStatus.NOT_FETCHING) return@setOnRefreshListener + currentCall = if (statuses.isEmpty()) { + fetchingStatus = FetchingStatus.INITIAL_FETCHING + api.accountStatuses(accountId, null, null, null, true) + } else { + fetchingStatus = FetchingStatus.REFRESHING + api.accountStatuses(accountId, null, statuses[0].id, null, true) + } + currentCall?.enqueue(callback) + + } + + recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { + + override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) { + if (dy > 0) { + val itemCount = layoutManager.itemCount + val lastItem = layoutManager.findLastCompletelyVisibleItemPosition() + if (itemCount <= lastItem + 3 && fetchingStatus == FetchingStatus.NOT_FETCHING) { + statuses.lastOrNull()?.let { last -> + Log.d(TAG, "Requesting statuses with max_id: ${last.id}, (bottom)") + fetchingStatus = FetchingStatus.FETCHING_BOTTOM + currentCall = api.accountStatuses(accountId, last.id, null, null, true) + currentCall?.enqueue(bottomCallback) + } + } + } + } + }) + + return view + } + + // That's sort of an optimization to only load media once user has opened the tab + override fun setUserVisibleHint(isVisibleToUser: Boolean) { + super.setUserVisibleHint(isVisibleToUser) + if (!isVisibleToUser) return + val accountId = arguments.getString(ACCOUNT_ID_ARG) + if (fetchingStatus == FetchingStatus.NOT_FETCHING && statuses.isEmpty()) { + fetchingStatus = FetchingStatus.INITIAL_FETCHING + currentCall = api.accountStatuses(accountId, null, null, null, true) + currentCall?.enqueue(callback) + } + } + + private fun viewMedia(items: List, currentIndex: Int, view: View?) { + val urls = items.map { it.url }.toTypedArray() + val type = items[currentIndex].type + + when (type) { + Status.MediaAttachment.Type.IMAGE -> { + val intent = Intent(context, ViewMediaActivity::class.java) + intent.putExtra("urls", urls) + intent.putExtra("urlIndex", currentIndex) + if (view != null) { + val url = urls[currentIndex] + ViewCompat.setTransitionName(view, url) + val options = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, + view, url) + startActivity(intent, options.toBundle()) + } else { + startActivity(intent) + } + } + Status.MediaAttachment.Type.GIFV, Status.MediaAttachment.Type.VIDEO -> { + val intent = Intent(context, ViewVideoActivity::class.java) + intent.putExtra("url", urls[currentIndex]) + startActivity(intent) + } + Status.MediaAttachment.Type.UNKNOWN, null -> { + }/* Intentionally do nothing. This case is here is to handle when new attachment + * types are added to the API before code is added here to handle them. So, the + * best fallback is to just show the preview and ignore requests to view them. */ + } + } + + private enum class FetchingStatus { + NOT_FETCHING, INITIAL_FETCHING, FETCHING_BOTTOM, REFRESHING + } + + inner class MediaGridAdapter + : RecyclerView.Adapter() { + + var baseItemColor = Color.BLACK + + private val items = mutableListOf() + private val itemBgBaseHSV = FloatArray(3) + private val random = Random() + + fun addTop(newItems: List) { + items.addAll(0, newItems) + notifyItemRangeInserted(0, newItems.size) + } + + fun addBottom(newItems: List) { + if (newItems.isEmpty()) return + + val oldLen = items.size + items.addAll(newItems) + notifyItemRangeInserted(oldLen, newItems.size) + } + + override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { + val hsv = FloatArray(3) + Color.colorToHSV(baseItemColor, hsv) + super.onAttachedToRecyclerView(recyclerView) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediaViewHolder { + val view = SquareImageView(parent.context) + view.scaleType = ImageView.ScaleType.CENTER_CROP + return MediaViewHolder(view) + } + + override fun getItemCount(): Int = items.size + + override fun onBindViewHolder(holder: MediaViewHolder, position: Int) { + itemBgBaseHSV[2] = random.nextFloat() * (1f - 0.3f) + 0.3f + holder.imageView.setBackgroundColor(Color.HSVToColor(itemBgBaseHSV)) + val item = items[position] + Picasso.with(holder.imageView.context) + .load(item.previewUrl) + .into(holder.imageView) + } + + + inner class MediaViewHolder(val imageView: ImageView) + : RecyclerView.ViewHolder(imageView), + View.OnClickListener { + init { + itemView.setOnClickListener(this) + } + + // saving some allocations + override fun onClick(v: View?) { + viewMedia(items, adapterPosition, imageView) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java index d5d6da60c..d3c9b3dfe 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java @@ -20,6 +20,7 @@ import android.content.SharedPreferences; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.preference.PreferenceManager; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.design.widget.FloatingActionButton; import android.support.design.widget.TabLayout; @@ -263,7 +264,7 @@ public class TimelineFragment extends SFragment implements final Status status = statuses.get(position); super.reblogWithCallback(status, reblog, new Callback() { @Override - public void onResponse(Call call, retrofit2.Response response) { + public void onResponse(@NonNull Call call, @NonNull retrofit2.Response response) { if (response.isSuccessful()) { status.reblogged = reblog; @@ -280,7 +281,7 @@ public class TimelineFragment extends SFragment implements } @Override - public void onFailure(Call call, Throwable t) { + public void onFailure(@NonNull Call call, @NonNull Throwable t) { Log.d(TAG, "Failed to reblog status " + status.id); t.printStackTrace(); } @@ -293,7 +294,7 @@ public class TimelineFragment extends SFragment implements super.favouriteWithCallback(status, favourite, new Callback() { @Override - public void onResponse(Call call, retrofit2.Response response) { + public void onResponse(@NonNull Call call, @NonNull retrofit2.Response response) { if (response.isSuccessful()) { status.favourited = favourite; @@ -310,7 +311,7 @@ public class TimelineFragment extends SFragment implements } @Override - public void onFailure(Call call, Throwable t) { + public void onFailure(@NonNull Call call, @NonNull Throwable t) { Log.d(TAG, "Failed to favourite status " + status.id); t.printStackTrace(); } @@ -462,7 +463,7 @@ public class TimelineFragment extends SFragment implements case TAG: return api.hashtagTimeline(tagOrId, null, fromId, uptoId, null); case USER: - return api.accountStatuses(tagOrId, fromId, uptoId, null); + return api.accountStatuses(tagOrId, fromId, uptoId, null, null); case FAVOURITES: return api.favourites(fromId, uptoId, null); } @@ -495,7 +496,7 @@ public class TimelineFragment extends SFragment implements Callback> callback = new Callback>() { @Override - public void onResponse(Call> call, Response> response) { + public void onResponse(@NonNull Call> call, @NonNull Response> response) { if (response.isSuccessful()) { String linkHeader = response.headers().get("Link"); onFetchTimelineSuccess(response.body(), linkHeader, fetchEnd); @@ -505,7 +506,7 @@ public class TimelineFragment extends SFragment implements } @Override - public void onFailure(Call> call, Throwable t) { + public void onFailure(@NonNull Call> call, @NonNull Throwable t) { onFetchTimelineFailure((Exception) t, fetchEnd); } }; diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java index 78003e1eb..502eeb39f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java @@ -15,6 +15,8 @@ package com.keylesspalace.tusky.network; +import android.support.annotation.Nullable; + import com.keylesspalace.tusky.entity.AccessToken; import com.keylesspalace.tusky.entity.Account; import com.keylesspalace.tusky.entity.AppCredentials; @@ -127,12 +129,25 @@ public interface MastodonApi { @Query("limit") Integer limit); @GET("api/v1/accounts/{id}") Call account(@Path("id") String accountId); + + /** + * Method to fetch statuses for the specified account. + * @param accountId ID for account for which statuses will be requested + * @param maxId Only statuses with ID less than maxID will be returned + * @param sinceId Only statuses with ID bigger than sinceID will be returned + * @param limit Limit returned statuses (current API limits: default - 20, max - 40) + * @param onlyMedia Should server return only statuses which contain media. Caution! The server + * works in a weird way so if any value if present at this field it will be + * interpreted as "true". Pass null to return all statuses. + * @return + */ @GET("api/v1/accounts/{id}/statuses") Call> accountStatuses( @Path("id") String accountId, @Query("max_id") String maxId, @Query("since_id") String sinceId, - @Query("limit") Integer limit); + @Query("limit") Integer limit, + @Nullable @Query("only_media") Boolean onlyMedia); @GET("api/v1/accounts/{id}/followers") Call> accountFollowers( @Path("id") String accountId, diff --git a/app/src/main/java/com/keylesspalace/tusky/pager/AccountPagerAdapter.java b/app/src/main/java/com/keylesspalace/tusky/pager/AccountPagerAdapter.java index 0929c1675..f031186f5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/pager/AccountPagerAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/pager/AccountPagerAdapter.java @@ -15,27 +15,19 @@ package com.keylesspalace.tusky.pager; -import android.content.Context; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentPagerAdapter; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; -import com.keylesspalace.tusky.R; -import com.keylesspalace.tusky.fragment.AccountListFragment; +import com.keylesspalace.tusky.fragment.AccountMediaFragment; import com.keylesspalace.tusky.fragment.TimelineFragment; public class AccountPagerAdapter extends FragmentPagerAdapter { - private Context context; private String accountId; private String[] pageTitles; - public AccountPagerAdapter(FragmentManager manager, Context context, String accountId) { + public AccountPagerAdapter(FragmentManager manager, String accountId) { super(manager); - this.context = context; this.accountId = accountId; } @@ -50,31 +42,21 @@ public class AccountPagerAdapter extends FragmentPagerAdapter { return TimelineFragment.newInstance(TimelineFragment.Kind.USER, accountId); } case 1: { - return AccountListFragment.newInstance(AccountListFragment.Type.FOLLOWS, accountId); - } - case 2: { - return AccountListFragment.newInstance(AccountListFragment.Type.FOLLOWERS, accountId); + return AccountMediaFragment.newInstance(accountId); } default: { - return null; + throw new AssertionError("Page " + position + " is out of AccountPagerAdapter bounds"); } } } @Override public int getCount() { - return 3; + return 2; } @Override public CharSequence getPageTitle(int position) { return pageTitles[position]; } - - public View getTabView(int position, ViewGroup root) { - View view = LayoutInflater.from(context).inflate(R.layout.tab_account, root, false); - TextView title = view.findViewById(R.id.title); - title.setText(pageTitles[position]); - return view; - } } diff --git a/app/src/main/java/com/keylesspalace/tusky/view/SquareImageView.kt b/app/src/main/java/com/keylesspalace/tusky/view/SquareImageView.kt new file mode 100644 index 000000000..5e2330efb --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/view/SquareImageView.kt @@ -0,0 +1,24 @@ +package com.keylesspalace.tusky.view + +import android.content.Context +import android.util.AttributeSet +import android.widget.ImageView + +/** + * Created by charlag on 26/10/2017. + */ + +class SquareImageView : ImageView { + constructor(context: Context) : super(context) + + constructor(context: Context, attributes: AttributeSet) : super(context, attributes) + + constructor(context: Context, attributes: AttributeSet, defStyleAttr: Int) + : super(context, attributes, defStyleAttr) + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + val width = measuredWidth + setMeasuredDimension(width, width) + } +} \ No newline at end of file diff --git a/app/src/main/res/color/account_tab_font_color.xml b/app/src/main/res/color/account_tab_font_color.xml index accc21cbc..c81c01a26 100644 --- a/app/src/main/res/color/account_tab_font_color.xml +++ b/app/src/main/res/color/account_tab_font_color.xml @@ -1,5 +1,5 @@ - - + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_account.xml b/app/src/main/res/layout/activity_account.xml index a1788cb45..16359a8dd 100644 --- a/app/src/main/res/layout/activity_account.xml +++ b/app/src/main/res/layout/activity_account.xml @@ -40,7 +40,7 @@ android:scaleType="centerCrop" app:layout_collapseMode="pin" /> -