From c869886c1921b0ce61d0fa2a49547067800f5d5b Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Thu, 27 Dec 2018 09:48:24 +0100 Subject: [PATCH] add the ability to see who faved or boosted a toot (#962) * move reblog/fav count up in detailed status view and make them clickable * use status object returned by api when reblogging/faving * Reblogs -> Boosts * add support for viewing who faved/reblogged a status * add onShowReblogs/onShowFavs to listener, fix display bug * remove unneeded icon from previous revision * small code improvements * fix liking/boosting toot with card --- .../keylesspalace/tusky/AccountActivity.kt | 2 +- .../tusky/AccountListActivity.java | 149 ------- .../tusky/AccountListActivity.kt | 102 +++++ .../tusky/adapter/AccountAdapter.java | 7 +- .../adapter/StatusDetailedViewHolder.java | 52 ++- .../tusky/fragment/AccountListFragment.java | 404 ------------------ .../tusky/fragment/AccountListFragment.kt | 336 +++++++++++++++ .../tusky/fragment/ViewThreadFragment.java | 103 +++-- .../interfaces/StatusActionListener.java | 13 + .../tusky/network/MastodonApi.java | 35 +- .../main/res/layout/fragment_account_list.xml | 2 +- .../main/res/layout/item_status_detailed.xml | 81 ++-- app/src/main/res/values/strings.xml | 13 + 13 files changed, 651 insertions(+), 648 deletions(-) delete mode 100644 app/src/main/java/com/keylesspalace/tusky/AccountListActivity.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/AccountListActivity.kt delete mode 100644 app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt index eaac74d6f..194c65d6b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt @@ -264,7 +264,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasSupportF val accountListClickListener = { v: View -> val type = when (v.id) { R.id.accountFollowers-> AccountListActivity.Type.FOLLOWERS - R.id.accountFollowing -> AccountListActivity.Type.FOLLOWING + R.id.accountFollowing -> AccountListActivity.Type.FOLLOWS else -> throw AssertionError() } val accountListIntent = AccountListActivity.newIntent(this, type, accountId) diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountListActivity.java b/app/src/main/java/com/keylesspalace/tusky/AccountListActivity.java deleted file mode 100644 index eb63a16be..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/AccountListActivity.java +++ /dev/null @@ -1,149 +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 . */ - -package com.keylesspalace.tusky; - -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentTransaction; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.widget.Toolbar; -import android.view.MenuItem; - -import com.keylesspalace.tusky.fragment.AccountListFragment; - -import javax.inject.Inject; - -import dagger.android.AndroidInjector; -import dagger.android.DispatchingAndroidInjector; -import dagger.android.support.HasSupportFragmentInjector; - -public final class AccountListActivity extends BaseActivity implements HasSupportFragmentInjector { - - @Inject - public DispatchingAndroidInjector dispatchingAndroidInjector; - - 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; - } - - public enum Type { - BLOCKS, - MUTES, - FOLLOW_REQUESTS, - FOLLOWERS, - FOLLOWING, - } - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_account_list); - - Type type; - Intent intent = getIntent(); - if (intent != null) { - type = (Type) intent.getSerializableExtra("type"); - } else { - type = Type.BLOCKS; - } - - Toolbar toolbar = findViewById(R.id.toolbar); - setSupportActionBar(toolbar); - 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 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 fragment; - switch (type) { - default: - 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: { - fragment = AccountListFragment.newInstance(AccountListFragment.Type.FOLLOW_REQUESTS); - break; - } - } - fragmentTransaction.replace(R.id.fragment_container, fragment); - fragmentTransaction.commit(); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: { - onBackPressed(); - return true; - } - } - return super.onOptionsItemSelected(item); - } - - @Override - public AndroidInjector supportFragmentInjector() { - return dispatchingAndroidInjector; - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountListActivity.kt b/app/src/main/java/com/keylesspalace/tusky/AccountListActivity.kt new file mode 100644 index 000000000..8421a1370 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/AccountListActivity.kt @@ -0,0 +1,102 @@ +/* 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 + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.MenuItem + +import com.keylesspalace.tusky.fragment.AccountListFragment + +import javax.inject.Inject + +import dagger.android.AndroidInjector +import dagger.android.DispatchingAndroidInjector +import dagger.android.support.HasSupportFragmentInjector +import kotlinx.android.synthetic.main.toolbar_basic.* + +class AccountListActivity : BaseActivity(), HasSupportFragmentInjector { + + @Inject + lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector + + enum class Type { + FOLLOWS, + FOLLOWERS, + BLOCKS, + MUTES, + FOLLOW_REQUESTS, + REBLOGGED, + FAVOURITED + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_account_list) + + val type = intent.getSerializableExtra(EXTRA_TYPE) as Type + val id: String? = intent.getStringExtra(EXTRA_ID) + + setSupportActionBar(toolbar) + supportActionBar?.apply { + when (type) { + AccountListActivity.Type.BLOCKS -> setTitle(R.string.title_blocks) + AccountListActivity.Type.MUTES -> setTitle(R.string.title_mutes) + AccountListActivity.Type.FOLLOW_REQUESTS -> setTitle(R.string.title_follow_requests) + AccountListActivity.Type.FOLLOWERS -> setTitle(R.string.title_followers) + AccountListActivity.Type.FOLLOWS -> setTitle(R.string.title_follows) + AccountListActivity.Type.REBLOGGED -> setTitle(R.string.title_reblogged_by) + AccountListActivity.Type.FAVOURITED -> setTitle(R.string.title_favourited_by) + } + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + } + + supportFragmentManager + .beginTransaction() + .replace(R.id.fragment_container, AccountListFragment.newInstance(type, id)) + .commit() + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + onBackPressed() + return true + } + } + return super.onOptionsItemSelected(item) + } + + override fun supportFragmentInjector(): AndroidInjector? { + return dispatchingAndroidInjector + } + + companion object { + private const val EXTRA_TYPE = "type" + private const val EXTRA_ID = "id" + + @JvmStatic + fun newIntent(context: Context, type: Type, id: String? = null): Intent { + return Intent(context, AccountListActivity::class.java).apply { + putExtra(EXTRA_TYPE, type) + putExtra(EXTRA_ID, id) + } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountAdapter.java index 66c8c5142..5c52e39ee 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountAdapter.java @@ -30,7 +30,6 @@ public abstract class AccountAdapter extends RecyclerView.Adapter { static final int VIEW_TYPE_ACCOUNT = 0; static final int VIEW_TYPE_FOOTER = 1; - List accountList; AccountActionListener accountActionListener; private boolean bottomLoading; @@ -60,7 +59,7 @@ public abstract class AccountAdapter extends RecyclerView.Adapter { notifyDataSetChanged(); } - public void addItems(List newAccounts) { + public void addItems(@NonNull List newAccounts) { int end = accountList.size(); Account last = accountList.get(end - 1); if (last != null && !findAccount(newAccounts, last.getId())) { @@ -82,7 +81,7 @@ public abstract class AccountAdapter extends RecyclerView.Adapter { } } - private static boolean findAccount(List accounts, String id) { + private static boolean findAccount(@NonNull List accounts, String id) { for (Account account : accounts) { if (account.getId().equals(id)) { return true; @@ -101,7 +100,7 @@ public abstract class AccountAdapter extends RecyclerView.Adapter { return account; } - public void addItem(Account account, int position) { + public void addItem(@NonNull Account account, int position) { if (position < 0 || position > accountList.size()) { return; } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java index c3b5e26ec..c60a56374 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java @@ -21,6 +21,7 @@ import com.keylesspalace.tusky.entity.Card; import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.util.CustomURLSpan; +import com.keylesspalace.tusky.util.HtmlUtils; import com.keylesspalace.tusky.util.LinkHelper; import com.keylesspalace.tusky.viewdata.StatusViewData; import com.squareup.picasso.Picasso; @@ -30,6 +31,7 @@ import java.text.NumberFormat; import java.util.Date; import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; class StatusDetailedViewHolder extends StatusBaseViewHolder { private TextView reblogs; @@ -40,6 +42,10 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder { private TextView cardTitle; private TextView cardDescription; private TextView cardUrl; + private View infoDivider; + private View favReblogInfoContainer; + + private NumberFormat numberFormat = NumberFormat.getNumberInstance(); StatusDetailedViewHolder(View view) { super(view, false); @@ -51,6 +57,8 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder { cardTitle = view.findViewById(R.id.card_title); cardDescription = view.findViewById(R.id.card_description); cardUrl = view.findViewById(R.id.card_link); + infoDivider = view.findViewById(R.id.status_info_divider); + favReblogInfoContainer = view.findViewById(R.id.status_reblog_fav_info); } @Override @@ -68,6 +76,45 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder { } } + private void setReblogAndFavCount(int reblogCount, int favCount, StatusActionListener listener) { + + if(reblogCount > 0) { + String reblogCountString = numberFormat.format(reblogCount); + reblogs.setText(HtmlUtils.fromHtml(reblogs.getResources().getQuantityString(R.plurals.reblogs, reblogCount, reblogCountString))); + reblogs.setVisibility(View.VISIBLE); + } else { + reblogs.setVisibility(View.GONE); + } + if(favCount > 0) { + String favCountString = numberFormat.format(favCount); + favourites.setText(HtmlUtils.fromHtml(favourites.getResources().getQuantityString(R.plurals.favs, favCount, favCountString))); + favourites.setVisibility(View.VISIBLE); + } else { + favourites.setVisibility(View.GONE); + } + + if(reblogs.getVisibility() == View.GONE && favourites.getVisibility() == View.GONE) { + infoDivider.setVisibility(View.GONE); + favReblogInfoContainer.setVisibility(View.GONE); + } else { + infoDivider.setVisibility(View.VISIBLE); + favReblogInfoContainer.setVisibility(View.VISIBLE); + } + + reblogs.setOnClickListener( v -> { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + listener.onShowReblogs(position); + } + }); + favourites.setOnClickListener( v -> { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + listener.onShowFavs(position); + } + }); + } + private void setApplication(@Nullable Status.Application app) { if (app != null) { @@ -91,12 +138,11 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder { boolean mediaPreviewEnabled) { super.setupWithStatus(status, listener, mediaPreviewEnabled); - NumberFormat numberFormat = NumberFormat.getNumberInstance(); + setReblogAndFavCount(status.getReblogsCount(), status.getFavouritesCount(), listener); - reblogs.setText(numberFormat.format(status.getReblogsCount())); - favourites.setText(numberFormat.format(status.getFavouritesCount())); setApplication(status.getApplication()); + View.OnLongClickListener longClickListener = view -> { TextView textView = (TextView)view; ClipboardManager clipboard = (ClipboardManager) view.getContext().getSystemService(Context.CLIPBOARD_SERVICE); diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.java deleted file mode 100644 index 0668a73f2..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.java +++ /dev/null @@ -1,404 +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 . */ - -package com.keylesspalace.tusky.fragment; - -import android.content.Context; -import android.content.Intent; -import android.graphics.drawable.Drawable; -import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import com.google.android.material.snackbar.Snackbar; -import androidx.recyclerview.widget.DividerItemDecoration; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import com.keylesspalace.tusky.AccountActivity; -import com.keylesspalace.tusky.R; -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.MutesAdapter; -import com.keylesspalace.tusky.di.Injectable; -import com.keylesspalace.tusky.entity.Account; -import com.keylesspalace.tusky.entity.Relationship; -import com.keylesspalace.tusky.interfaces.AccountActionListener; -import com.keylesspalace.tusky.network.MastodonApi; -import com.keylesspalace.tusky.util.HttpHeaderLink; -import com.keylesspalace.tusky.util.ThemeUtils; -import com.keylesspalace.tusky.view.EndlessOnScrollListener; - -import java.util.List; - -import javax.inject.Inject; - -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; - -public class AccountListFragment extends BaseFragment implements AccountActionListener, - Injectable { - private static final String TAG = "AccountList"; // logging tag - - public AccountListFragment() { - } - - public enum Type { - FOLLOWS, - FOLLOWERS, - BLOCKS, - MUTES, - FOLLOW_REQUESTS, - } - - @Inject - public MastodonApi api; - - private Type type; - private String accountId; - private LinearLayoutManager layoutManager; - private RecyclerView recyclerView; - private EndlessOnScrollListener scrollListener; - private AccountAdapter adapter; - private boolean fetching = false; - private String bottomId; - - public static AccountListFragment newInstance(Type type) { - Bundle arguments = new Bundle(); - AccountListFragment fragment = new AccountListFragment(); - arguments.putSerializable("type", type); - fragment.setArguments(arguments); - return fragment; - } - - public static AccountListFragment newInstance(Type type, String accountId) { - Bundle arguments = new Bundle(); - AccountListFragment fragment = new AccountListFragment(); - arguments.putSerializable("type", type); - arguments.putString("accountId", accountId); - fragment.setArguments(arguments); - return fragment; - } - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - Bundle arguments = getArguments(); - type = (Type) arguments.getSerializable("type"); - accountId = arguments.getString("accountId"); - api = null; - } - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - - View rootView = inflater.inflate(R.layout.fragment_account_list, container, false); - - Context context = getContext(); - recyclerView = rootView.findViewById(R.id.recycler_view); - recyclerView.setHasFixedSize(true); - layoutManager = new LinearLayoutManager(context); - recyclerView.setLayoutManager(layoutManager); - DividerItemDecoration divider = new DividerItemDecoration( - context, layoutManager.getOrientation()); - Drawable drawable = ThemeUtils.getDrawable(context, R.attr.status_divider_drawable, - R.drawable.status_divider_dark); - divider.setDrawable(drawable); - recyclerView.addItemDecoration(divider); - scrollListener = null; - if (type == Type.BLOCKS) { - adapter = new BlocksAdapter(this); - } else if (type == Type.MUTES) { - adapter = new MutesAdapter(this); - } else if (type == Type.FOLLOW_REQUESTS) { - adapter = new FollowRequestsAdapter(this); - } else { - adapter = new FollowAdapter(this); - } - recyclerView.setAdapter(adapter); - - - return rootView; - } - - @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - // Just use the basic scroll listener to load more accounts. - scrollListener = new EndlessOnScrollListener(layoutManager) { - @Override - public void onLoadMore(int totalItemsCount, RecyclerView view) { - AccountListFragment.this.onLoadMore(); - } - }; - - recyclerView.addOnScrollListener(scrollListener); - - fetchAccounts(null); - - } - - @Override - public void onViewAccount(String id) { - Context context = getContext(); - if(context != null) { - Intent intent = AccountActivity.getIntent(context, id); - startActivity(intent); - } - } - - @Override - public void onMute(final boolean mute, final String id, final int position) { - Callback callback = new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - if (response.isSuccessful()) { - onMuteSuccess(mute, id, position); - } else { - onMuteFailure(mute, id); - } - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - onMuteFailure(mute, id); - } - }; - - Call call; - if (!mute) { - call = api.unmuteAccount(id); - } else { - call = api.muteAccount(id); - } - callList.add(call); - call.enqueue(callback); - } - - private void onMuteSuccess(boolean muted, final String id, final int position) { - if (muted) { - return; - } - final MutesAdapter mutesAdapter = (MutesAdapter) adapter; - final Account unmutedUser = mutesAdapter.removeItem(position); - View.OnClickListener listener = v -> { - mutesAdapter.addItem(unmutedUser, position); - onMute(true, id, position); - }; - Snackbar.make(recyclerView, R.string.confirmation_unmuted, Snackbar.LENGTH_LONG) - .setAction(R.string.action_undo, listener) - .show(); - } - - private void onMuteFailure(boolean mute, String id) { - String verb; - if (mute) { - verb = "mute"; - } else { - verb = "unmute"; - } - Log.e(TAG, String.format("Failed to %s account id %s", verb, id)); - } - - @Override - public void onBlock(final boolean block, final String id, final int position) { - Callback cb = new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - if (response.isSuccessful()) { - onBlockSuccess(block, id, position); - } else { - onBlockFailure(block, id); - } - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - onBlockFailure(block, id); - } - }; - - Call call; - if (!block) { - call = api.unblockAccount(id); - } else { - call = api.blockAccount(id); - } - callList.add(call); - call.enqueue(cb); - } - - private void onBlockSuccess(boolean blocked, final String id, final int position) { - if (blocked) { - return; - } - final BlocksAdapter blocksAdapter = (BlocksAdapter) adapter; - final Account unblockedUser = blocksAdapter.removeItem(position); - View.OnClickListener listener = v -> { - blocksAdapter.addItem(unblockedUser, position); - onBlock(true, id, position); - }; - Snackbar.make(recyclerView, R.string.confirmation_unblocked, Snackbar.LENGTH_LONG) - .setAction(R.string.action_undo, listener) - .show(); - } - - private void onBlockFailure(boolean block, String id) { - String verb; - if (block) { - verb = "block"; - } else { - verb = "unblock"; - } - Log.e(TAG, String.format("Failed to %s account id %s", verb, id)); - } - - @Override - public void onRespondToFollowRequest(final boolean accept, final String accountId, - final int position) { - - Callback callback = new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - if (response.isSuccessful()) { - onRespondToFollowRequestSuccess(position); - } else { - onRespondToFollowRequestFailure(accept, accountId); - } - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - onRespondToFollowRequestFailure(accept, accountId); - } - }; - - Call call; - if (accept) { - call = api.authorizeFollowRequest(accountId); - } else { - call = api.rejectFollowRequest(accountId); - } - callList.add(call); - call.enqueue(callback); - } - - private void onRespondToFollowRequestSuccess(int position) { - FollowRequestsAdapter followRequestsAdapter = (FollowRequestsAdapter) adapter; - followRequestsAdapter.removeItem(position); - } - - private void onRespondToFollowRequestFailure(boolean accept, String accountId) { - String verb; - if (accept) { - verb = "accept"; - } else { - verb = "reject"; - } - String message = String.format("Failed to %s account id %s.", verb, accountId); - Log.e(TAG, message); - } - - private Call> getFetchCallByListType(Type type, String fromId) { - switch (type) { - default: - case FOLLOWS: - return api.accountFollowing(accountId, fromId, null, null); - case FOLLOWERS: - return api.accountFollowers(accountId, fromId, null, null); - case BLOCKS: - return api.blocks(fromId, null, null); - case MUTES: - return api.mutes(fromId, null, null); - case FOLLOW_REQUESTS: - return api.followRequests(fromId, null, null); - } - } - - private void fetchAccounts(String id) { - if (fetching) { - return; - } - fetching = true; - - if (id != null) { - recyclerView.post(() -> adapter.setBottomLoading(true)); - } - - Callback> cb = new Callback>() { - @Override - public void onResponse(@NonNull Call> call, @NonNull Response> response) { - if (response.isSuccessful()) { - String linkHeader = response.headers().get("Link"); - onFetchAccountsSuccess(response.body(), linkHeader); - } else { - onFetchAccountsFailure(new Exception(response.message())); - } - } - - @Override - public void onFailure(@NonNull Call> call, @NonNull Throwable t) { - onFetchAccountsFailure((Exception) t); - } - }; - Call> listCall = getFetchCallByListType(type, id); - callList.add(listCall); - listCall.enqueue(cb); - } - - private void onFetchAccountsSuccess(List accounts, String linkHeader) { - adapter.setBottomLoading(false); - - - List links = HttpHeaderLink.parse(linkHeader); - 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); - } else { - adapter.update(accounts); - } - - bottomId = fromId; - - fetching = false; - - adapter.setBottomLoading(false); - } - - private void onFetchAccountsFailure(Exception exception) { - fetching = false; - Log.e(TAG, "Fetch failure: " + exception.getMessage()); - } - - private void onLoadMore() { - if(bottomId == null) { - return; - } - fetchAccounts(bottomId); - } - -} diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt new file mode 100644 index 000000000..136c2763f --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt @@ -0,0 +1,336 @@ +/* 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.os.Bundle +import com.google.android.material.snackbar.Snackbar +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup + +import com.keylesspalace.tusky.AccountActivity +import com.keylesspalace.tusky.AccountListActivity.Type +import com.keylesspalace.tusky.BaseActivity +import com.keylesspalace.tusky.R +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.MutesAdapter +import com.keylesspalace.tusky.di.Injectable +import com.keylesspalace.tusky.entity.Account +import com.keylesspalace.tusky.entity.Relationship +import com.keylesspalace.tusky.interfaces.AccountActionListener +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.HttpHeaderLink +import com.keylesspalace.tusky.util.ThemeUtils +import com.keylesspalace.tusky.view.EndlessOnScrollListener +import kotlinx.android.synthetic.main.fragment_account_list.* + +import javax.inject.Inject + +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response + +class AccountListFragment : BaseFragment(), AccountActionListener, Injectable { + + @Inject + lateinit var api: MastodonApi + + private lateinit var type: Type + private var id: String? = null + private lateinit var scrollListener: EndlessOnScrollListener + private lateinit var adapter: AccountAdapter + private var fetching = false + private var bottomId: String? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + type = arguments?.getSerializable(ARG_TYPE) as Type + id = arguments?.getString(ARG_ID) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + return inflater.inflate(R.layout.fragment_account_list, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + recyclerView.setHasFixedSize(true) + val layoutManager = LinearLayoutManager(context) + recyclerView.layoutManager = layoutManager + val divider = DividerItemDecoration(context, layoutManager.orientation) + val drawable = ThemeUtils.getDrawable(context, R.attr.status_divider_drawable, R.drawable.status_divider_dark) + divider.setDrawable(drawable) + recyclerView.addItemDecoration(divider) + + adapter = when(type) { + Type.BLOCKS -> BlocksAdapter(this) + Type.MUTES -> MutesAdapter(this) + Type.FOLLOW_REQUESTS -> FollowRequestsAdapter(this) + else -> FollowAdapter(this) + } + recyclerView.adapter = adapter + + scrollListener = object : EndlessOnScrollListener(layoutManager) { + override fun onLoadMore(totalItemsCount: Int, view: RecyclerView) { + if (bottomId == null) { + return + } + fetchAccounts(bottomId) + } + } + + recyclerView.addOnScrollListener(scrollListener) + + fetchAccounts() + } + + override fun onViewAccount(id: String) { + (activity as BaseActivity?)?.let { + val intent = AccountActivity.getIntent(it, id) + it.startActivityWithSlideInAnimation(intent) + } + } + + override fun onMute(mute: Boolean, id: String, position: Int) { + val callback = object : Callback { + override fun onResponse(call: Call, response: Response) { + if (response.isSuccessful) { + onMuteSuccess(mute, id, position) + } else { + onMuteFailure(mute, id) + } + } + + override fun onFailure(call: Call, t: Throwable) { + onMuteFailure(mute, id) + } + } + + val call = if (!mute) { + api.unmuteAccount(id) + } else { + api.muteAccount(id) + } + callList.add(call) + call.enqueue(callback) + } + + private fun onMuteSuccess(muted: Boolean, id: String, position: Int) { + if (muted) { + return + } + val mutesAdapter = adapter as MutesAdapter + val unmutedUser = mutesAdapter.removeItem(position) + + if(unmutedUser != null) { + Snackbar.make(recyclerView, R.string.confirmation_unmuted, Snackbar.LENGTH_LONG) + .setAction(R.string.action_undo) { + mutesAdapter.addItem(unmutedUser, position) + onMute(true, id, position) + } + .show() + } + } + + private fun onMuteFailure(mute: Boolean, accountId: String) { + val verb = if (mute) { + "mute" + } else { + "unmute" + } + Log.e(TAG, "Failed to $verb account id $accountId") + } + + override fun onBlock(block: Boolean, id: String, position: Int) { + val cb = object : Callback { + override fun onResponse(call: Call, response: Response) { + if (response.isSuccessful) { + onBlockSuccess(block, id, position) + } else { + onBlockFailure(block, id) + } + } + + override fun onFailure(call: Call, t: Throwable) { + onBlockFailure(block, id) + } + } + + val call = if (!block) { + api.unblockAccount(id) + } else { + api.blockAccount(id) + } + callList.add(call) + call.enqueue(cb) + } + + private fun onBlockSuccess(blocked: Boolean, id: String, position: Int) { + if (blocked) { + return + } + val blocksAdapter = adapter as BlocksAdapter + val unblockedUser = blocksAdapter.removeItem(position) + + if(unblockedUser != null) { + Snackbar.make(recyclerView, R.string.confirmation_unblocked, Snackbar.LENGTH_LONG) + .setAction(R.string.action_undo) { + blocksAdapter.addItem(unblockedUser, position) + onBlock(true, id, position) + } + .show() + } + } + + private fun onBlockFailure(block: Boolean, accountId: String) { + val verb = if (block) { + "block" + } else { + "unblock" + } + Log.e(TAG, "Failed to $verb account accountId $accountId") + } + + override fun onRespondToFollowRequest(accept: Boolean, accountId: String, + position: Int) { + + val callback = object : Callback { + override fun onResponse(call: Call, response: Response) { + if (response.isSuccessful) { + onRespondToFollowRequestSuccess(position) + } else { + onRespondToFollowRequestFailure(accept, accountId) + } + } + + override fun onFailure(call: Call, t: Throwable) { + onRespondToFollowRequestFailure(accept, accountId) + } + } + + val call = if (accept) { + api.authorizeFollowRequest(accountId) + } else { + api.rejectFollowRequest(accountId) + } + callList.add(call) + call.enqueue(callback) + } + + private fun onRespondToFollowRequestSuccess(position: Int) { + val followRequestsAdapter = adapter as FollowRequestsAdapter + followRequestsAdapter.removeItem(position) + } + + private fun onRespondToFollowRequestFailure(accept: Boolean, accountId: String) { + val verb = if (accept) { + "accept" + } else { + "reject" + } + Log.e(TAG, "Failed to $verb account id $accountId.") + } + + private fun getFetchCallByListType(type: Type, fromId: String?): Call> { + return when (type) { + Type.FOLLOWS -> api.accountFollowing(id, fromId) + Type.FOLLOWERS -> api.accountFollowers(id, fromId) + Type.BLOCKS -> api.blocks(fromId) + Type.MUTES -> api.mutes(fromId) + Type.FOLLOW_REQUESTS -> api.followRequests(fromId) + Type.REBLOGGED -> api.statusRebloggedBy(id, fromId) + Type.FAVOURITED -> api.statusFavouritedBy(id, fromId) + } + } + + private fun fetchAccounts(id: String? = null) { + if (fetching) { + return + } + fetching = true + + if (id != null) { + recyclerView.post { adapter.setBottomLoading(true) } + } + + val cb = object : Callback> { + override fun onResponse(call: Call>, response: Response>) { + val accountList = response.body() + if (response.isSuccessful && accountList != null) { + val linkHeader = response.headers().get("Link") + onFetchAccountsSuccess(accountList, linkHeader) + } else { + onFetchAccountsFailure(Exception(response.message())) + } + } + + override fun onFailure(call: Call>, t: Throwable) { + onFetchAccountsFailure(t as Exception) + } + } + val listCall = getFetchCallByListType(type, id) + callList.add(listCall) + listCall.enqueue(cb) + } + + private fun onFetchAccountsSuccess(accounts: List, linkHeader: String?) { + adapter.setBottomLoading(false) + + val links = HttpHeaderLink.parse(linkHeader) + val next = HttpHeaderLink.findByRelationType(links, "next") + val fromId = next?.uri?.getQueryParameter("max_id") + + if (adapter.itemCount > 0) { + adapter.addItems(accounts) + } else { + adapter.update(accounts) + } + + bottomId = fromId + + fetching = false + + } + + private fun onFetchAccountsFailure(exception: Exception) { + fetching = false + Log.e(TAG, "Fetch failure", exception) + } + + companion object { + private const val TAG = "AccountList" // logging tag + private const val ARG_TYPE = "type" + private const val ARG_ID = "id" + + fun newInstance(type: Type, id: String? = null): AccountListFragment { + return AccountListFragment().apply { + arguments = Bundle(2).apply { + putSerializable(ARG_TYPE, type) + putString(ARG_ID, id) + } + } + } + } + +} diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java index 777001cfc..4678daa0f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java @@ -18,6 +18,7 @@ package com.keylesspalace.tusky.fragment; import androidx.arch.core.util.Function; import androidx.lifecycle.Lifecycle; import android.content.Context; +import android.content.Intent; import android.content.SharedPreferences; import android.graphics.drawable.Drawable; import android.os.Bundle; @@ -37,6 +38,8 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import com.keylesspalace.tusky.AccountListActivity; +import com.keylesspalace.tusky.BaseActivity; import com.keylesspalace.tusky.BuildConfig; import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.ViewThreadActivity; @@ -237,7 +240,8 @@ public final class ViewThreadFragment extends SFragment implements @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { - setReblogForStatus(position, status, reblog); + updateStatus(position, response.body()); + eventHub.dispatch(new ReblogEvent(status.getId(), reblog)); } } @@ -250,24 +254,6 @@ public final class ViewThreadFragment extends SFragment implements }); } - private void setReblogForStatus(int position, Status status, boolean reblog) { - status.setReblogged(reblog); - - if (status.getReblog() != null) { - status.getReblog().setReblogged(reblog); - } - - StatusViewData.Concrete viewdata = statuses.getPairedItem(position); - - StatusViewData.Builder viewDataBuilder = new StatusViewData.Builder((viewdata)); - viewDataBuilder.setReblogged(reblog); - - StatusViewData.Concrete newViewData = viewDataBuilder.createStatusViewData(); - - statuses.setPairedItem(position, newViewData); - adapter.setItem(position, newViewData, true); - } - @Override public void onFavourite(final boolean favourite, final int position) { final Status status = statuses.get(position); @@ -275,7 +261,8 @@ public final class ViewThreadFragment extends SFragment implements @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { - setFavForStatus(position, status, favourite); + updateStatus(position, response.body()); + eventHub.dispatch(new FavoriteEvent(status.getId(), favourite)); } } @@ -288,22 +275,20 @@ public final class ViewThreadFragment extends SFragment implements }); } - private void setFavForStatus(int position, Status status, boolean favourite) { - status.setFavourited(favourite); + private void updateStatus(int position, Status status) { + if(position >= 0 && position < statuses.size()) { + + statuses.set(position, status); + + if(position == statusIndex && card != null) { + StatusViewData.Concrete viewData = new StatusViewData.Builder(statuses.getPairedItem(position)) + .setCard(card) + .createStatusViewData(); + statuses.setPairedItem(position, viewData); + } + adapter.setItem(position, statuses.getPairedItem(position), true); - if (status.getReblog() != null) { - status.getReblog().setFavourited(favourite); } - - StatusViewData.Concrete viewdata = statuses.getPairedItem(position); - - StatusViewData.Builder viewDataBuilder = new StatusViewData.Builder((viewdata)); - viewDataBuilder.setFavourited(favourite); - - StatusViewData.Concrete newViewData = viewDataBuilder.createStatusViewData(); - - statuses.setPairedItem(position, newViewData); - adapter.setItem(position, newViewData, true); } @Override @@ -355,10 +340,24 @@ public final class ViewThreadFragment extends SFragment implements } @Override - public void onLoadMore(int pos) { + public void onLoadMore(int position) { } + @Override + public void onShowReblogs(int position) { + String statusId = statuses.get(position).getId(); + Intent intent = AccountListActivity.newIntent(getContext(), AccountListActivity.Type.REBLOGGED, statusId); + ((BaseActivity) getActivity()).startActivityWithSlideInAnimation(intent); + } + + @Override + public void onShowFavs(int position) { + String statusId = statuses.get(position).getId(); + Intent intent = AccountListActivity.newIntent(getContext(), AccountListActivity.Type.FAVOURITED, statusId); + ((BaseActivity) getActivity()).startActivityWithSlideInAnimation(intent); + } + @Override public void onContentCollapsedChange(boolean isCollapsed, int position) { if (position < 0 || position >= statuses.size()) { @@ -615,14 +614,44 @@ public final class ViewThreadFragment extends SFragment implements Pair posAndStatus = findStatusAndPos(event.getStatusId()); if (posAndStatus == null) return; //noinspection ConstantConditions - setFavForStatus(posAndStatus.first, posAndStatus.second, event.getFavourite()); + boolean favourite = event.getFavourite(); + posAndStatus.second.setFavourited(favourite); + + if (posAndStatus.second.getReblog() != null) { + posAndStatus.second.getReblog().setFavourited(favourite); + } + + StatusViewData.Concrete viewdata = statuses.getPairedItem(posAndStatus.first); + + StatusViewData.Builder viewDataBuilder = new StatusViewData.Builder((viewdata)); + viewDataBuilder.setFavourited(favourite); + + StatusViewData.Concrete newViewData = viewDataBuilder.createStatusViewData(); + + statuses.setPairedItem(posAndStatus.first, newViewData); + adapter.setItem(posAndStatus.first, newViewData, true); } private void handleReblogEvent(ReblogEvent event) { Pair posAndStatus = findStatusAndPos(event.getStatusId()); if (posAndStatus == null) return; //noinspection ConstantConditions - setReblogForStatus(posAndStatus.first, posAndStatus.second, event.getReblog()); + boolean reblog = event.getReblog(); + posAndStatus.second.setReblogged(reblog); + + if (posAndStatus.second.getReblog() != null) { + posAndStatus.second.getReblog().setReblogged(reblog); + } + + StatusViewData.Concrete viewdata = statuses.getPairedItem(posAndStatus.first); + + StatusViewData.Builder viewDataBuilder = new StatusViewData.Builder((viewdata)); + viewDataBuilder.setReblogged(reblog); + + StatusViewData.Concrete newViewData = viewDataBuilder.createStatusViewData(); + + statuses.setPairedItem(posAndStatus.first, newViewData); + adapter.setItem(posAndStatus.first, newViewData, true); } private void handleStatusComposedEvent(StatusComposedEvent event) { diff --git a/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java b/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java index 79c42db92..1ae56d464 100644 --- a/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java +++ b/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java @@ -37,4 +37,17 @@ public interface StatusActionListener extends LinkListener { * @param position The position of the status in the list. */ void onContentCollapsedChange(boolean isCollapsed, int position); + + /** + * called when the reblog count has been clicked + * @param position The position of the status in the list. + */ + default void onShowReblogs(int position) {} + + /** + * called when the favourite count has been clicked + * @param position The position of the status in the list. + */ + default void onShowFavs(int position) {} + } 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 01945b765..a837b1462 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java @@ -51,6 +51,10 @@ import retrofit2.http.Part; import retrofit2.http.Path; import retrofit2.http.Query; + +/** + * for documentation of the Mastodon REST API see https://docs.joinmastodon.org/api/ + */ public interface MastodonApi { String ENDPOINT_AUTHORIZE = "/oauth/authorize"; String DOMAIN_HEADER = "domain"; @@ -131,16 +135,12 @@ public interface MastodonApi { @GET("api/v1/statuses/{id}/reblogged_by") Call> statusRebloggedBy( @Path("id") String statusId, - @Query("max_id") String maxId, - @Query("since_id") String sinceId, - @Query("limit") Integer limit); + @Query("max_id") String maxId); @GET("api/v1/statuses/{id}/favourited_by") Call> statusFavouritedBy( @Path("id") String statusId, - @Query("max_id") String maxId, - @Query("since_id") String sinceId, - @Query("limit") Integer limit); + @Query("max_id") String maxId); @DELETE("api/v1/statuses/{id}") Call deleteStatus(@Path("id") String statusId); @@ -218,16 +218,12 @@ public interface MastodonApi { @GET("api/v1/accounts/{id}/followers") Call> accountFollowers( @Path("id") String accountId, - @Query("max_id") String maxId, - @Query("since_id") String sinceId, - @Query("limit") Integer limit); + @Query("max_id") String maxId); @GET("api/v1/accounts/{id}/following") Call> accountFollowing( @Path("id") String accountId, - @Query("max_id") String maxId, - @Query("since_id") String sinceId, - @Query("limit") Integer limit); + @Query("max_id") String maxId); @FormUrlEncoded @POST("api/v1/accounts/{id}/follow") @@ -252,16 +248,10 @@ public interface MastodonApi { Call> relationships(@Query("id[]") List accountIds); @GET("api/v1/blocks") - Call> blocks( - @Query("max_id") String maxId, - @Query("since_id") String sinceId, - @Query("limit") Integer limit); + Call> blocks(@Query("max_id") String maxId); @GET("api/v1/mutes") - Call> mutes( - @Query("max_id") String maxId, - @Query("since_id") String sinceId, - @Query("limit") Integer limit); + Call> mutes(@Query("max_id") String maxId); @GET("api/v1/favourites") Call> favourites( @@ -270,10 +260,7 @@ public interface MastodonApi { @Query("limit") Integer limit); @GET("api/v1/follow_requests") - Call> followRequests( - @Query("max_id") String maxId, - @Query("since_id") String sinceId, - @Query("limit") Integer limit); + Call> followRequests(@Query("max_id") String maxId); @POST("api/v1/follow_requests/{id}/authorize") Call authorizeFollowRequest(@Path("id") String accountId); diff --git a/app/src/main/res/layout/fragment_account_list.xml b/app/src/main/res/layout/fragment_account_list.xml index 6add5b63e..591c4a9d7 100644 --- a/app/src/main/res/layout/fragment_account_list.xml +++ b/app/src/main/res/layout/fragment_account_list.xml @@ -1,6 +1,6 @@ \ No newline at end of file diff --git a/app/src/main/res/layout/item_status_detailed.xml b/app/src/main/res/layout/item_status_detailed.xml index 5008f66a9..a99d59e07 100644 --- a/app/src/main/res/layout/item_status_detailed.xml +++ b/app/src/main/res/layout/item_status_detailed.xml @@ -15,8 +15,8 @@ android:id="@+id/status_avatar" android:layout_width="48dp" android:layout_height="48dp" - android:layout_marginEnd="14dp" android:layout_marginTop="14dp" + android:layout_marginEnd="14dp" android:contentDescription="@string/action_view_profile" android:scaleType="centerCrop" tools:src="@drawable/avatar_default" /> @@ -25,8 +25,8 @@ android:id="@+id/status_name_bar" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginBottom="8dp" android:layout_marginTop="14dp" + android:layout_marginBottom="8dp" android:layout_toEndOf="@+id/status_avatar" android:gravity="center_vertical" android:minHeight="48dp" @@ -71,15 +71,15 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@+id/status_content_warning_description" - android:layout_marginBottom="4dp" android:layout_marginTop="4dp" + android:layout_marginBottom="4dp" android:background="?attr/content_warning_button" - android:minHeight="0dp" android:minWidth="160dp" - android:paddingBottom="4dp" + android:minHeight="0dp" android:paddingLeft="16dp" - android:paddingRight="16dp" android:paddingTop="4dp" + android:paddingRight="16dp" + android:paddingBottom="4dp" android:textAllCaps="true" android:textOff="@string/status_content_warning_show_more" android:textOn="@string/status_content_warning_show_less" @@ -118,10 +118,10 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" - android:paddingBottom="6dp" android:paddingLeft="6dp" + android:paddingTop="6dp" android:paddingRight="6dp" - android:paddingTop="6dp"> + android:paddingBottom="6dp"> + android:layout_marginTop="@dimen/status_media_preview_margin_top" + android:layout_marginBottom="4dp"> + + + + + + + + + + + + android:paddingTop="4dp" + android:paddingBottom="4dp"> - - - - Unpin Pin + + <b>%1$s</b> Favourite + <b>%1$s</b> Favourites + + + + <b>%s</b> Boost + <b>%s</b> Boosts + + + Boosted by + Favourited by +