Merge branch 'master' into timeline-improvement

This commit is contained in:
Konrad Pozniak 2017-11-07 13:03:49 +01:00 committed by GitHub
commit 21de60f739
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 647 additions and 194 deletions

View File

@ -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()
}

View File

@ -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) {
@ -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_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
@ -307,10 +351,16 @@ 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)),
""
};
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;
}

View File

@ -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();
}

View File

@ -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);
}

View File

@ -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();
@ -152,61 +153,10 @@ 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
@ -214,24 +164,10 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
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<List<Account>> getFetchCallByListType(Type type, String fromId, String uptoId) {
switch (type) {
default:
case FOLLOWS: return api.accountFollowing(accountId, fromId, uptoId, null);
case FOLLOWERS: return api.accountFollowers(accountId, fromId, uptoId, null);
case BLOCKS: return api.blocks(fromId, uptoId, null);
case MUTES: return api.mutes(fromId, uptoId, null);
case FOLLOW_REQUESTS: return api.followRequests(fromId, uptoId, null);
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);
}
}

View File

@ -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 <http://www.gnu.org/licenses>. */
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<List<Status>>? = null
private lateinit var api: MastodonApi
private val statuses = mutableListOf<Status>()
private var fetchingStatus = FetchingStatus.NOT_FETCHING
lateinit private var swipeLayout: SwipeRefreshLayout
private val callback = object : Callback<List<Status>> {
override fun onFailure(call: Call<List<Status>>?, t: Throwable?) {
fetchingStatus = FetchingStatus.NOT_FETCHING
swipeLayout.isRefreshing = false
Log.d(TAG, "Failed to fetch account media", t)
}
override fun onResponse(call: Call<List<Status>>, response: Response<List<Status>>) {
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<Status.MediaAttachment>()
for (status in fetched) {
result.addAll(status.attachments)
}
adapter.addTop(result)
}
}
}
private val bottomCallback = object : Callback<List<Status>> {
override fun onFailure(call: Call<List<Status>>?, t: Throwable?) {
fetchingStatus = FetchingStatus.NOT_FETCHING
Log.d(TAG, "Failed to fetch account media", t)
}
override fun onResponse(call: Call<List<Status>>, response: Response<List<Status>>) {
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<Status.MediaAttachment>()
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<RecyclerView>(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<SwipeRefreshLayout>(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<Status.MediaAttachment>, 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<MediaGridAdapter.MediaViewHolder>() {
var baseItemColor = Color.BLACK
private val items = mutableListOf<Status.MediaAttachment>()
private val itemBgBaseHSV = FloatArray(3)
private val random = Random()
fun addTop(newItems: List<Status.MediaAttachment>) {
items.addAll(0, newItems)
notifyItemRangeInserted(0, newItems.size)
}
fun addBottom(newItems: List<Status.MediaAttachment>) {
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)
}
}
}
}

View File

@ -290,6 +290,7 @@ public class TimelineFragment extends SFragment implements
super.reblogWithCallback(status, reblog, new Callback<Status>() {
@Override
public void onResponse(@NonNull Call<Status> call, @NonNull Response<Status> response) {
if (response.isSuccessful()) {
status.reblogged = reblog;
@ -324,6 +325,7 @@ public class TimelineFragment extends SFragment implements
super.favouriteWithCallback(status, favourite, new Callback<Status>() {
@Override
public void onResponse(@NonNull Call<Status> call, @NonNull Response<Status> response) {
if (response.isSuccessful()) {
status.favourited = favourite;

View File

@ -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> 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<List<Status>> 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<List<Account>> accountFollowers(
@Path("id") String accountId,

View File

@ -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;
}
}

View File

@ -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)
}
}

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="false" android:color="?android:attr/textColorTertiary"/>
<item android:state_selected="true" android:color="?attr/colorAccent"/>
<item android:state_pressed="false" android:color="?android:attr/textColorPrimary"/>
<item android:state_pressed="true" android:color="?android:attr/textColorTertiary"/>
</selector>

View File

@ -40,7 +40,7 @@
android:scaleType="centerCrop"
app:layout_collapseMode="pin" />
<RelativeLayout
<android.support.constraint.ConstraintLayout
android:id="@+id/account_header_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -54,71 +54,67 @@
android:id="@+id/account_avatar"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_marginEnd="10dp"
android:layout_marginRight="10dp"
android:layout_toLeftOf="@+id/follow_btn"
android:layout_toStartOf="@+id/follow_btn"
android:src="@drawable/avatar_default"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shadow="true" />
<Button
android:id="@+id/follow_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_marginTop="6dp" />
android:layout_marginTop="6dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/account_follows_you"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_below="@id/follow_btn"
android:layout_marginEnd="10dp"
android:layout_marginRight="10dp"
android:text="@string/follows_you"
android:textColor="?android:textColorPrimary" />
android:textColor="?android:textColorPrimary"
app:layout_constraintEnd_toEndOf="@id/follow_btn"
app:layout_constraintTop_toBottomOf="@id/follow_btn" />
<TextView
android:id="@+id/account_display_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/account_avatar"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?android:textColorPrimary"
android:textSize="18sp"
android:textStyle="normal|bold"
app:layout_constraintTop_toBottomOf="@id/account_avatar"
tools:text="Tusky Mastodon Client" />
<TextView
android:id="@+id/account_username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/account_display_name"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?android:textColorSecondary"
app:layout_constraintTop_toBottomOf="@id/account_display_name"
tools:text="\@Tusky" />
<ImageView
android:id="@+id/account_locked"
android:layout_width="16sp"
android:layout_height="16sp"
android:layout_alignBottom="@id/account_username"
android:layout_centerVertical="true"
android:layout_marginLeft="4dp"
android:layout_marginStart="4dp"
android:layout_toEndOf="@id/account_username"
android:layout_toRightOf="@id/account_username"
android:contentDescription="@string/description_account_locked"
android:tint="?android:textColorSecondary"
android:visibility="gone"
app:srcCompat="@drawable/reblog_disabled_light" />
app:layout_constraintBottom_toBottomOf="@id/account_username"
app:layout_constraintLeft_toRightOf="@id/account_username"
app:srcCompat="@drawable/reblog_disabled_light"
tools:visibility="visible" />
<TextView
android:id="@+id/account_note"
@ -128,9 +124,46 @@
android:paddingBottom="16dp"
android:paddingTop="10dp"
android:textColor="?android:textColorTertiary"
app:layout_constraintTop_toBottomOf="@id/account_username"
tools:text="This is a test description" />
</RelativeLayout>
<TextView
android:id="@+id/followers_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/account_note"
tools:text="3000 Followers"
android:background="@android:color/transparent"
android:textColor="@color/account_tab_font_color"
app:layout_constraintHorizontal_bias="0"/>
<TextView
android:id="@+id/following_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="@id/followers_tv"
app:layout_constraintEnd_toStartOf="@id/statuses_btn"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@id/followers_tv"
app:layout_constraintTop_toTopOf="@id/followers_tv"
tools:text="500 Following"
android:textColor="@color/account_tab_font_color" />
<TextView
android:id="@+id/statuses_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toEndOf="@id/following_tv"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/followers_tv"
tools:text="3000 Posts"
app:layout_constraintHorizontal_bias="0"
android:textColor="@color/account_tab_font_color"/>
</android.support.constraint.ConstraintLayout>
</RelativeLayout>
@ -158,12 +191,8 @@
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabBackground="?android:colorBackground"
app:tabGravity="fill">
<android.support.design.widget.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
android:background="?android:colorBackground"
app:tabSelectedTextColor="?attr/colorAccent" >
<android.support.design.widget.TabItem
android:layout_width="wrap_content"

View File

@ -1,26 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true">
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical">
<TextView
<android.support.v7.widget.AppCompatTextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_gravity="center_horizontal"
android:layout_marginTop="6dp"
android:textAllCaps="true"
android:textColor="@color/account_tab_font_color"
android:textStyle="normal|bold" />
android:textStyle="normal|bold"
tools:text="Followers"
android:singleLine="true"
app:autoSizeTextType="uniform"
app:autoSizeMinTextSize="12sp"
app:autoSizeMaxTextSize="14sp"
app:autoSizeStepGranularity="2sp"
android:textAlignment="center"
android:ellipsize="middle"/>
<TextView
android:id="@+id/total"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/title"
android:layout_centerHorizontal="true"
android:layout_gravity="center_horizontal"
android:textColor="@color/account_tab_font_color"
android:textSize="12sp" />
android:textSize="12sp"
tools:text="2,412"/>
</RelativeLayout>
</LinearLayout>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="pull_notification_check_interval_names">
<item>15分</item>
<item>20分</item>
<item>25分</item>
<item>30分</item>
<item>45分</item>
<item>1時間</item>
<item>2時間</item>
</string-array>
</resources>

View File

@ -100,9 +100,11 @@
<string name="action_reject">拒否</string>
<string name="action_search">検索</string>
<string name="action_access_saved_toot">下書き</string>
<string name="action_copy_link">リンクをコピー</string>
<string name="download_image">%1$s をダウンロードしています</string>
<string name="action_copy_link">リンクをコピー</string>
<string name="send_status_link_to">トゥートのURLを共有…</string>
<string name="send_status_content_to">トゥートを共有…</string>
@ -127,14 +129,16 @@
<string name="login_connection">接続中…</string>
<string name="dialog_whats_an_instance">mastodon.social, mstdn.jp, pawoo.net や
<a href="https://instances.social">その他</a>
のような、あらゆるインスタンスのアドレスやドメインを入力できます。
\n\nまだアカウントをお持ちでない場合は、参加したいインスタンスの名前を入力することで
そのインスタンスにアカウントを作成できます。\n\nインスタンスはあなたのアカウントが提供される単独の場所ですが、
他のインスタンスのユーザーとあたかも同じ場所にいるように簡単にコミュニケーションをとったりフォローしたりできます。
<string name="dialog_whats_an_instance">mastodon.social, mstdn.jp, pawoo.netや<!--
--><a href="https://instances.social">その他</a><!--
-->のような、あらゆるインスタンスのアドレスやドメインを入力できます。
\n\nまだアカウントをお持ちでない場合は、参加したいインスタンスの名前を入力することで<!--
-->そのインスタンスにアカウントを作成できます。
\n\nインスタンスはあなたのアカウントが提供される単独の場所ですが、<!--
-->他のインスタンスのユーザーとあたかも同じ場所にいるように簡単にコミュニケーションをとったりフォローしたりできます。
\n\nさらに詳しい情報は<a href="https://joinmastodon.org">joinmastodon.org</a>でご覧いただけます。
</string>
<!-- Commented out line breaks to avoid appearing spaces in the middle of a sentence. -->
<string name="dialog_title_finishing_media_upload">メディアをアップロードしています</string>
<string name="dialog_message_uploading_media">アップロード中…</string>
<string name="dialog_download_image">ダウンロード</string>
@ -170,6 +174,15 @@
<string name="pref_title_show_replies">返信を表示</string>
<string name="pref_title_show_media_preview">メディアのプレビューを表示する</string>
<string name="notification_channel_mention_name">新しい返信</string>
<string name="notification_channel_mention_descriptions">新しい返信の通知</string>
<string name="notification_channel_follow_name">新しいフォロワー</string>
<string name="notification_channel_follow_description">新しいフォロワーの通知</string>
<string name="notification_channel_boost_name">ブースト</string>
<string name="notification_channel_boost_description">あなたのトゥートがブーストされたときの通知</string>
<string name="notification_channel_favourite_name">お気に入り</string>
<string name="notification_channel_favourite_description">あなたのトゥートがお気に入りに登録されたときの通知</string>
<string name="notification_mention_format">%sさんが返信しました</string>
<string name="notification_summary_large">%1$sさん、%2$sさん、%3$sさんと他%4$d人</string>
<string name="notification_summary_medium">%1$sさん、%2$sさん、%3$sさん</string>
@ -180,12 +193,17 @@
<string name="about_title_activity">このアプリについて</string>
<string name="about_application_version">アプリのバージョン:%s</string>
<string name="about_tusky_version">Tusky %s</string>
<string name="about_tusky_license">Tuskyは無料のオープンソースソフトウェアです。<!--
-->GNU General Public License Version 3 の下で使用許諾されています。<!--
-->ライセンスはここからご覧いただけます: https://www.gnu.org/licenses/gpl-3.0.ja.html</string>
<!-- Commented out line breaks brcause inserting a space after the japanese period is incorrect. -->
<string name="about_project_site">
プロジェクトのWebサイト\n
プロジェクトのWebサイト(英語)\n
https://tusky.keylesspalace.com
</string>-->
</string>
<string name="about_bug_feature_request_site">
バグ報告 &amp; 機能リクエスト:\n
バグ報告 &amp; 機能リクエスト(英語)\n
https://github.com/Vavassor/Tusky/issues
</string>
<string name="about_tusky_account">Tusky公式アカウント</string>
@ -200,7 +218,20 @@
<string name="no_content">下書きはありません</string>
<string name="action_save_one_toot">下書きに保存しました!</string>
<!--These are for timestamps on statuses. For example: "16s" or "2d"-->
<string name="abbreviated_in_years">%d年後</string>
<string name="abbreviated_in_days">%d日後</string>
<string name="abbreviated_in_hours">%d時間後</string>
<string name="abbreviated_in_minutes">%d分後</string>
<string name="abbreviated_in_seconds">%d秒後</string>
<string name="abbreviated_years_ago">%d年前</string>
<string name="abbreviated_days_ago">%d日前</string>
<string name="abbreviated_hours_ago">%d時間前</string>
<string name="abbreviated_minutes_ago">%d分前</string>
<string name="abbreviated_seconds_ago">%d秒前</string>
<string name="follows_you">あなたをフォロー中</string>
<string name="pref_title_alway_show_sensitive_media">常にすべての閲覧注意なコンテンツを表示する</string>
<string name="pref_title_alway_show_sensitive_media">閲覧注意のコンテンツを常に表示</string>
<string name="replying_to">@%sに返信</string>
</resources>

View File

@ -218,6 +218,7 @@
<string name="follows_you">Подписан(а) на вас</string>
<string name="pref_title_alway_show_sensitive_media">Всегда показывать NSFW-контент</string>
<string name="title_media">Медиа</string>
<string name="replying_to">Ответ @%s</string>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="profile_media_column_count">2</integer>
</resources>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="profile_media_column_count">3</integer>
</resources>

View File

@ -33,6 +33,9 @@
<string name="title_follow_requests">Follow Requests</string>
<string name="title_edit_profile">Edit your profile</string>
<string name="title_saved_toot">Drafts</string>
<string name="title_x_followers"><b>%d</b> Followers</string>
<string name="title_x_following"><b>%d</b> Following</string>
<string name="title_x_statuses"><b>%d</b> Posts</string>
<string name="status_username_format">\@%s</string>
<string name="status_boosted_format">%s boosted</string>
@ -232,6 +235,7 @@
<string name="follows_you">Follows you</string>
<string name="pref_title_alway_show_sensitive_media">Always show all nsfw content</string>
<string name="title_media">Media</string>
<string name="replying_to">Replying to @%s</string>
<string name="load_more_placeholder_text">load more</string>

View File

@ -1,12 +1,14 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.1.51'
repositories {
jcenter()
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}